[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 0f40a9fce5 -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21605061

Change-Id: Iec7c2cb3b6ab265a0800fa574bab20f2557b1b82
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 7393bcd..21ed1eb 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -37,7 +37,9 @@
 import android.os.HandlerExecutor;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.os.WorkSource;
 import android.text.TextUtils;
 import android.util.Log;
@@ -91,6 +93,14 @@
 public class AlarmManager {
     private static final String TAG = "AlarmManager";
 
+    /**
+     * Prefix used by {{@link #makeTag(long, WorkSource)}} to make a tag on behalf of the caller
+     * when the {@link #set(int, long, long, long, OnAlarmListener, Handler, WorkSource)} API is
+     * used. This prefix is a unique sequence of characters to differentiate with other tags that
+     * apps may provide to other APIs that accept a listener callback.
+     */
+    private static final String GENERATED_TAG_PREFIX = "$android.alarm.generated";
+
     /** @hide */
     @IntDef(prefix = { "RTC", "ELAPSED" }, value = {
             RTC_WAKEUP,
@@ -861,6 +871,24 @@
     }
 
     /**
+     * This is only used to make an identifying tag for the deprecated
+     * {@link #set(int, long, long, long, OnAlarmListener, Handler, WorkSource)} API which doesn't
+     * accept a tag. For all other APIs, the tag provided by the app is used, even if it is
+     * {@code null}.
+     */
+    private static String makeTag(long triggerMillis, WorkSource ws) {
+        final StringBuilder tagBuilder = new StringBuilder(GENERATED_TAG_PREFIX);
+
+        tagBuilder.append(":");
+        final int attributionUid =
+                (ws == null || ws.isEmpty()) ? Process.myUid() : ws.getAttributionUid();
+        tagBuilder.append(UserHandle.formatUid(attributionUid));
+        tagBuilder.append(":");
+        tagBuilder.append(triggerMillis);
+        return tagBuilder.toString();
+    }
+
+    /**
      * Direct callback version of {@link #set(int, long, long, long, PendingIntent, WorkSource)}.
      * Note that repeating alarms must use the PendingIntent variant, not an OnAlarmListener.
      * <p>
@@ -875,8 +903,8 @@
     public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
             long intervalMillis, OnAlarmListener listener, Handler targetHandler,
             WorkSource workSource) {
-        setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, null,
-                targetHandler, workSource, null);
+        setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener,
+                makeTag(triggerAtMillis, workSource), targetHandler, workSource, null);
     }
 
     /**
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/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
index a6a3aaf..6a4a52a 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
@@ -4,6 +4,7 @@
       "name": "CtsUsageStatsTestCases",
       "options": [
         {"include-filter": "android.app.usage.cts.UsageStatsTest"},
+        {"include-filter": "android.app.usage.cts.BroadcastResponseStatsTest"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.MediumTest"},
@@ -19,18 +20,6 @@
       ]
     }
   ],
-  "presubmit-large": [
-    {
-      "name": "CtsUsageStatsTestCases",
-      "options": [
-        {"include-filter": "android.app.usage.cts.BroadcastResponseStatsTest"},
-        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
-        {"exclude-annotation": "androidx.test.filters.MediumTest"},
-        {"exclude-annotation": "androidx.test.filters.LargeTest"}
-      ]
-    }
-  ],
   "postsubmit": [
     {
       "name": "CtsUsageStatsTestCases"
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index bb52cfb..5e6be82 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1140,7 +1140,6 @@
   public final class CameraManager {
     method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
-    field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L
   }
 
   public abstract static class CameraManager.AvailabilityCallback {
@@ -2425,6 +2424,7 @@
   public abstract class DreamOverlayService extends android.app.Service {
     ctor public DreamOverlayService();
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
+    method public void onEndDream();
     method public abstract void onStartDream(@NonNull android.view.WindowManager.LayoutParams);
     method public final void requestExit();
     method public final boolean shouldShowComplications();
@@ -3465,7 +3465,7 @@
 
   public class WindowOrganizer {
     ctor public WindowOrganizer();
-    method @RequiresPermission(value=android.Manifest.permission.MANAGE_ACTIVITY_TASKS, conditional=true) public int applySyncTransaction(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.WindowContainerTransactionCallback);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public int applySyncTransaction(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.WindowContainerTransactionCallback);
     method @RequiresPermission(value=android.Manifest.permission.MANAGE_ACTIVITY_TASKS, conditional=true) public void applyTransaction(@NonNull android.window.WindowContainerTransaction);
   }
 
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index c17fbf1..dd95540 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -2523,6 +2523,10 @@
         IAccessibilityServiceConnection connection =
                 AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId);
         if (mInfo != null && connection != null) {
+            if (!mInfo.isWithinParcelableSize()) {
+                throw new IllegalStateException(
+                        "Cannot update service info: size is larger than safe parcelable limits.");
+            }
             try {
                 connection.setServiceInfo(mInfo);
                 mInfo = null;
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index 530de0f..0cbcdb5 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -40,6 +40,7 @@
 import android.graphics.drawable.Drawable;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.Build;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
@@ -1128,6 +1129,15 @@
         return 0;
     }
 
+    /** @hide */
+    public final boolean isWithinParcelableSize() {
+        final Parcel parcel = Parcel.obtain();
+        writeToParcel(parcel, 0);
+        final boolean result = parcel.dataSize() <= IBinder.MAX_IPC_SIZE;
+        parcel.recycle();
+        return result;
+    }
+
     public void writeToParcel(Parcel parcel, int flagz) {
         parcel.writeInt(eventTypes);
         parcel.writeStringArray(packageNames);
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index d24b677..03a97ba 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -997,6 +997,8 @@
 
     private ComponentCallbacksController mCallbacksController;
 
+    @Nullable private IVoiceInteractionManagerService mVoiceInteractionManagerService;
+
     private final WindowControllerCallback mWindowControllerCallback =
             new WindowControllerCallback() {
         /**
@@ -1606,18 +1608,17 @@
 
     private void notifyVoiceInteractionManagerServiceActivityEvent(
             @VoiceInteractionSession.VoiceInteractionActivityEventType int type) {
-
-        final IVoiceInteractionManagerService service =
-                IVoiceInteractionManagerService.Stub.asInterface(
-                        ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
-        if (service == null) {
-            Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
-                    + "VoiceInteractionManagerService");
-            return;
+        if (mVoiceInteractionManagerService == null) {
+            mVoiceInteractionManagerService = IVoiceInteractionManagerService.Stub.asInterface(
+                    ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+            if (mVoiceInteractionManagerService == null) {
+                Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
+                        + "VoiceInteractionManagerService");
+                return;
+            }
         }
-
         try {
-            service.notifyActivityEventChanged(mToken, type);
+            mVoiceInteractionManagerService.notifyActivityEventChanged(mToken, type);
         } catch (RemoteException e) {
             // Empty
         }
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index e003293..53e0a05 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -327,6 +327,20 @@
             "android:activity.applyActivityFlagsForBubbles";
 
     /**
+     * Indicates to apply {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} to the launching shortcut.
+     * @hide
+     */
+    private static final String KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT =
+            "android:activity.applyMultipleTaskFlagForShortcut";
+
+    /**
+     * Indicates to apply {@link Intent#FLAG_ACTIVITY_NO_USER_ACTION} to the launching shortcut.
+     * @hide
+     */
+    private static final String KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT =
+            "android:activity.applyNoUserActionFlagForShortcut";
+
+    /**
      * For Activity transitions, the calling Activity's TransitionListener used to
      * notify the called Activity when the shared element and the exit transitions
      * complete.
@@ -459,6 +473,8 @@
     private boolean mLockTaskMode = false;
     private boolean mDisallowEnterPictureInPictureWhileLaunching;
     private boolean mApplyActivityFlagsForBubbles;
+    private boolean mApplyMultipleTaskFlagForShortcut;
+    private boolean mApplyNoUserActionFlagForShortcut;
     private boolean mTaskAlwaysOnTop;
     private boolean mTaskOverlay;
     private boolean mTaskOverlayCanResume;
@@ -1258,6 +1274,10 @@
                 KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING, false);
         mApplyActivityFlagsForBubbles = opts.getBoolean(
                 KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES, false);
+        mApplyMultipleTaskFlagForShortcut = opts.getBoolean(
+                KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT, false);
+        mApplyNoUserActionFlagForShortcut = opts.getBoolean(
+                KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT, false);
         if (opts.containsKey(KEY_ANIM_SPECS)) {
             Parcelable[] specs = opts.getParcelableArray(KEY_ANIM_SPECS);
             mAnimSpecs = new AppTransitionAnimationSpec[specs.length];
@@ -1844,6 +1864,26 @@
         return mApplyActivityFlagsForBubbles;
     }
 
+    /** @hide */
+    public void setApplyMultipleTaskFlagForShortcut(boolean apply) {
+        mApplyMultipleTaskFlagForShortcut = apply;
+    }
+
+    /** @hide */
+    public boolean isApplyMultipleTaskFlagForShortcut() {
+        return mApplyMultipleTaskFlagForShortcut;
+    }
+
+    /** @hide */
+    public void setApplyNoUserActionFlagForShortcut(boolean apply) {
+        mApplyNoUserActionFlagForShortcut = apply;
+    }
+
+    /** @hide */
+    public boolean isApplyNoUserActionFlagForShortcut() {
+        return mApplyNoUserActionFlagForShortcut;
+    }
+
     /**
      * Sets a launch cookie that can be used to track the activity and task that are launch as a
      * result of this option. If the launched activity is a trampoline that starts another activity
@@ -2175,6 +2215,13 @@
         if (mApplyActivityFlagsForBubbles) {
             b.putBoolean(KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES, mApplyActivityFlagsForBubbles);
         }
+        if (mApplyMultipleTaskFlagForShortcut) {
+            b.putBoolean(KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT,
+                    mApplyMultipleTaskFlagForShortcut);
+        }
+        if (mApplyNoUserActionFlagForShortcut) {
+            b.putBoolean(KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT, true);
+        }
         if (mAnimSpecs != null) {
             b.putParcelableArray(KEY_ANIM_SPECS, mAnimSpecs);
         }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 7f4af8b..0c26d43 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6189,10 +6189,8 @@
         private RemoteViews generateActionButton(Action action, boolean emphasizedMode,
                 StandardTemplateParams p) {
             final boolean tombstone = (action.actionIntent == null);
-            RemoteViews button = new BuilderRemoteViews(mContext.getApplicationInfo(),
-                    emphasizedMode ? getEmphasizedActionLayoutResource()
-                            : tombstone ? getActionTombstoneLayoutResource()
-                                    : getActionLayoutResource());
+            final RemoteViews button = new BuilderRemoteViews(mContext.getApplicationInfo(),
+                    getActionButtonLayoutResource(emphasizedMode, tombstone));
             if (!tombstone) {
                 button.setOnClickPendingIntent(R.id.action0, action.actionIntent);
             }
@@ -6204,6 +6202,12 @@
                 // change the background bgColor
                 CharSequence title = action.title;
                 int buttonFillColor = getColors(p).getSecondaryAccentColor();
+                if (tombstone) {
+                    buttonFillColor = setAlphaComponentByFloatDimen(mContext,
+                            ContrastColorUtil.resolveSecondaryColor(
+                                    mContext, getColors(p).getBackgroundColor(), mInNightMode),
+                            R.dimen.notification_action_disabled_container_alpha);
+                }
                 if (isLegacy()) {
                     title = ContrastColorUtil.clearColorSpans(title);
                 } else {
@@ -6219,8 +6223,14 @@
                     title = ensureColorSpanContrast(title, buttonFillColor);
                 }
                 button.setTextViewText(R.id.action0, processTextSpans(title));
-                final int textColor = ContrastColorUtil.resolvePrimaryColor(mContext,
+                int textColor = ContrastColorUtil.resolvePrimaryColor(mContext,
                         buttonFillColor, mInNightMode);
+                if (tombstone) {
+                    textColor = setAlphaComponentByFloatDimen(mContext,
+                            ContrastColorUtil.resolveSecondaryColor(
+                                    mContext, getColors(p).getBackgroundColor(), mInNightMode),
+                            R.dimen.notification_action_disabled_content_alpha);
+                }
                 button.setTextColor(R.id.action0, textColor);
                 // We only want about 20% alpha for the ripple
                 final int rippleColor = (textColor & 0x00ffffff) | 0x33000000;
@@ -6250,6 +6260,26 @@
             return button;
         }
 
+        private int getActionButtonLayoutResource(boolean emphasizedMode, boolean tombstone) {
+            if (emphasizedMode) {
+                return tombstone ? getEmphasizedTombstoneActionLayoutResource()
+                        : getEmphasizedActionLayoutResource();
+            } else {
+                return tombstone ? getActionTombstoneLayoutResource()
+                        : getActionLayoutResource();
+            }
+        }
+
+        /**
+         * Set the alpha component of {@code color} to be {@code alphaDimenResId}.
+         */
+        private static int setAlphaComponentByFloatDimen(Context context, @ColorInt int color,
+                @DimenRes int alphaDimenResId) {
+            final TypedValue alphaValue = new TypedValue();
+            context.getResources().getValue(alphaDimenResId, alphaValue, true);
+            return ColorUtils.setAlphaComponent(color, Math.round(alphaValue.getFloat() * 255));
+        }
+
         /**
          * Extract the color from a full-length span from the text.
          *
@@ -6729,6 +6759,10 @@
             return R.layout.notification_material_action_emphasized;
         }
 
+        private int getEmphasizedTombstoneActionLayoutResource() {
+            return R.layout.notification_material_action_emphasized_tombstone;
+        }
+
         private int getActionTombstoneLayoutResource() {
             return R.layout.notification_material_action_tombstone;
         }
@@ -7948,8 +7982,6 @@
          * @hide
          */
         public MessagingStyle setShortcutIcon(@Nullable Icon conversationIcon) {
-            // TODO(b/228941516): This icon should be downscaled to avoid using too much memory,
-            // see reduceImageSizes.
             mShortcutIcon = conversationIcon;
             return this;
         }
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index f6d27ad..37a90de 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -317,7 +317,10 @@
 
     /**
      * Intent that is broadcast when the state of {@link #getEffectsSuppressor()} changes.
-     * This broadcast is only sent to registered receivers.
+     *
+     * <p>This broadcast is only sent to registered receivers and (starting from
+     * {@link Build.VERSION_CODES#Q}) receivers in packages that have been granted Do Not
+     * Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
      *
      * @hide
      */
@@ -337,7 +340,10 @@
 
     /**
      * Intent that is broadcast when the state of getNotificationPolicy() changes.
-     * This broadcast is only sent to registered receivers.
+     *
+     * <p>This broadcast is only sent to registered receivers and (starting from
+     * {@link Build.VERSION_CODES#Q}) receivers in packages that have been granted Do Not
+     * Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
      */
     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_NOTIFICATION_POLICY_CHANGED
@@ -345,7 +351,10 @@
 
     /**
      * Intent that is broadcast when the state of getCurrentInterruptionFilter() changes.
-     * This broadcast is only sent to registered receivers.
+     *
+     * <p>This broadcast is only sent to registered receivers and (starting from
+     * {@link Build.VERSION_CODES#Q}) receivers in packages that have been granted Do Not
+     * Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
      */
     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_INTERRUPTION_FILTER_CHANGED
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index a51b9d3..27f9f54 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1406,6 +1406,17 @@
     }
 
     /**
+     * Return the number of entries in the cache.  This is used for testing and has package-only
+     * visibility.
+     * @hide
+     */
+    public int size() {
+        synchronized (mLock) {
+            return mCache.size();
+        }
+    }
+
+    /**
      * Returns a list of caches alive at the current time.
      */
     @GuardedBy("sGlobalLock")
@@ -1612,8 +1623,12 @@
      * @hide
      */
     public static void onTrimMemory() {
-        for (PropertyInvalidatedCache pic : getActiveCaches()) {
-            pic.clear();
+        ArrayList<PropertyInvalidatedCache> activeCaches;
+        synchronized (sGlobalLock) {
+            activeCaches = getActiveCaches();
+        }
+        for (int i = 0; i < activeCaches.size(); i++) {
+            activeCaches.get(i).clear();
         }
     }
 }
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 3bf3067..8ee23aa 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -444,6 +444,7 @@
                 && Objects.equals(shouldDockBigOverlays, that.shouldDockBigOverlays)
                 && Objects.equals(displayCutoutInsets, that.displayCutoutInsets)
                 && getWindowingMode() == that.getWindowingMode()
+                && configuration.uiMode == that.configuration.uiMode
                 && Objects.equals(taskDescription, that.taskDescription)
                 && isFocused == that.isFocused
                 && isVisible == that.isVisible
@@ -472,6 +473,7 @@
                     .equals(that.configuration.windowConfiguration.getBounds()))
                 && (!hasCompatUI() || configuration.getLayoutDirection()
                     == that.configuration.getLayoutDirection())
+                && (!hasCompatUI() || configuration.uiMode == that.configuration.uiMode)
                 && (!hasCompatUI() || isVisible == that.isVisible);
     }
 
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java
index 067a4c3..a34a50c 100644
--- a/core/java/android/app/WallpaperColors.java
+++ b/core/java/android/app/WallpaperColors.java
@@ -27,6 +27,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemProperties;
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Size;
@@ -101,11 +102,13 @@
     // Decides when dark theme is optimal for this wallpaper
     private static final float DARK_THEME_MEAN_LUMINANCE = 0.3f;
     // Minimum mean luminosity that an image needs to have to support dark text
-    private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.7f;
+    private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = SystemProperties.getInt(
+            "persist.wallpapercolors.threshold", 70) / 100f;
     // We also check if the image has dark pixels in it,
     // to avoid bright images with some dark spots.
     private static final float DARK_PIXEL_CONTRAST = 5.5f;
-    private static final float MAX_DARK_AREA = 0.05f;
+    private static final float MAX_DARK_AREA = SystemProperties.getInt(
+            "persist.wallpapercolors.max_dark_area", 5) / 100f;
 
     private final List<Color> mMainColors;
     private final Map<Integer, Integer> mAllColors;
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 67d5148..c8955f7 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6640,6 +6640,13 @@
     public static final int KEYGUARD_DISABLE_IRIS = 1 << 8;
 
     /**
+     * Disable all keyguard shortcuts.
+     *
+     * @hide
+     */
+    public static final int KEYGUARD_DISABLE_SHORTCUTS_ALL = 1 << 9;
+
+    /**
      * NOTE: Please remember to update the DevicePolicyManagerTest's testKeyguardDisabledFeatures
      * CTS test when adding to the list above.
      */
@@ -6682,7 +6689,8 @@
      */
     public static final int ORG_OWNED_PROFILE_KEYGUARD_FEATURES_PARENT_ONLY =
             DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA
-                    | DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS;
+                    | DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS
+                    | DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL;
 
     /**
      * Keyguard features that when set on a normal or organization-owned managed profile, have
@@ -6789,6 +6797,8 @@
      * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE},
      * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY},
      * {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
+     *
+     * @throws SecurityException if called on a parent instance.
      */
     public int getStorageEncryptionStatus() {
         throwIfParentInstance("getStorageEncryptionStatus");
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index d614f30..7de7c67 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4040,7 +4040,7 @@
      * <p>Note: When implementing this method, keep in mind that new services can be added on newer
      * Android releases, so if you're looking for just the explicit names mentioned above, make sure
      * to return {@code null} when you don't recognize the name &mdash; if you throw a
-     * {@link RuntimeException} exception instead, you're app might break on new Android releases.
+     * {@link RuntimeException} exception instead, your app might break on new Android releases.
      *
      * @param name The name of the desired service.
      *
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 2ea0d82..809dc3c4 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5737,18 +5737,17 @@
      * @hide
      */
     public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS =
-            "android.intent.extra.EXTRA_CHOOSER_CUSTOM_ACTIONS";
+            "android.intent.extra.CHOOSER_CUSTOM_ACTIONS";
 
     /**
      * Optional argument to be used with {@link #ACTION_CHOOSER}.
-     * A {@link android.app.PendingIntent} to be sent when the user wants to do payload reselection
-     * in the sharesheet.
-     * A reselection action allows the user to return to the source app to change the content being
-     * shared.
+     * A {@link android.app.PendingIntent} to be sent when the user wants to modify the content that
+     * they're sharing. This can be used to allow the user to return to the source app to, for
+     * example, select different media.
      * @hide
      */
-    public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION =
-            "android.intent.extra.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION";
+    public static final String EXTRA_CHOOSER_MODIFY_SHARE_ACTION =
+            "android.intent.extra.CHOOSER_MODIFY_SHARE_ACTION";
 
     /**
      * An {@code ArrayList} of {@code String} annotations describing content for
@@ -11488,7 +11487,7 @@
     private void toUriInner(StringBuilder uri, String scheme, String defAction,
             String defPackage, int flags) {
         if (scheme != null) {
-            uri.append("scheme=").append(scheme).append(';');
+            uri.append("scheme=").append(Uri.encode(scheme)).append(';');
         }
         if (mAction != null && !mAction.equals(defAction)) {
             uri.append("action=").append(Uri.encode(mAction)).append(';');
diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java
index b1252fd..49d3cac 100644
--- a/core/java/android/content/IntentSender.java
+++ b/core/java/android/content/IntentSender.java
@@ -19,6 +19,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManager.PendingIntentInfo;
+import android.app.ActivityOptions;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Bundle;
 import android.os.Handler;
@@ -158,7 +159,7 @@
      */
     public void sendIntent(Context context, int code, Intent intent,
             OnFinished onFinished, Handler handler) throws SendIntentException {
-        sendIntent(context, code, intent, onFinished, handler, null);
+        sendIntent(context, code, intent, onFinished, handler, null, null /* options */);
     }
 
     /**
@@ -190,6 +191,42 @@
     public void sendIntent(Context context, int code, Intent intent,
             OnFinished onFinished, Handler handler, String requiredPermission)
             throws SendIntentException {
+        sendIntent(context, code, intent, onFinished, handler, requiredPermission,
+                null /* options */);
+    }
+
+    /**
+     * Perform the operation associated with this IntentSender, allowing the
+     * caller to specify information about the Intent to use and be notified
+     * when the send has completed.
+     *
+     * @param context The Context of the caller.  This may be null if
+     * <var>intent</var> is also null.
+     * @param code Result code to supply back to the IntentSender's target.
+     * @param intent Additional Intent data.  See {@link Intent#fillIn
+     * Intent.fillIn()} for information on how this is applied to the
+     * original Intent.  Use null to not modify the original Intent.
+     * @param onFinished The object to call back on when the send has
+     * completed, or null for no callback.
+     * @param handler Handler identifying the thread on which the callback
+     * should happen.  If null, the callback will happen from the thread
+     * pool of the process.
+     * @param requiredPermission Name of permission that a recipient of the PendingIntent
+     * is required to hold.  This is only valid for broadcast intents, and
+     * corresponds to the permission argument in
+     * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}.
+     * If null, no permission is required.
+     * @param options Additional options the caller would like to provide to modify the sending
+     * behavior.  May be built from an {@link ActivityOptions} to apply to an activity start.
+     *
+     * @throws SendIntentException Throws CanceledIntentException if the IntentSender
+     * is no longer allowing more intents to be sent through it.
+     * @hide
+     */
+    public void sendIntent(Context context, int code, Intent intent,
+            OnFinished onFinished, Handler handler, String requiredPermission,
+            @Nullable Bundle options)
+            throws SendIntentException {
         try {
             String resolvedType = intent != null ?
                     intent.resolveTypeIfNeeded(context.getContentResolver())
@@ -199,7 +236,7 @@
                     onFinished != null
                             ? new FinishedDispatcher(this, onFinished, handler)
                             : null,
-                    requiredPermission, null);
+                    requiredPermission, options);
             if (res < 0) {
                 throw new SendIntentException();
             }
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 9e5e8de..bbe99f5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1058,6 +1058,52 @@
     public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id
 
     /**
+     * This change id excludes the packages it is applied to from ignoreOrientationRequest behaviour
+     * that can be enabled by the device manufacturers for the com.android.server.wm.DisplayArea
+     * or for the whole display.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_RESPECT_REQUESTED_ORIENTATION = 236283604L; // buganizer id
+
+    /**
+     * This change id excludes the packages it is applied to from the camera compat force rotation
+     * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION =
+            263959004L; // buganizer id
+
+    /**
+     * This change id excludes the packages it is applied to from activity refresh after camera
+     * compat force rotation treatment. See com.android.server.wm.DisplayRotationCompatPolicy for
+     * context.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id
+
+    /**
+     * This change id makes the packages it is applied to do activity refresh after camera compat
+     * force rotation treatment using "resumed -> paused -> resumed" cycle rather than "resumed ->
+     * ... -> stopped -> ... -> resumed" cycle. See
+     * com.android.server.wm.DisplayRotationCompatPolicy for context.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE =
+            264301586L; // buganizer id
+
+    /**
      * This change id is the gatekeeper for all treatments that force a given min aspect ratio.
      * Enabling this change will allow the following min aspect ratio treatments to be applied:
      * OVERRIDE_MIN_ASPECT_RATIO_MEDIUM
@@ -1152,6 +1198,91 @@
     @Overridable
     public static final long OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS = 263259275L;
 
+    // Compat framework that per-app overrides rely on only supports booleans. That's why we have
+    // multiple OVERRIDE_*_ORIENTATION_* change ids below instead of just one override with
+    // the integer value.
+
+    /**
+     * Enables {@link #SCREEN_ORIENTATION_PORTRAIT}. Unless OVERRIDE_ANY_ORIENTATION
+     * is enabled, this override is used only when no other fixed orientation was specified by the
+     * activity.
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT = 265452344L;
+
+    /**
+     * Enables {@link #SCREEN_ORIENTATION_NOSENSOR}. Unless OVERRIDE_ANY_ORIENTATION
+     * is enabled, this override is used only when no other fixed orientation was specified by the
+     * activity.
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR = 265451093L;
+
+    /**
+     * Enables {@link #SCREEN_ORIENTATION_REVERSE_LANDSCAPE}. Unless OVERRIDE_ANY_ORIENTATION
+     * is enabled, this override is used only when activity specify landscape orientation.
+     * This can help apps that assume that landscape display orientation corresponds to {@link
+     * android.view.Surface#ROTATION_90}, while on some devices it can be {@link
+     * android.view.Surface#ROTATION_270}.
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE = 266124927L;
+
+    /**
+     * When enabled, allows OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE,
+     * OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR and OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+     * to override any orientation requested by the activity.
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_ANY_ORIENTATION = 265464455L;
+
+    /**
+     * When enabled, activates OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE,
+     * OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR and OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+     * only when an app is connected to the camera. See
+     * com.android.server.wm.DisplayRotationCompatPolicy for more context.
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA = 265456536L;
+
+    /**
+     * This override fixes display orientation to landscape natural orientation when a task is
+     * fullscreen. While display rotation is fixed to landscape, the orientation requested by the
+     * activity will be still respected by bounds resolution logic. For instance, if an activity
+     * requests portrait orientation and this override is set, then activity will appear in the
+     * letterbox mode for fixed orientation with the display rotated to the lanscape natural
+     * orientation.
+     *
+     * <p>This override is applicable only when natural orientation of the device is
+     * landscape and display ignores orientation requestes.
+     *
+     * <p>Main use case for this override are camera-using activities that are portrait-only and
+     * assume alignment with natural device orientation. Such activities can automatically be
+     * rotated with com.android.server.wm.DisplayRotationCompatPolicy but not all of them can
+     * handle dynamic rotation and thus can benefit from this override.
+     *
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION = 255940284L;
+
     /**
      * Compares activity window layout min width/height with require space for multi window to
      * determine if it can be put into multi window mode.
@@ -1370,8 +1501,19 @@
      * @hide
      */
     public boolean isFixedOrientation() {
-        return isFixedOrientationLandscape() || isFixedOrientationPortrait()
-                || screenOrientation == SCREEN_ORIENTATION_LOCKED;
+        return isFixedOrientation(screenOrientation);
+    }
+
+    /**
+     * Returns true if the passed activity's orientation is fixed.
+     * @hide
+     */
+    public static boolean isFixedOrientation(@ScreenOrientation int orientation) {
+        return orientation == SCREEN_ORIENTATION_LOCKED
+                // Orientation is fixed to natural display orientation
+                || orientation == SCREEN_ORIENTATION_NOSENSOR
+                || isFixedOrientationLandscape(orientation)
+                || isFixedOrientationPortrait(orientation);
     }
 
     /**
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index 1c1f58a..d4a24af 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -29,6 +29,9 @@
     },
     {
       "path": "system/apex/tests"
+    },
+    {
+      "path": "cts/tests/tests/content/pm/SecureFrp"
     }
   ],
   "presubmit": [
@@ -111,17 +114,6 @@
       ]
     },
     {
-      "name": "CtsSecureFrpInstallTestCases",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    },
-    {
       "name": "CtsSuspendAppsPermissionTestCases",
       "options": [
         {
diff --git a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
index 41f2f9c..b86b97c 100644
--- a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
+++ b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
@@ -17,7 +17,7 @@
 package android.database.sqlite;
 
 /**
- * Thrown if the the bind or column parameter index is out of range
+ * Thrown if the bind or column parameter index is out of range.
  */
 public class SQLiteBindOrColumnIndexOutOfRangeException extends SQLiteException {
     public SQLiteBindOrColumnIndexOutOfRangeException() {}
diff --git a/core/java/android/database/sqlite/SQLiteDiskIOException.java b/core/java/android/database/sqlite/SQLiteDiskIOException.java
index 01b2069..152d90a 100644
--- a/core/java/android/database/sqlite/SQLiteDiskIOException.java
+++ b/core/java/android/database/sqlite/SQLiteDiskIOException.java
@@ -17,7 +17,7 @@
 package android.database.sqlite;
 
 /**
- * An exception that indicates that an IO error occured while accessing the 
+ * Indicates that an IO error occurred while accessing the
  * SQLite database file.
  */
 public class SQLiteDiskIOException extends SQLiteException {
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 5291d2b..7ccf07a 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -29,7 +29,6 @@
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.ActivityThread;
 import android.app.AppOpsManager;
-import android.app.compat.CompatChanges;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.graphics.ImageFormat;
@@ -47,7 +46,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemProperties;
 import android.renderscript.Allocation;
 import android.renderscript.Element;
 import android.renderscript.RSIllegalArgumentException;
@@ -284,14 +282,6 @@
      */
     public native static int getNumberOfCameras();
 
-    private static final boolean sLandscapeToPortrait =
-            SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false);
-
-    private static boolean shouldOverrideToPortrait() {
-        return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT)
-                && sLandscapeToPortrait;
-    }
-
     /**
      * Returns the information about a particular camera.
      * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1.
@@ -301,7 +291,8 @@
      *    low-level failure).
      */
     public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) {
-        boolean overrideToPortrait = shouldOverrideToPortrait();
+        boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(
+                ActivityThread.currentApplication().getApplicationContext());
 
         _getCameraInfo(cameraId, overrideToPortrait, cameraInfo);
         IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
@@ -498,7 +489,8 @@
             mEventHandler = null;
         }
 
-        boolean overrideToPortrait = shouldOverrideToPortrait();
+        boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(
+                ActivityThread.currentApplication().getApplicationContext());
         return native_setup(new WeakReference<Camera>(this), cameraId,
                 ActivityThread.currentOpPackageName(), overrideToPortrait);
     }
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 10a7538..2f81e0c 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -880,8 +880,8 @@
      * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code VIDEO_CALL}</td> <td colspan="3" id="rb"></td> <td>Preview with video call</td> </tr>
      * <tr> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code PREVIEW_VIDEO_STILL}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>Multi-purpose stream with JPEG or YUV still capture</td> </tr>
      * <tr> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>{@code JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>YUV and JPEG concurrent still image capture (for testing)</td> </tr>
-     * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, video record and JPEG or YUV video snapshot</td> </tr>
-     * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, in-application image processing, and JPEG or YUV still image capture</td> </tr>
+     * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td>{@code JPEG}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, video record and JPEG video snapshot</td> </tr>
+     * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, in-application image processing, and JPEG still image capture</td> </tr>
      * </table><br>
      * </p>
      *
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index ed1e9e5..2b8dc7c8 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -115,8 +115,14 @@
     @ChangeId
     @Overridable
     @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
-    @TestApi
-    public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L;
+    public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L;
+
+    /**
+     * Package-level opt in/out for the above.
+     * @hide
+     */
+    public static final String PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT =
+            "android.camera.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT";
 
     /**
      * System property for allowing the above
@@ -392,6 +398,23 @@
      * except that it uses {@link java.util.concurrent.Executor} as an argument
      * instead of {@link android.os.Handler}.</p>
      *
+     * <p>Note: If the order between some availability callbacks matters, the implementation of the
+     * executor should handle those callbacks in the same thread to maintain the callbacks' order.
+     * Some examples are:</p>
+     *
+     * <ul>
+     *
+     * <li>{@link AvailabilityCallback#onCameraAvailable} and
+     * {@link AvailabilityCallback#onCameraUnavailable} of the same camera ID.</li>
+     *
+     * <li>{@link AvailabilityCallback#onCameraAvailable} or
+     * {@link AvailabilityCallback#onCameraUnavailable} of a logical multi-camera, and {@link
+     * AvailabilityCallback#onPhysicalCameraUnavailable} or
+     * {@link AvailabilityCallback#onPhysicalCameraAvailable} of its physical
+     * cameras.</li>
+     *
+     * </ul>
+     *
      * @param executor The executor which will be used to invoke the callback.
      * @param callback the new callback to send camera availability notices to
      *
@@ -608,7 +631,7 @@
             try {
                 Size displaySize = getDisplaySize();
 
-                boolean overrideToPortrait = shouldOverrideToPortrait();
+                boolean overrideToPortrait = shouldOverrideToPortrait(mContext);
                 CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId,
                         mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait);
                 try {
@@ -728,7 +751,7 @@
                         "Camera service is currently unavailable");
                 }
 
-                boolean overrideToPortrait = shouldOverrideToPortrait();
+                boolean overrideToPortrait = shouldOverrideToPortrait(mContext);
                 cameraUser = cameraService.connectDevice(callbacks, cameraId,
                     mContext.getOpPackageName(), mContext.getAttributionTag(), uid,
                     oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion,
@@ -1160,9 +1183,26 @@
         return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId);
     }
 
-    private static boolean shouldOverrideToPortrait() {
-        return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT)
-                && CameraManagerGlobal.sLandscapeToPortrait;
+    /**
+     * @hide
+     */
+    public static boolean shouldOverrideToPortrait(@Nullable Context context) {
+        if (!CameraManagerGlobal.sLandscapeToPortrait) {
+            return false;
+        }
+
+        if (context != null) {
+            PackageManager packageManager = context.getPackageManager();
+
+            try {
+                return packageManager.getProperty(context.getOpPackageName(),
+                            PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT).getBoolean();
+            } catch (PackageManager.NameNotFoundException e) {
+                // No such property
+            }
+        }
+
+        return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT);
     }
 
     /**
@@ -2319,6 +2359,15 @@
                 final AvailabilityCallback callback = mCallbackMap.keyAt(i);
 
                 postSingleUpdate(callback, executor, id, null /*physicalId*/, status);
+
+                // Send the NOT_PRESENT state for unavailable physical cameras
+                if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) {
+                    ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id);
+                    for (String unavailableId : unavailableIds) {
+                        postSingleUpdate(callback, executor, id, unavailableId,
+                                ICameraServiceListener.STATUS_NOT_PRESENT);
+                    }
+                }
             }
         } // onStatusChangedLocked
 
@@ -2338,9 +2387,8 @@
             }
 
             //TODO: Do we need to treat this as error?
-            if (!mDeviceStatus.containsKey(id) || !isAvailable(mDeviceStatus.get(id))
-                    || !mUnavailablePhysicalDevices.containsKey(id)) {
-                Log.e(TAG, String.format("Camera %s is not available. Ignore physical camera "
+            if (!mDeviceStatus.containsKey(id) || !mUnavailablePhysicalDevices.containsKey(id)) {
+                Log.e(TAG, String.format("Camera %s is not present. Ignore physical camera "
                         + "status change", id));
                 return;
             }
@@ -2365,6 +2413,12 @@
                 return;
             }
 
+            if (!isAvailable(mDeviceStatus.get(id))) {
+                Log.i(TAG, String.format("Camera %s is not available. Ignore physical camera "
+                        + "status change callback(s)", id));
+                return;
+            }
+
             final int callbackCount = mCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
                 Executor executor = mCallbackMap.valueAt(i);
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index a6c79b3..0c2468e 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -87,6 +87,7 @@
 
     // TODO: guard every function with if (!mRemoteDevice) check (if it was closed)
     private ICameraDeviceUserWrapper mRemoteDevice;
+    private boolean mRemoteDeviceInit = false;
 
     // Lock to synchronize cross-thread access to device public interface
     final Object mInterfaceLock = new Object(); // access from this class and Session only!
@@ -338,6 +339,8 @@
 
             mDeviceExecutor.execute(mCallOnOpened);
             mDeviceExecutor.execute(mCallOnUnconfigured);
+
+            mRemoteDeviceInit = true;
         }
     }
 
@@ -1754,8 +1757,8 @@
         }
 
         synchronized(mInterfaceLock) {
-            if (mRemoteDevice == null) {
-                return; // Camera already closed
+            if (mRemoteDevice == null && mRemoteDeviceInit) {
+                return; // Camera already closed, user is not interested in errors anymore.
             }
 
             // Redirect device callback to the offline session in case we are in the middle
diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
index 956a474..d64009c 100644
--- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
+++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
@@ -1223,14 +1223,6 @@
         new StreamCombinationTemplate(new StreamTemplate [] {
                 new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
                         STREAM_USE_CASE_PREVIEW),
-                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.RECORD,
-                        STREAM_USE_CASE_RECORD),
-                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
-                        STREAM_USE_CASE_STILL_CAPTURE)},
-                "Preview, video record and YUV video snapshot"),
-        new StreamCombinationTemplate(new StreamTemplate [] {
-                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
-                        STREAM_USE_CASE_PREVIEW),
                 new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
                         STREAM_USE_CASE_RECORD),
                 new StreamTemplate(ImageFormat.JPEG, SizeThreshold.RECORD,
@@ -1239,27 +1231,11 @@
         new StreamCombinationTemplate(new StreamTemplate [] {
                 new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
                         STREAM_USE_CASE_PREVIEW),
-                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
-                        STREAM_USE_CASE_RECORD),
-                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
-                        STREAM_USE_CASE_STILL_CAPTURE)},
-                "Preview, in-application video processing and YUV video snapshot"),
-        new StreamCombinationTemplate(new StreamTemplate [] {
-                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
-                        STREAM_USE_CASE_PREVIEW),
                 new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
                         STREAM_USE_CASE_PREVIEW),
                 new StreamTemplate(ImageFormat.JPEG, SizeThreshold.MAXIMUM,
                         STREAM_USE_CASE_STILL_CAPTURE)},
                 "Preview, in-application image processing, and JPEG still image capture"),
-        new StreamCombinationTemplate(new StreamTemplate [] {
-                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
-                        STREAM_USE_CASE_PREVIEW),
-                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
-                        STREAM_USE_CASE_PREVIEW),
-                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.MAXIMUM,
-                        STREAM_USE_CASE_STILL_CAPTURE)},
-                "Preview, in-application image processing, and YUV still image capture"),
     };
 
     private static StreamCombinationTemplate sPreviewStabilizedStreamCombinations[] = {
diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java
index dba1a5e..6a667fe 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManager.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManager.java
@@ -251,6 +251,10 @@
         @Nullable
         private Boolean lastResult;
 
+        public FoldStateListener(Context context) {
+            this(context, folded -> {});
+        }
+
         public FoldStateListener(Context context, Consumer<Boolean> listener) {
             mFoldedDeviceStates = context.getResources().getIntArray(
                     com.android.internal.R.array.config_foldedDeviceStates);
@@ -266,5 +270,10 @@
                 mDelegate.accept(folded);
             }
         }
+
+        @Nullable
+        public Boolean getFolded() {
+            return lastResult;
+        }
     }
 }
diff --git a/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl b/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
index 9c2aa66..a36ccf6 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
@@ -39,5 +39,15 @@
      *        {@link android.view.Display#getDisplayId()}.
      */
     void onHbmDisabled(int displayId);
+
+    /**
+     * To avoid delay in switching refresh rate when activating LHBM, allow screens to request
+     * higher refersh rate if auth is possible on particular screen
+     *
+     * @param displayId The displayId for which the refresh rate should be unset. See
+     *        {@link android.view.Display#getDisplayId()}.
+     * @param isPossible If authentication is possible on particualr screen
+     */
+    void onAuthenticationPossible(int displayId, boolean isPossible);
 }
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index baf8836..b6cd06e 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -168,7 +168,7 @@
      * The <code>android:label</code> attribute specifies a human-readable descriptive
      * label to describe the keyboard layout in the user interface, such as "English (US)".
      * The <code>android:keyboardLayout</code> attribute refers to a
-     * <a href="http://source.android.com/tech/input/key-character-map-files.html">
+     * <a href="https://source.android.com/docs/core/interaction/input/key-character-map-files">
      * key character map</a> resource that defines the keyboard layout.
      * </p>
      */
diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
index 0954013..793a70e 100644
--- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -660,7 +660,7 @@
      * @param proto the ProtoOutputStream to write to
      */
     public void dumpDebug(ProtoOutputStream proto) {
-        getComponent().dumpDebug(proto, ApduServiceInfoProto.COMPONENT_NAME);
+        Utils.dumpDebugComponentName(getComponent(), proto, ApduServiceInfoProto.COMPONENT_NAME);
         proto.write(ApduServiceInfoProto.DESCRIPTION, getDescription());
         proto.write(ApduServiceInfoProto.ON_HOST, mOnHost);
         if (!mOnHost) {
diff --git a/core/java/android/nfc/cardemulation/NfcFServiceInfo.java b/core/java/android/nfc/cardemulation/NfcFServiceInfo.java
index f8f7dfe..7a36b26 100644
--- a/core/java/android/nfc/cardemulation/NfcFServiceInfo.java
+++ b/core/java/android/nfc/cardemulation/NfcFServiceInfo.java
@@ -340,7 +340,7 @@
      * @param proto the ProtoOutputStream to write to
      */
     public void dumpDebug(ProtoOutputStream proto) {
-        getComponent().dumpDebug(proto, NfcFServiceInfoProto.COMPONENT_NAME);
+        Utils.dumpDebugComponentName(getComponent(), proto, NfcFServiceInfoProto.COMPONENT_NAME);
         proto.write(NfcFServiceInfoProto.DESCRIPTION, getDescription());
         proto.write(NfcFServiceInfoProto.SYSTEM_CODE, getSystemCode());
         proto.write(NfcFServiceInfoProto.NFCID2, getNfcid2());
diff --git a/core/java/android/nfc/cardemulation/Utils.java b/core/java/android/nfc/cardemulation/Utils.java
new file mode 100644
index 0000000..202e1cf
--- /dev/null
+++ b/core/java/android/nfc/cardemulation/Utils.java
@@ -0,0 +1,37 @@
+/*
+ * 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 android.nfc.cardemulation;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.ComponentNameProto;
+import android.util.proto.ProtoOutputStream;
+
+/** @hide */
+public final class Utils {
+    private Utils() {
+    }
+
+    /** Copied from {@link ComponentName#dumpDebug(ProtoOutputStream, long)} */
+    public static void dumpDebugComponentName(
+            @NonNull ComponentName componentName, @NonNull ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+        proto.write(ComponentNameProto.PACKAGE_NAME, componentName.getPackageName());
+        proto.write(ComponentNameProto.CLASS_NAME, componentName.getClassName());
+        proto.end(token);
+    }
+}
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 3d5c34c..76475f2 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -649,8 +649,11 @@
             return Uid.PROCESS_STATE_NONEXISTENT;
         } else if (procState == ActivityManager.PROCESS_STATE_TOP) {
             return Uid.PROCESS_STATE_TOP;
-        } else if (ActivityManager.isForegroundService(procState)) {
-            // State when app has put itself in the foreground.
+        } else if (procState == ActivityManager.PROCESS_STATE_BOUND_TOP) {
+            return Uid.PROCESS_STATE_BACKGROUND;
+        } else if (procState == ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+            return Uid.PROCESS_STATE_FOREGROUND_SERVICE;
+        } else if (procState == ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
             return Uid.PROCESS_STATE_FOREGROUND_SERVICE;
         } else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
             // Persistent and other foreground states go here.
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index e1d15de..125bdaf 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -74,6 +74,7 @@
     String getUserAccount(int userId);
     void setUserAccount(int userId, String accountName);
     long getUserCreationTime(int userId);
+    int getUserSwitchability(int userId);
     boolean isUserSwitcherEnabled(int mUserId);
     boolean isRestricted(int userId);
     boolean canHaveRestrictedProfile(int userId);
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index 320b02c..bc65061 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -110,7 +110,8 @@
     public static final long TRACE_TAG_THERMAL = 1L << 27;
 
     private static final long TRACE_TAG_NOT_READY = 1L << 63;
-    private static final int MAX_SECTION_NAME_LEN = 127;
+    /** @hide **/
+    public static final int MAX_SECTION_NAME_LEN = 127;
 
     // Must be volatile to avoid word tearing.
     // This is only kept in case any apps get this by reflection but do not
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index 91d231e..787b609 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -51,8 +51,7 @@
     }
 
     /**
-     * The state of an application when it is either running a foreground (top) activity
-     * or a foreground service.
+     * The state of an application when it is either running a foreground (top) activity.
      */
     public static final int STATE_FOREGROUND = 0;
 
@@ -64,7 +63,8 @@
      * {@link android.app.ActivityManager#PROCESS_STATE_TRANSIENT_BACKGROUND},
      * {@link android.app.ActivityManager#PROCESS_STATE_BACKUP},
      * {@link android.app.ActivityManager#PROCESS_STATE_SERVICE},
-     * {@link android.app.ActivityManager#PROCESS_STATE_RECEIVER}.
+     * {@link android.app.ActivityManager#PROCESS_STATE_RECEIVER},
+     * {@link android.app.ActivityManager#PROCESS_STATE_FOREGROUND_SERVICE}.
      */
     public static final int STATE_BACKGROUND = 1;
 
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index e5b8472..df82a54 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -58,7 +58,6 @@
 import android.graphics.drawable.Drawable;
 import android.location.LocationManager;
 import android.provider.Settings;
-import android.telephony.TelephonyManager;
 import android.util.AndroidException;
 import android.util.ArraySet;
 import android.util.Log;
@@ -595,8 +594,11 @@
     /**
      * Specifies if a user is disallowed from transferring files over USB.
      *
-     * <p>This restriction can only be set by a device owner, a profile owner on the primary
-     * user or a profile owner of an organization-owned managed profile on the parent profile.
+     * <p>This restriction can only be set by a <a href="https://developers.google.com/android/work/terminology#device_owner_do">
+     * device owner</a> or a <a href="https://developers.google.com/android/work/terminology#profile_owner_po">
+     * profile owner</a> on the primary user's profile or a profile owner of an organization-owned
+     * <a href="https://developers.google.com/android/work/terminology#managed_profile">
+     * managed profile</a> on the parent profile.
      * When it is set by a device owner, it applies globally. When it is set by a profile owner
      * on the primary user or by a profile owner of an organization-owned managed profile on
      * the parent profile, it disables the primary user from transferring files over USB. No other
@@ -1687,7 +1689,7 @@
     public static final int SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED = 1 << 2;
 
     /**
-     * Result returned in {@link #getUserSwitchability()} indicating user swichability.
+     * Result returned in {@link #getUserSwitchability()} indicating user switchability.
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
@@ -2054,25 +2056,16 @@
      * @hide
      */
     @Deprecated
-    @RequiresPermission(allOf = {
-            Manifest.permission.READ_PHONE_STATE,
-            Manifest.permission.MANAGE_USERS}, // Can be INTERACT_ACROSS_USERS instead.
-            conditional = true)
+    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS})
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     @UserHandleAware
     public boolean canSwitchUsers() {
-        boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
-                mContext.getContentResolver(),
-                Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
-        boolean isSystemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
-        boolean inCall = false;
-        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
-        if (telephonyManager != null) {
-            inCall = telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
+        try {
+            return mService.getUserSwitchability(mUserId) == SWITCHABILITY_STATUS_OK;
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
         }
-        boolean isUserSwitchDisallowed = hasUserRestrictionForUser(DISALLOW_USER_SWITCH, mUserId);
-        return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall
-                && !isUserSwitchDisallowed;
     }
 
     /**
@@ -2106,34 +2099,14 @@
      * @return A {@link UserSwitchabilityResult} flag indicating if the user is switchable.
      * @hide
      */
-    @RequiresPermission(allOf = {Manifest.permission.READ_PHONE_STATE,
-            android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS})
     public @UserSwitchabilityResult int getUserSwitchability(UserHandle userHandle) {
-        final TelephonyManager tm =
-                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-
-        int flags = SWITCHABILITY_STATUS_OK;
-        if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
-            flags |= SWITCHABILITY_STATUS_USER_IN_CALL;
+        try {
+            return mService.getUserSwitchability(userHandle.getIdentifier());
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
         }
-        if (hasUserRestrictionForUser(DISALLOW_USER_SWITCH, userHandle)) {
-            flags |= SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
-        }
-
-        // System User is always unlocked in Headless System User Mode, so ignore this flag
-        if (!isHeadlessSystemUserMode()) {
-            final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
-                    mContext.getContentResolver(),
-                    Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
-            final boolean systemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
-
-            if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
-                flags |= SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
-            }
-        }
-
-        return flags;
     }
 
     /**
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 08de87e..0d9f500c 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -2552,6 +2552,8 @@
      * This API doesn't require any special permissions, though typical implementations
      * will require being called from an SELinux domain that allows setting file attributes
      * related to quota (eg the GID or project ID).
+     * If the calling user has MANAGE_EXTERNAL_STORAGE permissions, quota for shared profile's
+     * volumes is also updated.
      *
      * The default platform user of this API is the MediaProvider process, which is
      * responsible for managing all of external storage.
@@ -2572,7 +2574,14 @@
             @QuotaType int quotaType) throws IOException {
         long projectId;
         final String filePath = path.getCanonicalPath();
-        final StorageVolume volume = getStorageVolume(path);
+        int volFlags = FLAG_REAL_STATE | FLAG_INCLUDE_INVISIBLE;
+        // If caller has MANAGE_EXTERNAL_STORAGE permission, results from User Profile(s) are also
+        // returned by enabling FLAG_INCLUDE_SHARED_PROFILE.
+        if (mContext.checkSelfPermission(MANAGE_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
+            volFlags |= FLAG_INCLUDE_SHARED_PROFILE;
+        }
+        final StorageVolume[] availableVolumes = getVolumeList(mContext.getUserId(), volFlags);
+        final StorageVolume volume = getStorageVolume(availableVolumes, path);
         if (volume == null) {
             Log.w(TAG, "Failed to update quota type for " + filePath);
             return;
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 3bf9ca0..b117a9a 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -16,7 +16,9 @@
 
 package android.preference;
 
+import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.app.NotificationManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
@@ -35,6 +37,7 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.preference.VolumePreference.VolumeStore;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.provider.Settings.System;
@@ -44,6 +47,7 @@
 import android.widget.SeekBar.OnSeekBarChangeListener;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.os.SomeArgs;
 
 import java.util.concurrent.TimeUnit;
@@ -115,7 +119,6 @@
     private final int mMaxStreamVolume;
     private boolean mAffectedByRingerMode;
     private boolean mNotificationOrRing;
-    private final boolean mNotifAliasRing;
     private final Receiver mReceiver = new Receiver();
 
     private Handler mHandler;
@@ -158,6 +161,7 @@
         this(context, streamType, defaultUri, callback, true /* playSample */);
     }
 
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public SeekBarVolumizer(
             Context context,
             int streamType,
@@ -180,8 +184,6 @@
         if (mNotificationOrRing) {
             mRingerMode = mAudioManager.getRingerModeInternal();
         }
-        mNotifAliasRing = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_alias_ring_notif_stream_types);
         mZenMode = mNotificationManager.getZenMode();
 
         if (hasAudioProductStrategies()) {
@@ -288,7 +290,9 @@
              * so that when user attempts to slide the notification seekbar out of vibrate the
              * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
              */
-            if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING
+            if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
+                    || mStreamType == AudioManager.STREAM_RING
                     || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
                 mSeekBar.setProgress(0, true);
             }
@@ -365,7 +369,9 @@
         // set the time of stop volume
         if ((mStreamType == AudioManager.STREAM_VOICE_CALL
                 || mStreamType == AudioManager.STREAM_RING
-                || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION)
+                || (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
+                && mStreamType == AudioManager.STREAM_NOTIFICATION)
                 || mStreamType == AudioManager.STREAM_ALARM)) {
             sStopVolumeTime = java.lang.System.currentTimeMillis();
         }
@@ -644,8 +650,10 @@
         }
 
         private void updateVolumeSlider(int streamType, int streamValue) {
-            final boolean streamMatch = mNotifAliasRing && mNotificationOrRing
-                    ? isNotificationOrRing(streamType) : streamType == mStreamType;
+            final boolean streamMatch =  !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
+                    && mNotificationOrRing ? isNotificationOrRing(streamType) :
+                    streamType == mStreamType;
             if (mSeekBar != null && streamMatch && streamValue != -1) {
                 final boolean muted = mAudioManager.isStreamMute(mStreamType)
                         || streamValue == 0;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 85e3aee..43fccbe 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9233,6 +9233,14 @@
         public static final int DOCK_SETUP_PROMPTED = 3;
 
         /**
+         * Indicates that the user has started dock setup but never finished it.
+         * One of the possible states for {@link #DOCK_SETUP_STATE}.
+         *
+         * @hide
+         */
+        public static final int DOCK_SETUP_INCOMPLETE = 4;
+
+        /**
          * Indicates that the user has completed dock setup.
          * One of the possible states for {@link #DOCK_SETUP_STATE}.
          *
@@ -9240,6 +9248,14 @@
          */
         public static final int DOCK_SETUP_COMPLETED = 10;
 
+        /**
+         * Indicates that dock setup timed out before the user could complete it.
+         * One of the possible states for {@link #DOCK_SETUP_STATE}.
+         *
+         * @hide
+         */
+        public static final int DOCK_SETUP_TIMED_OUT = 11;
+
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
         @IntDef({
@@ -9247,7 +9263,9 @@
                 DOCK_SETUP_STARTED,
                 DOCK_SETUP_PAUSED,
                 DOCK_SETUP_PROMPTED,
-                DOCK_SETUP_COMPLETED
+                DOCK_SETUP_INCOMPLETE,
+                DOCK_SETUP_COMPLETED,
+                DOCK_SETUP_TIMED_OUT
         })
         public @interface DockSetupState {
         }
@@ -9516,7 +9534,7 @@
         /**
          * Indicates whether "seen" notifications should be suppressed from the lockscreen.
          * <p>
-         * Type: int (0 for false, 1 for true)
+         * Type: int (0 for unset, 1 for true, 2 for false)
          *
          * @hide
          */
@@ -9819,11 +9837,12 @@
                 "fingerprint_side_fps_auth_downtime";
 
         /**
-         * Whether or not a SFPS device is required to be interactive for auth to unlock the device.
+         * Whether or not a SFPS device is enabling the performant auth setting.
+         * The "_V2" suffix was added to re-introduce the default behavior for
+         * users. See b/265264294 fore more details.
          * @hide
          */
-        public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED =
-                "sfps_require_screen_on_to_auth_enabled";
+        public static final String SFPS_PERFORMANT_AUTH_ENABLED = "sfps_performant_auth_enabled_v2";
 
         /**
          * Whether or not debugging is enabled.
@@ -9902,6 +9921,28 @@
                 "active_unlock_on_unlock_intent_when_biometric_enrolled";
 
         /**
+         * If active unlock triggers on unlock intents, then also request active unlock on
+         * these wake-up reasons. See {@link PowerManager.WakeReason} for value mappings.
+         * WakeReasons should be separated by a pipe. For example: "0|3" or "0". If this
+         * setting should be disabled, then this should be set to an empty string. A null value
+         * will use the system default value (WAKE_REASON_UNFOLD_DEVICE).
+         * @hide
+         */
+        public static final String ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS =
+                "active_unlock_wakeups_considered_unlock_intents";
+
+        /**
+         * If active unlock triggers and succeeds on these wakeups, force dismiss keyguard on
+         * these wake reasons. See {@link PowerManager#WakeReason} for value mappings.
+         * WakeReasons should be separated by a pipe. For example: "0|3" or "0". If this
+         * setting should be disabled, then this should be set to an empty string. A null value
+         * will use the system default value (WAKE_REASON_UNFOLD_DEVICE).
+         * @hide
+         */
+        public static final String ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD =
+                "active_unlock_wakeups_to_force_dismiss_keyguard";
+
+        /**
          * Whether the assist gesture should be enabled.
          *
          * @hide
@@ -11017,6 +11058,13 @@
                 "extra_automatic_power_save_mode";
 
         /**
+         * Whether lockscreen weather is enabled.
+         *
+         * @hide
+         */
+        public static final String LOCK_SCREEN_WEATHER_ENABLED = "lockscreen_weather_enabled";
+
+        /**
          * These entries are considered common between the personal and the managed profile,
          * since the managed profile doesn't get to change them.
          */
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index 001a25d..3dc805e 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -53,6 +53,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -4353,6 +4354,12 @@
          */
         public static final String COLUMN_NAME_SOURCE = "name_source";
 
+        /**
+         * The name source is unknown.
+         * @hide
+         */
+        public static final int NAME_SOURCE_UNKNOWN = -1;
+
         /** The name_source is from the carrier id. {@hide} */
         public static final int NAME_SOURCE_CARRIER_ID = 0;
 
@@ -4834,5 +4841,86 @@
          * @hide
          */
         public static final String COLUMN_USER_HANDLE = "user_handle";
+
+        /** All columns in {@link SimInfo} table. */
+        private static final List<String> ALL_COLUMNS = List.of(
+                COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID,
+                COLUMN_ICC_ID,
+                COLUMN_SIM_SLOT_INDEX,
+                COLUMN_DISPLAY_NAME,
+                COLUMN_CARRIER_NAME,
+                COLUMN_NAME_SOURCE,
+                COLUMN_COLOR,
+                COLUMN_NUMBER,
+                COLUMN_DISPLAY_NUMBER_FORMAT,
+                COLUMN_DATA_ROAMING,
+                COLUMN_MCC,
+                COLUMN_MNC,
+                COLUMN_MCC_STRING,
+                COLUMN_MNC_STRING,
+                COLUMN_EHPLMNS,
+                COLUMN_HPLMNS,
+                COLUMN_SIM_PROVISIONING_STATUS,
+                COLUMN_IS_EMBEDDED,
+                COLUMN_CARD_ID,
+                COLUMN_ACCESS_RULES,
+                COLUMN_ACCESS_RULES_FROM_CARRIER_CONFIGS,
+                COLUMN_IS_REMOVABLE,
+                COLUMN_CB_EXTREME_THREAT_ALERT,
+                COLUMN_CB_SEVERE_THREAT_ALERT,
+                COLUMN_CB_AMBER_ALERT,
+                COLUMN_CB_EMERGENCY_ALERT,
+                COLUMN_CB_ALERT_SOUND_DURATION,
+                COLUMN_CB_ALERT_REMINDER_INTERVAL,
+                COLUMN_CB_ALERT_VIBRATE,
+                COLUMN_CB_ALERT_SPEECH,
+                COLUMN_CB_ETWS_TEST_ALERT,
+                COLUMN_CB_CHANNEL_50_ALERT,
+                COLUMN_CB_CMAS_TEST_ALERT,
+                COLUMN_CB_OPT_OUT_DIALOG,
+                COLUMN_ENHANCED_4G_MODE_ENABLED,
+                COLUMN_VT_IMS_ENABLED,
+                COLUMN_WFC_IMS_ENABLED,
+                COLUMN_WFC_IMS_MODE,
+                COLUMN_WFC_IMS_ROAMING_MODE,
+                COLUMN_WFC_IMS_ROAMING_ENABLED,
+                COLUMN_IS_OPPORTUNISTIC,
+                COLUMN_GROUP_UUID,
+                COLUMN_IS_METERED,
+                COLUMN_ISO_COUNTRY_CODE,
+                COLUMN_CARRIER_ID,
+                COLUMN_PROFILE_CLASS,
+                COLUMN_SUBSCRIPTION_TYPE,
+                COLUMN_GROUP_OWNER,
+                COLUMN_DATA_ENABLED_OVERRIDE_RULES,
+                COLUMN_ENABLED_MOBILE_DATA_POLICIES,
+                COLUMN_IMSI,
+                COLUMN_UICC_APPLICATIONS_ENABLED,
+                COLUMN_ALLOWED_NETWORK_TYPES,
+                COLUMN_IMS_RCS_UCE_ENABLED,
+                COLUMN_CROSS_SIM_CALLING_ENABLED,
+                COLUMN_RCS_CONFIG,
+                COLUMN_ALLOWED_NETWORK_TYPES_FOR_REASONS,
+                COLUMN_D2D_STATUS_SHARING,
+                COLUMN_VOIMS_OPT_IN_STATUS,
+                COLUMN_D2D_STATUS_SHARING_SELECTED_CONTACTS,
+                COLUMN_NR_ADVANCED_CALLING_ENABLED,
+                COLUMN_PHONE_NUMBER_SOURCE_CARRIER,
+                COLUMN_PHONE_NUMBER_SOURCE_IMS,
+                COLUMN_PORT_INDEX,
+                COLUMN_USAGE_SETTING,
+                COLUMN_TP_MESSAGE_REF,
+                COLUMN_USER_HANDLE
+        );
+
+        /**
+         * @return All columns in {@link SimInfo} table.
+         *
+         * @hide
+         */
+        @NonNull
+        public static List<String> getAllColumns() {
+            return ALL_COLUMNS;
+        }
     }
 }
diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java
index 4f37cd9..a2ffa5d 100644
--- a/core/java/android/service/appprediction/AppPredictionService.java
+++ b/core/java/android/service/appprediction/AppPredictionService.java
@@ -328,7 +328,7 @@
                 Slog.e(TAG, "Callback is null, likely the binder has died.");
                 return false;
             }
-            return mCallback.equals(callback);
+            return mCallback.asBinder().equals(callback.asBinder());
         }
 
         public void destroy() {
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index 0fb9f57..b0e847c 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -166,7 +166,7 @@
     }
 
     /**
-     * Description of an event that occured after the latest call to
+     * Description of an event that occurred after the latest call to
      * {@link FillCallback#onSuccess(FillResponse)}.
      */
     public static final class Event {
diff --git a/core/java/android/service/chooser/ChooserAction.java b/core/java/android/service/chooser/ChooserAction.java
index 3010049..a61b781 100644
--- a/core/java/android/service/chooser/ChooserAction.java
+++ b/core/java/android/service/chooser/ChooserAction.java
@@ -27,10 +27,9 @@
 
 /**
  * A ChooserAction is an app-defined action that can be provided to the Android Sharesheet to
- * be shown to the user when {@link android.content.Intent.ACTION_CHOOSER} is invoked.
+ * be shown to the user when {@link android.content.Intent#ACTION_CHOOSER} is invoked.
  *
- * @see android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
- * @see android.content.Intent.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION
+ * @see android.content.Intent#EXTRA_CHOOSER_CUSTOM_ACTIONS
  * @hide
  */
 public final class ChooserAction implements Parcelable {
@@ -88,6 +87,7 @@
         return "ChooserAction {" + "label=" + mLabel + ", intent=" + mAction + "}";
     }
 
+    @NonNull
     public static final Parcelable.Creator<ChooserAction> CREATOR =
             new Creator<ChooserAction>() {
                 @Override
@@ -137,6 +137,7 @@
          * object.
          * @return the built action
          */
+        @NonNull
         public ChooserAction build() {
             return new ChooserAction(mIcon, mLabel, mAction);
         }
diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java
index a2fa139..a389223 100644
--- a/core/java/android/service/dreams/DreamActivity.java
+++ b/core/java/android/service/dreams/DreamActivity.java
@@ -58,11 +58,13 @@
             setTitle(title);
         }
 
-        final Bundle extras = getIntent().getExtras();
-        mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK);
-
-        if (mCallback != null) {
+        final Object callback = getIntent().getExtras().getBinder(EXTRA_CALLBACK);
+        if (callback instanceof DreamService.DreamActivityCallbacks) {
+            mCallback = (DreamService.DreamActivityCallbacks) callback;
             mCallback.onActivityCreated(this);
+        } else {
+            mCallback = null;
+            finishAndRemoveTask();
         }
     }
 
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 6e8198b..6e4535b 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -36,33 +36,100 @@
 public abstract class DreamOverlayService extends Service {
     private static final String TAG = "DreamOverlayService";
     private static final boolean DEBUG = false;
-    private boolean mShowComplications;
-    private ComponentName mDreamComponent;
 
-    private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
+    // The last client that started dreaming and hasn't ended
+    private OverlayClient mCurrentClient;
+
+    // An {@link IDreamOverlayClient} implementation that identifies itself when forwarding
+    // requests to the {@link DreamOverlayService}
+    private static class OverlayClient extends IDreamOverlayClient.Stub {
+        private final DreamOverlayService mService;
+        private boolean mShowComplications;
+        private ComponentName mDreamComponent;
+        IDreamOverlayCallback mDreamOverlayCallback;
+
+        OverlayClient(DreamOverlayService service) {
+            mService = service;
+        }
+
         @Override
-        public void startDream(WindowManager.LayoutParams layoutParams,
-                IDreamOverlayCallback callback, String dreamComponent,
-                boolean shouldShowComplications) {
-            mDreamOverlayCallback = callback;
+        public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback,
+                String dreamComponent, boolean shouldShowComplications) throws RemoteException {
             mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
             mShowComplications = shouldShowComplications;
-            onStartDream(layoutParams);
+            mDreamOverlayCallback = callback;
+            mService.startDream(this, params);
         }
 
+
+
         @Override
         public void wakeUp() {
-            onWakeUp(() -> {
+            mService.wakeUp(this, () -> {
                 try {
                     mDreamOverlayCallback.onWakeUpComplete();
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Could not notify dream of wakeUp:" + e);
+                    Log.e(TAG, "Could not notify dream of wakeUp", e);
                 }
             });
         }
-    };
 
-    IDreamOverlayCallback mDreamOverlayCallback;
+        @Override
+        public void endDream() {
+            mService.endDream(this);
+        }
+
+        private void onExitRequested() {
+            try {
+                mDreamOverlayCallback.onExitRequested();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Could not request exit:" + e);
+            }
+        }
+
+        private boolean shouldShowComplications() {
+            return mShowComplications;
+        }
+
+        private ComponentName getComponent() {
+            return mDreamComponent;
+        }
+    }
+
+    private void startDream(OverlayClient client, WindowManager.LayoutParams params) {
+        endDream(mCurrentClient);
+        mCurrentClient = client;
+        onStartDream(params);
+    }
+
+    private void endDream(OverlayClient client) {
+        if (client == null || client != mCurrentClient) {
+            return;
+        }
+
+        onEndDream();
+        mCurrentClient = null;
+    }
+
+    private void wakeUp(OverlayClient client, Runnable callback) {
+        if (mCurrentClient != client) {
+            return;
+        }
+
+        onWakeUp(callback);
+    }
+
+    private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
+        @Override
+        public void getClient(IDreamOverlayClientCallback callback) {
+            try {
+                callback.onDreamOverlayClient(
+                        new OverlayClient(DreamOverlayService.this));
+            } catch (RemoteException e) {
+                Log.e(TAG, "could not send client to callback", e);
+            }
+        }
+    };
 
     public DreamOverlayService() {
     }
@@ -83,31 +150,45 @@
 
     /**
      * This method is overridden by implementations to handle when the dream has been requested
-     * to wakeup. This allows any overlay animations to run.
+     * to wakeup. This allows any overlay animations to run. By default, the method will invoke
+     * the callback immediately.
      *
      * @param onCompleteCallback The callback to trigger to notify the dream service that the
      *                           overlay has completed waking up.
      * @hide
      */
     public void onWakeUp(@NonNull Runnable onCompleteCallback) {
+        onCompleteCallback.run();
+    }
+
+    /**
+     * This method is overridden by implementations to handle when the dream has ended. There may
+     * be earlier signals leading up to this step, such as @{@link #onWakeUp(Runnable)}.
+     */
+    public void onEndDream() {
     }
 
     /**
      * This method is invoked to request the dream exit.
      */
     public final void requestExit() {
-        try {
-            mDreamOverlayCallback.onExitRequested();
-        } catch (RemoteException e) {
-            Log.e(TAG, "Could not request exit:" + e);
+        if (mCurrentClient == null) {
+            throw new IllegalStateException("requested exit with no dream present");
         }
+
+        mCurrentClient.onExitRequested();
     }
 
     /**
      * Returns whether to show complications on the dream overlay.
      */
     public final boolean shouldShowComplications() {
-        return mShowComplications;
+        if (mCurrentClient == null) {
+            throw new IllegalStateException(
+                    "requested if should show complication when no dream active");
+        }
+
+        return mCurrentClient.shouldShowComplications();
     }
 
     /**
@@ -115,6 +196,10 @@
      * @hide
      */
     public final ComponentName getDreamComponent() {
-        return mDreamComponent;
+        if (mCurrentClient == null) {
+            throw new IllegalStateException("requested dream component when no dream active");
+        }
+
+        return mCurrentClient.getComponent();
     }
 }
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 8b9852a..d79ea89 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -234,6 +234,7 @@
     private boolean mCanDoze;
     private boolean mDozing;
     private boolean mWindowless;
+    private boolean mOverlayFinishing;
     private int mDozeScreenState = Display.STATE_UNKNOWN;
     private int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
 
@@ -248,25 +249,39 @@
     private OverlayConnection mOverlayConnection;
 
     private static class OverlayConnection extends PersistentServiceConnection<IDreamOverlay> {
-        // Overlay set during onBind.
-        private IDreamOverlay mOverlay;
+        // Retrieved Client
+        private IDreamOverlayClient mClient;
+
         // A list of pending requests to execute on the overlay.
-        private final ArrayList<Consumer<IDreamOverlay>> mConsumers = new ArrayList<>();
+        private final ArrayList<Consumer<IDreamOverlayClient>> mConsumers = new ArrayList<>();
+
+        private final IDreamOverlayClientCallback mClientCallback =
+                new IDreamOverlayClientCallback.Stub() {
+            @Override
+            public void onDreamOverlayClient(IDreamOverlayClient client) {
+                mClient = client;
+
+                for (Consumer<IDreamOverlayClient> consumer : mConsumers) {
+                    consumer.accept(mClient);
+                }
+            }
+        };
 
         private final Callback<IDreamOverlay> mCallback = new Callback<IDreamOverlay>() {
             @Override
             public void onConnected(ObservableServiceConnection<IDreamOverlay> connection,
                     IDreamOverlay service) {
-                mOverlay = service;
-                for (Consumer<IDreamOverlay> consumer : mConsumers) {
-                    consumer.accept(mOverlay);
+                try {
+                    service.getClient(mClientCallback);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "could not get DreamOverlayClient", e);
                 }
             }
 
             @Override
             public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection,
                     int reason) {
-                mOverlay = null;
+                mClient = null;
             }
         };
 
@@ -296,15 +311,21 @@
             super.unbind();
         }
 
-        public void addConsumer(Consumer<IDreamOverlay> consumer) {
-            mConsumers.add(consumer);
-            if (mOverlay != null) {
-                consumer.accept(mOverlay);
-            }
+        public void addConsumer(Consumer<IDreamOverlayClient> consumer) {
+            execute(() -> {
+                mConsumers.add(consumer);
+                if (mClient != null) {
+                    consumer.accept(mClient);
+                }
+            });
         }
 
-        public void removeConsumer(Consumer<IDreamOverlay> consumer) {
-            mConsumers.remove(consumer);
+        public void removeConsumer(Consumer<IDreamOverlayClient> consumer) {
+            execute(() -> mConsumers.remove(consumer));
+        }
+
+        public void clearConsumers() {
+            execute(() -> mConsumers.clear());
         }
     }
 
@@ -1031,6 +1052,7 @@
         // We must unbind from any overlay connection if we are unbound before finishing.
         if (mOverlayConnection != null) {
             mOverlayConnection.unbind();
+            mOverlayConnection = null;
         }
 
         return super.onUnbind(intent);
@@ -1044,6 +1066,26 @@
      * </p>
      */
     public final void finish() {
+        // If there is an active overlay connection, signal that the dream is ending before
+        // continuing. Note that the overlay cannot rely on the unbound state, since another dream
+        // might have bound to it in the meantime.
+        if (mOverlayConnection != null && !mOverlayFinishing) {
+            // Set mOverlayFinish to true to only allow this consumer to be added once.
+            mOverlayFinishing = true;
+            mOverlayConnection.addConsumer(overlay -> {
+                try {
+                    overlay.endDream();
+                    mOverlayConnection.unbind();
+                    mOverlayConnection = null;
+                    finish();
+                } catch (RemoteException e) {
+                    Log.e(mTag, "could not inform overlay of dream end:" + e);
+                }
+            });
+            mOverlayConnection.clearConsumers();
+            return;
+        }
+
         if (mDebug) Slog.v(mTag, "finish(): mFinished=" + mFinished);
 
         Activity activity = mActivity;
@@ -1060,10 +1102,6 @@
         }
         mFinished = true;
 
-        if (mOverlayConnection != null) {
-            mOverlayConnection.unbind();
-        }
-
         if (mDreamToken == null) {
             if (mDebug) Slog.v(mTag, "finish() called when not attached.");
             stopSelf();
@@ -1266,9 +1304,10 @@
      * Must run on mHandler.
      *
      * @param dreamToken Token for this dream service.
-     * @param started A callback that will be invoked once onDreamingStarted has completed.
+     * @param started    A callback that will be invoked once onDreamingStarted has completed.
      */
-    private void attach(IBinder dreamToken, boolean canDoze, IRemoteCallback started) {
+    private void attach(IBinder dreamToken, boolean canDoze, boolean isPreviewMode,
+            IRemoteCallback started) {
         if (mDreamToken != null) {
             Slog.e(mTag, "attach() called when dream with token=" + mDreamToken
                     + " already attached");
@@ -1316,7 +1355,8 @@
             i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken));
             final ServiceInfo serviceInfo = fetchServiceInfo(this,
                     new ComponentName(this, getClass()));
-            i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo));
+            i.putExtra(DreamActivity.EXTRA_DREAM_TITLE,
+                    fetchDreamLabel(this, serviceInfo, isPreviewMode));
 
             try {
                 if (!ActivityTaskManager.getService().startDreamActivity(i)) {
@@ -1359,7 +1399,7 @@
 
         mWindow.getDecorView().addOnAttachStateChangeListener(
                 new View.OnAttachStateChangeListener() {
-                    private Consumer<IDreamOverlay> mDreamStartOverlayConsumer;
+                    private Consumer<IDreamOverlayClient> mDreamStartOverlayConsumer;
 
                     @Override
                     public void onViewAttachedToWindow(View v) {
@@ -1391,6 +1431,7 @@
                             mActivity = null;
                             finish();
                         }
+
                         if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) {
                             mOverlayConnection.removeConsumer(mDreamStartOverlayConsumer);
                         }
@@ -1431,10 +1472,18 @@
 
     @Nullable
     private static CharSequence fetchDreamLabel(Context context,
-            @Nullable ServiceInfo serviceInfo) {
-        if (serviceInfo == null) return null;
+            @Nullable ServiceInfo serviceInfo,
+            boolean isPreviewMode) {
+        if (serviceInfo == null) {
+            return null;
+        }
         final PackageManager pm = context.getPackageManager();
-        return serviceInfo.loadLabel(pm);
+        final CharSequence dreamLabel = serviceInfo.loadLabel(pm);
+        if (!isPreviewMode || dreamLabel == null) {
+            return dreamLabel;
+        }
+        // When in preview mode, return a special label indicating the dream is in preview.
+        return context.getResources().getString(R.string.dream_preview_title, dreamLabel);
     }
 
     @Nullable
@@ -1490,8 +1539,9 @@
     final class DreamServiceWrapper extends IDreamService.Stub {
         @Override
         public void attach(final IBinder dreamToken, final boolean canDoze,
-                IRemoteCallback started) {
-            mHandler.post(() -> DreamService.this.attach(dreamToken, canDoze, started));
+                final boolean isPreviewMode, IRemoteCallback started) {
+            mHandler.post(
+                    () -> DreamService.this.attach(dreamToken, canDoze, isPreviewMode, started));
         }
 
         @Override
diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl
index 7aeceb2c..7ec75a5 100644
--- a/core/java/android/service/dreams/IDreamOverlay.aidl
+++ b/core/java/android/service/dreams/IDreamOverlay.aidl
@@ -16,8 +16,7 @@
 
 package android.service.dreams;
 
-import android.service.dreams.IDreamOverlayCallback;
-import android.view.WindowManager.LayoutParams;
+import android.service.dreams.IDreamOverlayClientCallback;
 
 /**
 * {@link IDreamOverlay} provides a way for a component to annotate a dream with additional view
@@ -28,17 +27,7 @@
 */
 interface IDreamOverlay {
     /**
-    * @param params The {@link LayoutParams} for the associated DreamWindow, including the window
-                    token of the Dream Activity.
-    * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
-    *                dream.
-    * @param dreamComponent The component name of the dream service requesting overlay.
-    * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
-    *                and weather.
+    * Retrieves a client the caller can use to interact with the dream overlay.
     */
-    void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
-        in String dreamComponent, in boolean shouldShowComplications);
-
-    /** Called when the dream is waking, to do any exit animations */
-    void wakeUp();
+    void getClient(in IDreamOverlayClientCallback callback);
 }
diff --git a/core/java/android/service/dreams/IDreamOverlayClient.aidl b/core/java/android/service/dreams/IDreamOverlayClient.aidl
new file mode 100644
index 0000000..78b7280
--- /dev/null
+++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl
@@ -0,0 +1,45 @@
+/**
+ * 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 android.service.dreams;
+
+import android.service.dreams.IDreamOverlayCallback;
+import android.view.WindowManager.LayoutParams;
+
+/**
+* {@link IDreamOverlayClient} allows {@link DreamService} instances to act upon the dream overlay.
+*
+* @hide
+*/
+interface IDreamOverlayClient {
+    /**
+    * @param params The {@link LayoutParams} for the associated DreamWindow, including the window
+                    token of the Dream Activity.
+    * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
+    *                dream.
+    * @param dreamComponent The component name of the dream service requesting overlay.
+    * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
+    *                and weather.
+    */
+    void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
+        in String dreamComponent, in boolean shouldShowComplications);
+
+    /** Called when the dream is waking, to do any exit animations */
+    void wakeUp();
+
+    /** Called when the dream has ended. */
+    void endDream();
+}
diff --git a/core/java/android/service/dreams/IDreamOverlayClientCallback.aidl b/core/java/android/service/dreams/IDreamOverlayClientCallback.aidl
new file mode 100644
index 0000000..244d999
--- /dev/null
+++ b/core/java/android/service/dreams/IDreamOverlayClientCallback.aidl
@@ -0,0 +1,30 @@
+/**
+ * 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 android.service.dreams;
+
+import android.service.dreams.IDreamOverlayClient;
+
+/**
+* {@link IDreamOverlayClientCallback} allows receiving a requested {@link IDreamOverlayClient}.
+* @hide
+*/
+interface IDreamOverlayClientCallback {
+    /**
+    * Called with a unique {@link IDreamOverlayClient}.
+    */
+    void onDreamOverlayClient(in IDreamOverlayClient client);
+}
diff --git a/core/java/android/service/dreams/IDreamService.aidl b/core/java/android/service/dreams/IDreamService.aidl
index ce04354..8b5d875 100644
--- a/core/java/android/service/dreams/IDreamService.aidl
+++ b/core/java/android/service/dreams/IDreamService.aidl
@@ -22,7 +22,7 @@
  * @hide
  */
 oneway interface IDreamService {
-    void attach(IBinder windowToken, boolean canDoze, IRemoteCallback started);
+    void attach(IBinder windowToken, boolean canDoze, boolean isPreviewMode, IRemoteCallback started);
     void detach();
     void wakeUp();
 }
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index 4b25c88..182a497 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -52,7 +52,7 @@
     /** @hide */
     @StringDef (prefix = { "KEY_" }, value = {
             KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA,
-            KEY_TEXT_REPLIES, KEY_USER_SENTIMENT
+            KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Keys {}
@@ -122,6 +122,19 @@
     public static final String KEY_IMPORTANCE = "key_importance";
 
     /**
+     * Weaker than {@link #KEY_IMPORTANCE}, this adjustment suggests an importance rather than
+     * mandates an importance change.
+     *
+     * A notification listener can interpet this suggestion to show the user a prompt to change
+     * notification importance for the notification (or type, or app) moving forward.
+     *
+     * Data type: int, one of importance values e.g.
+     * {@link android.app.NotificationManager#IMPORTANCE_MIN}.
+     * @hide
+     */
+    public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal";
+
+    /**
      * Data type: float, a ranking score from 0 (lowest) to 1 (highest).
      * Used to rank notifications inside that fall under the same classification (i.e. alerting,
      * silenced).
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index ad2e9d5..dc4cb9f 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -1711,6 +1711,8 @@
         private ShortcutInfo mShortcutInfo;
         private @RankingAdjustment int mRankingAdjustment;
         private boolean mIsBubble;
+        // Notification assistant importance suggestion
+        private int mProposedImportance;
 
         private static final int PARCEL_VERSION = 2;
 
@@ -1748,6 +1750,7 @@
             out.writeParcelable(mShortcutInfo, flags);
             out.writeInt(mRankingAdjustment);
             out.writeBoolean(mIsBubble);
+            out.writeInt(mProposedImportance);
         }
 
         /** @hide */
@@ -1786,6 +1789,7 @@
             mShortcutInfo = in.readParcelable(cl, android.content.pm.ShortcutInfo.class);
             mRankingAdjustment = in.readInt();
             mIsBubble = in.readBoolean();
+            mProposedImportance = in.readInt();
         }
 
 
@@ -1878,6 +1882,22 @@
         }
 
         /**
+         * Returns the proposed importance provided by the {@link NotificationAssistantService}.
+         *
+         * This can be used to suggest that the user change the importance of this type of
+         * notification moving forward. A value of
+         * {@link NotificationManager#IMPORTANCE_UNSPECIFIED} means that the NAS has not recommended
+         * a change to the importance, and no UI should be shown to the user. See
+         * {@link Adjustment#KEY_IMPORTANCE_PROPOSAL}.
+         *
+         * @return the importance of the notification
+         * @hide
+         */
+        public @NotificationManager.Importance int getProposedImportance() {
+            return mProposedImportance;
+        }
+
+        /**
          * If the system has overridden the group key, then this will be non-null, and this
          * key should be used to bundle notifications.
          */
@@ -2041,7 +2061,7 @@
                 boolean noisy, ArrayList<Notification.Action> smartActions,
                 ArrayList<CharSequence> smartReplies, boolean canBubble,
                 boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo,
-                int rankingAdjustment, boolean isBubble) {
+                int rankingAdjustment, boolean isBubble, int proposedImportance) {
             mKey = key;
             mRank = rank;
             mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
@@ -2067,6 +2087,7 @@
             mShortcutInfo = shortcutInfo;
             mRankingAdjustment = rankingAdjustment;
             mIsBubble = isBubble;
+            mProposedImportance = proposedImportance;
         }
 
         /**
@@ -2107,7 +2128,8 @@
                     other.mIsConversation,
                     other.mShortcutInfo,
                     other.mRankingAdjustment,
-                    other.mIsBubble);
+                    other.mIsBubble,
+                    other.mProposedImportance);
         }
 
         /**
@@ -2166,7 +2188,8 @@
                     &&  Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()),
                     (other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId()))
                     && Objects.equals(mRankingAdjustment, other.mRankingAdjustment)
-                    && Objects.equals(mIsBubble, other.mIsBubble);
+                    && Objects.equals(mIsBubble, other.mIsBubble)
+                    && Objects.equals(mProposedImportance, other.mProposedImportance);
         }
     }
 
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index e285b1c..9d8c963 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -953,7 +953,7 @@
 
     private static Uri safeUri(TypedXmlPullParser parser, String att) {
         final String val = parser.getAttributeValue(null, att);
-        if (TextUtils.isEmpty(val)) return null;
+        if (val == null) return null;
         return Uri.parse(val);
     }
 
diff --git a/core/java/android/service/quickaccesswallet/WalletCard.java b/core/java/android/service/quickaccesswallet/WalletCard.java
index b09d2e9..e234755 100644
--- a/core/java/android/service/quickaccesswallet/WalletCard.java
+++ b/core/java/android/service/quickaccesswallet/WalletCard.java
@@ -16,6 +16,7 @@
 
 package android.service.quickaccesswallet;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.PendingIntent;
@@ -24,28 +25,73 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
 /**
  * A {@link WalletCard} can represent anything that a user might carry in their wallet -- a credit
  * card, library card, transit pass, etc. Cards are identified by a String identifier and contain a
- * card image, card image content description, and a {@link PendingIntent} to be used if the user
- * clicks on the card. Cards may be displayed with an icon and label, though these are optional.
+ * card type, card image, card image content description, and a {@link PendingIntent} to be used if
+ * the user clicks on the card. Cards may be displayed with an icon and label, though these are
+ * optional. Non-payment cards will also have a second image that will be displayed when the card is
+ * tapped.
  */
+
 public final class WalletCard implements Parcelable {
 
+    /**
+     * Unknown cards refer to cards whose types are unspecified.
+     * @hide
+     */
+    public static final int CARD_TYPE_UNKNOWN = 0;
+
+    /**
+     * Payment cards refer to credit cards, debit cards or any other cards in the wallet used to
+     * make cash-equivalent payments.
+     * @hide
+     */
+    public static final int CARD_TYPE_PAYMENT = 1;
+
+    /**
+     * Non-payment cards refer to any cards that are not used for cash-equivalent payment, including
+     * event tickets, flights, offers, loyalty cards, gift cards and transit tickets.
+     * @hide
+     */
+    public static final int CARD_TYPE_NON_PAYMENT = 2;
+
     private final String mCardId;
+    private final int mCardType;
     private final Icon mCardImage;
     private final CharSequence mContentDescription;
     private final PendingIntent mPendingIntent;
     private final Icon mCardIcon;
     private final CharSequence mCardLabel;
+    private final Icon mNonPaymentCardSecondaryImage;
 
     private WalletCard(Builder builder) {
         this.mCardId = builder.mCardId;
+        this.mCardType = builder.mCardType;
         this.mCardImage = builder.mCardImage;
         this.mContentDescription = builder.mContentDescription;
         this.mPendingIntent = builder.mPendingIntent;
         this.mCardIcon = builder.mCardIcon;
         this.mCardLabel = builder.mCardLabel;
+        this.mNonPaymentCardSecondaryImage = builder.mNonPaymentCardSecondaryImage;
+    }
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"CARD_TYPE_"}, value = {
+            CARD_TYPE_UNKNOWN,
+            CARD_TYPE_PAYMENT,
+            CARD_TYPE_NON_PAYMENT
+    })
+    public @interface CardType {
     }
 
     @Override
@@ -56,29 +102,44 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString(mCardId);
+        dest.writeInt(mCardType);
         mCardImage.writeToParcel(dest, flags);
         TextUtils.writeToParcel(mContentDescription, dest, flags);
         PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
-        if (mCardIcon == null) {
+        writeIconIfNonNull(mCardIcon, dest, flags);
+        TextUtils.writeToParcel(mCardLabel, dest, flags);
+        writeIconIfNonNull(mNonPaymentCardSecondaryImage, dest, flags);
+
+    }
+
+    /** Utility function called by writeToParcel
+     */
+    private void writeIconIfNonNull(Icon icon,  Parcel dest, int flags) {
+        if (icon == null) {
             dest.writeByte((byte) 0);
         } else {
             dest.writeByte((byte) 1);
-            mCardIcon.writeToParcel(dest, flags);
+            icon.writeToParcel(dest, flags);
         }
-        TextUtils.writeToParcel(mCardLabel, dest, flags);
     }
 
     private static WalletCard readFromParcel(Parcel source) {
         String cardId = source.readString();
+        int cardType = source.readInt();
         Icon cardImage = Icon.CREATOR.createFromParcel(source);
         CharSequence contentDesc = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
         PendingIntent pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(source);
         Icon cardIcon = source.readByte() == 0 ? null : Icon.CREATOR.createFromParcel(source);
         CharSequence cardLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
-        return new Builder(cardId, cardImage, contentDesc, pendingIntent)
+        Icon nonPaymentCardSecondaryImage = source.readByte() == 0 ? null :
+                Icon.CREATOR.createFromParcel(source);
+        Builder builder = new Builder(cardId, cardType, cardImage, contentDesc, pendingIntent)
                 .setCardIcon(cardIcon)
-                .setCardLabel(cardLabel)
-                .build();
+                .setCardLabel(cardLabel);
+
+        return cardType == CARD_TYPE_NON_PAYMENT
+                ? builder.setNonPaymentCardSecondaryImage(nonPaymentCardSecondaryImage).build() :
+                 builder.build();
     }
 
     @NonNull
@@ -104,6 +165,16 @@
     }
 
     /**
+     * Returns the card type.
+     * @hide
+     */
+    @NonNull
+    @CardType
+    public int getCardType() {
+        return mCardType;
+    }
+
+    /**
      * The visual representation of the card. If the card image Icon is a bitmap, it should have a
      * width of {@link GetWalletCardsRequest#getCardWidthPx()} and a height of {@link
      * GetWalletCardsRequest#getCardHeightPx()}.
@@ -158,23 +229,37 @@
     }
 
     /**
-     * Builder for {@link WalletCard} objects. You must to provide cardId, cardImage,
+     * Visual representation of the card when it is tapped. May include additional information
+     *  unique to the card, such as a barcode or number. Only valid for CARD_TYPE_NON_PAYMENT.
+     * @hide
+     */
+    @Nullable
+    public Icon getNonPaymentCardSecondaryImage() {
+        return mNonPaymentCardSecondaryImage;
+    }
+
+    /**
+     * Builder for {@link WalletCard} objects. You must provide cardId, cardImage,
      * contentDescription, and pendingIntent. If the card is opaque and should be shown with
      * elevation, set hasShadow to true. cardIcon and cardLabel are optional.
      */
     public static final class Builder {
         private String mCardId;
+        private int mCardType;
         private Icon mCardImage;
         private CharSequence mContentDescription;
         private PendingIntent mPendingIntent;
         private Icon mCardIcon;
         private CharSequence mCardLabel;
+        private Icon mNonPaymentCardSecondaryImage;
 
         /**
          * @param cardId             The card id must be non-null and unique within the list of
          *                           cards returned. <b>Note:
          *                           </b> this card ID should <b>not</b> contain PII (Personally
          *                           Identifiable Information, such as username or email address).
+         * @param cardType           Integer representing the card type. The card type must be
+         *                           non-null.
          * @param cardImage          The visual representation of the card. If the card image Icon
          *                           is a bitmap, it should have a width of {@link
          *                           GetWalletCardsRequest#getCardWidthPx()} and a height of {@link
@@ -193,15 +278,30 @@
          *                           request device unlock before sending the pending intent. It is
          *                           recommended that the pending intent be immutable (use {@link
          *                           PendingIntent#FLAG_IMMUTABLE}).
+         * @hide
+         */
+        public Builder(@NonNull String cardId,
+                @NonNull @CardType int cardType,
+                @NonNull Icon cardImage,
+                @NonNull CharSequence contentDescription,
+                @NonNull PendingIntent pendingIntent
+        ) {
+            mCardId = cardId;
+            mCardType = cardType;
+            mCardImage = cardImage;
+            mContentDescription = contentDescription;
+            mPendingIntent = pendingIntent;
+        }
+
+        /**
+         * Called when a card type is not provided, in which case it defaults to CARD_TYPE_UNKNOWN.
          */
         public Builder(@NonNull String cardId,
                 @NonNull Icon cardImage,
                 @NonNull CharSequence contentDescription,
                 @NonNull PendingIntent pendingIntent) {
-            mCardId = cardId;
-            mCardImage = cardImage;
-            mContentDescription = contentDescription;
-            mPendingIntent = pendingIntent;
+            this(cardId, WalletCard.CARD_TYPE_UNKNOWN, cardImage, contentDescription,
+                    pendingIntent);
         }
 
         /**
@@ -236,6 +336,20 @@
         }
 
         /**
+         * Visual representation of the card when it is tapped. May include additional information
+         *  unique to the card, such as a barcode or number. Only valid for CARD_TYPE_NON_PAYMENT.
+         * @hide
+         */
+        @NonNull
+        public Builder
+                setNonPaymentCardSecondaryImage(@Nullable Icon nonPaymentCardSecondaryImage) {
+            Preconditions.checkState(mCardType == CARD_TYPE_NON_PAYMENT,
+                    "This field can only be set on non-payment cards");
+            mNonPaymentCardSecondaryImage = nonPaymentCardSecondaryImage;
+            return this;
+        }
+
+        /**
          * Builds a new {@link WalletCard} instance.
          *
          * @return A built response.
diff --git a/core/java/android/service/smartspace/SmartspaceService.java b/core/java/android/service/smartspace/SmartspaceService.java
index 3a148df..b13a069 100644
--- a/core/java/android/service/smartspace/SmartspaceService.java
+++ b/core/java/android/service/smartspace/SmartspaceService.java
@@ -302,7 +302,7 @@
                 Slog.e(TAG, "Callback is null, likely the binder has died.");
                 return false;
             }
-            return mCallback.equals(callback);
+            return mCallback.asBinder().equals(callback.asBinder());
         }
 
         @Override
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 2d1a41e..d53ad17 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -61,6 +61,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -170,6 +171,7 @@
             Float.NEGATIVE_INFINITY);
 
     private static final int NOTIFY_COLORS_RATE_LIMIT_MS = 1000;
+    private static final int PROCESS_LOCAL_COLORS_INTERVAL_MS = 1000;
 
     private static final boolean ENABLE_WALLPAPER_DIMMING =
             SystemProperties.getBoolean("persist.debug.enable_wallpaper_dimming", true);
@@ -179,6 +181,9 @@
     private final ArrayList<Engine> mActiveEngines
             = new ArrayList<Engine>();
 
+    private Handler mBackgroundHandler;
+    private HandlerThread mBackgroundThread;
+
     static final class WallpaperCommand {
         String action;
         int x;
@@ -197,14 +202,6 @@
      */
     public class Engine {
         IWallpaperEngineWrapper mIWallpaperEngine;
-        final ArraySet<RectF> mLocalColorAreas = new ArraySet<>(4);
-        final ArraySet<RectF> mLocalColorsToAdd = new ArraySet<>(4);
-
-        // 2D matrix [x][y] to represent a page of a portion of a window
-        EngineWindowPage[] mWindowPages = new EngineWindowPage[0];
-        Bitmap mLastScreenshot;
-        int mLastWindowPage = -1;
-        private boolean mResetWindowPages;
 
         // Copies from mIWallpaperEngine.
         HandlerCaller mCaller;
@@ -266,21 +263,37 @@
 
         final Object mLock = new Object();
         boolean mOffsetMessageEnqueued;
+
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
         float mPendingXOffset;
         float mPendingYOffset;
         float mPendingXOffsetStep;
         float mPendingYOffsetStep;
+
+        /**
+         * local color extraction related fields
+         * to be used by the background thread only (except the atomic boolean)
+         */
+        final ArraySet<RectF> mLocalColorAreas = new ArraySet<>(4);
+        final ArraySet<RectF> mLocalColorsToAdd = new ArraySet<>(4);
+        private long mLastProcessLocalColorsTimestamp;
+        private AtomicBoolean mProcessLocalColorsPending = new AtomicBoolean(false);
+        private int mPixelCopyCount = 0;
+        // 2D matrix [x][y] to represent a page of a portion of a window
+        EngineWindowPage[] mWindowPages = new EngineWindowPage[0];
+        Bitmap mLastScreenshot;
+        private boolean mResetWindowPages;
+
         boolean mPendingSync;
         MotionEvent mPendingMove;
         boolean mIsInAmbientMode;
 
-        // Needed for throttling onComputeColors.
+        // used to throttle onComputeColors
         private long mLastColorInvalidation;
         private final Runnable mNotifyColorsChanged = this::notifyColorsChanged;
+
         private final Supplier<Long> mClockFunction;
         private final Handler mHandler;
-
         private Display mDisplay;
         private Context mDisplayContext;
         private int mDisplayState;
@@ -820,7 +833,7 @@
                             + "was not established.");
                 }
                 mResetWindowPages = true;
-                processLocalColors(mPendingXOffset, mPendingXOffsetStep);
+                processLocalColors();
             } catch (RemoteException e) {
                 Log.w(TAG, "Can't notify system because wallpaper connection was lost.", e);
             }
@@ -1359,7 +1372,7 @@
                             resetWindowPages();
                             mSession.finishDrawing(mWindow, null /* postDrawTransaction */,
                                                    Integer.MAX_VALUE);
-                            processLocalColors(mPendingXOffset, mPendingXOffsetStep);
+                            processLocalColors();
                         }
                         reposition();
                         reportEngineShown(shouldWaitForEngineShown());
@@ -1504,7 +1517,7 @@
             if (!mDestroyed) {
                 mVisible = visible;
                 reportVisibility();
-                if (mReportedVisible) processLocalColors(mPendingXOffset, mPendingXOffsetStep);
+                if (mReportedVisible) processLocalColors();
             } else {
                 AnimationHandler.requestAnimatorsEnabled(visible, this);
             }
@@ -1588,12 +1601,41 @@
             }
 
             // setup local color extraction data
-            processLocalColors(xOffset, xOffsetStep);
+            processLocalColors();
         }
 
-        private void processLocalColors(float xOffset, float xOffsetStep) {
+        /**
+         * Thread-safe util to call {@link #processLocalColorsInternal} with a minimum interval of
+         * {@link #PROCESS_LOCAL_COLORS_INTERVAL_MS} between two calls.
+         */
+        private void processLocalColors() {
+            if (mProcessLocalColorsPending.compareAndSet(false, true)) {
+                final long now = mClockFunction.get();
+                final long timeSinceLastColorProcess = now - mLastProcessLocalColorsTimestamp;
+                final long timeToWait = Math.max(0,
+                        PROCESS_LOCAL_COLORS_INTERVAL_MS - timeSinceLastColorProcess);
+
+                mBackgroundHandler.postDelayed(() -> {
+                    mLastProcessLocalColorsTimestamp = now + timeToWait;
+                    mProcessLocalColorsPending.set(false);
+                    processLocalColorsInternal();
+                }, timeToWait);
+            }
+        }
+
+        private void processLocalColorsInternal() {
             // implemented by the wallpaper
             if (supportsLocalColorExtraction()) return;
+            assertBackgroundThread();
+            float xOffset;
+            float xOffsetStep;
+            float wallpaperDimAmount;
+            synchronized (mLock) {
+                xOffset = mPendingXOffset;
+                xOffsetStep = mPendingXOffsetStep;
+                wallpaperDimAmount = mWallpaperDimAmount;
+            }
+
             if (DEBUG) {
                 Log.d(TAG, "processLocalColors " + xOffset + " of step "
                         + xOffsetStep);
@@ -1625,40 +1667,39 @@
 
             float finalXOffsetStep = xOffsetStep;
             float finalXOffset = xOffset;
-            mHandler.post(() -> {
-                Trace.beginSection("WallpaperService#processLocalColors");
-                resetWindowPages();
-                int xPage = xCurrentPage;
-                EngineWindowPage current;
-                if (mWindowPages.length == 0 || (mWindowPages.length != xPages)) {
-                    mWindowPages = new EngineWindowPage[xPages];
-                    initWindowPages(mWindowPages, finalXOffsetStep);
+
+            Trace.beginSection("WallpaperService#processLocalColors");
+            resetWindowPages();
+            int xPage = xCurrentPage;
+            EngineWindowPage current;
+            if (mWindowPages.length == 0 || (mWindowPages.length != xPages)) {
+                mWindowPages = new EngineWindowPage[xPages];
+                initWindowPages(mWindowPages, finalXOffsetStep);
+            }
+            if (mLocalColorsToAdd.size() != 0) {
+                for (RectF colorArea : mLocalColorsToAdd) {
+                    if (!isValid(colorArea)) continue;
+                    mLocalColorAreas.add(colorArea);
+                    int colorPage = getRectFPage(colorArea, finalXOffsetStep);
+                    EngineWindowPage currentPage = mWindowPages[colorPage];
+                    currentPage.setLastUpdateTime(0);
+                    currentPage.removeColor(colorArea);
                 }
-                if (mLocalColorsToAdd.size() != 0) {
-                    for (RectF colorArea : mLocalColorsToAdd) {
-                        if (!isValid(colorArea)) continue;
-                        mLocalColorAreas.add(colorArea);
-                        int colorPage = getRectFPage(colorArea, finalXOffsetStep);
-                        EngineWindowPage currentPage = mWindowPages[colorPage];
-                        currentPage.setLastUpdateTime(0);
-                        currentPage.removeColor(colorArea);
-                    }
-                    mLocalColorsToAdd.clear();
+                mLocalColorsToAdd.clear();
+            }
+            if (xPage >= mWindowPages.length) {
+                if (DEBUG) {
+                    Log.e(TAG, "error xPage >= mWindowPages.length page: " + xPage);
+                    Log.e(TAG, "error on page " + xPage + " out of " + xPages);
+                    Log.e(TAG,
+                            "error on xOffsetStep " + finalXOffsetStep
+                                    + " xOffset " + finalXOffset);
                 }
-                if (xPage >= mWindowPages.length) {
-                    if (DEBUG) {
-                        Log.e(TAG, "error xPage >= mWindowPages.length page: " + xPage);
-                        Log.e(TAG, "error on page " + xPage + " out of " + xPages);
-                        Log.e(TAG,
-                                "error on xOffsetStep " + finalXOffsetStep
-                                        + " xOffset " + finalXOffset);
-                    }
-                    xPage = mWindowPages.length - 1;
-                }
-                current = mWindowPages[xPage];
-                updatePage(current, xPage, xPages, finalXOffsetStep);
-                Trace.endSection();
-            });
+                xPage = mWindowPages.length - 1;
+            }
+            current = mWindowPages[xPage];
+            updatePage(current, xPage, xPages, wallpaperDimAmount);
+            Trace.endSection();
         }
 
         private void initWindowPages(EngineWindowPage[] windowPages, float step) {
@@ -1677,16 +1718,23 @@
             }
         }
 
+        /**
+         * Must be called with the surface lock held.
+         * Must not be called if the surface is not valid.
+         * Will unlock the surface when done using it.
+         */
         void updatePage(EngineWindowPage currentPage, int pageIndx, int numPages,
-                float xOffsetStep) {
+                float wallpaperDimAmount) {
+
+            assertBackgroundThread();
+
             // in case the clock is zero, we start with negative time
             long current = SystemClock.elapsedRealtime() - DEFAULT_UPDATE_SCREENSHOT_DURATION;
             long lapsed = current - currentPage.getLastUpdateTime();
             // Always update the page when the last update time is <= 0
             // This is important especially when the device first boots
-            if (lapsed < DEFAULT_UPDATE_SCREENSHOT_DURATION) {
-                return;
-            }
+            if (lapsed < DEFAULT_UPDATE_SCREENSHOT_DURATION) return;
+
             Surface surface = mSurfaceHolder.getSurface();
             if (!surface.isValid()) return;
             boolean widthIsLarger = mSurfaceSize.x > mSurfaceSize.y;
@@ -1702,33 +1750,42 @@
             Bitmap screenShot = Bitmap.createBitmap(width, height,
                     Bitmap.Config.ARGB_8888);
             final Bitmap finalScreenShot = screenShot;
-            Trace.beginSection("WallpaperService#pixelCopy");
-            PixelCopy.request(surface, screenShot, (res) -> {
-                Trace.endSection();
-                if (DEBUG) Log.d(TAG, "result of pixel copy is " + res);
-                if (res != PixelCopy.SUCCESS) {
-                    Bitmap lastBitmap = currentPage.getBitmap();
-                    // assign the last bitmap taken for now
-                    currentPage.setBitmap(mLastScreenshot);
-                    Bitmap lastScreenshot = mLastScreenshot;
-                    if (lastScreenshot != null && !lastScreenshot.isRecycled()
-                            && !Objects.equals(lastBitmap, lastScreenshot)) {
-                        updatePageColors(currentPage, pageIndx, numPages, xOffsetStep);
+            final String pixelCopySectionName = "WallpaperService#pixelCopy";
+            final int pixelCopyCount = mPixelCopyCount++;
+            Trace.beginAsyncSection(pixelCopySectionName, pixelCopyCount);
+            try {
+                PixelCopy.request(surface, screenShot, (res) -> {
+                    Trace.endAsyncSection(pixelCopySectionName, pixelCopyCount);
+                    if (DEBUG) Log.d(TAG, "result of pixel copy is " + res);
+                    if (res != PixelCopy.SUCCESS) {
+                        Bitmap lastBitmap = currentPage.getBitmap();
+                        // assign the last bitmap taken for now
+                        currentPage.setBitmap(mLastScreenshot);
+                        Bitmap lastScreenshot = mLastScreenshot;
+                        if (lastScreenshot != null && !lastScreenshot.isRecycled()
+                                && !Objects.equals(lastBitmap, lastScreenshot)) {
+                            updatePageColors(currentPage, pageIndx, numPages, wallpaperDimAmount);
+                        }
+                    } else {
+                        mLastScreenshot = finalScreenShot;
+                        // going to hold this lock for a while
+                        currentPage.setBitmap(finalScreenShot);
+                        currentPage.setLastUpdateTime(current);
+                        updatePageColors(currentPage, pageIndx, numPages, wallpaperDimAmount);
                     }
-                } else {
-                    mLastScreenshot = finalScreenShot;
-                    // going to hold this lock for a while
-                    currentPage.setBitmap(finalScreenShot);
-                    currentPage.setLastUpdateTime(current);
-                    updatePageColors(currentPage, pageIndx, numPages, xOffsetStep);
-                }
-            }, mHandler);
-
+                }, mBackgroundHandler);
+            } catch (IllegalArgumentException e) {
+                // this can potentially happen if the surface is invalidated right between the
+                // surface.isValid() check and the PixelCopy operation.
+                // in this case, stop: we'll compute colors on the next processLocalColors call.
+                Log.i(TAG, "Cancelling processLocalColors: exception caught during PixelCopy");
+            }
         }
         // locked by the passed page
-        private void updatePageColors(EngineWindowPage page, int pageIndx, int numPages,
-                float xOffsetStep) {
+        private void updatePageColors(
+                EngineWindowPage page, int pageIndx, int numPages, float wallpaperDimAmount) {
             if (page.getBitmap() == null) return;
+            assertBackgroundThread();
             Trace.beginSection("WallpaperService#updatePageColors");
             if (DEBUG) {
                 Log.d(TAG, "updatePageColorsLocked for page " + pageIndx + " with areas "
@@ -1750,7 +1807,7 @@
                     Log.e(TAG, "Error creating page local color bitmap", e);
                     continue;
                 }
-                WallpaperColors color = WallpaperColors.fromBitmap(target, mWallpaperDimAmount);
+                WallpaperColors color = WallpaperColors.fromBitmap(target, wallpaperDimAmount);
                 target.recycle();
                 WallpaperColors currentColor = page.getColors(area);
 
@@ -1767,17 +1824,26 @@
                                 + " local color callback for area" + area + " for page " + pageIndx
                                 + " of " + numPages);
                     }
-                    try {
-                        mConnection.onLocalWallpaperColorsChanged(area, color,
-                                mDisplayContext.getDisplayId());
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e);
-                    }
+                    mHandler.post(() -> {
+                        try {
+                            mConnection.onLocalWallpaperColorsChanged(area, color,
+                                    mDisplayContext.getDisplayId());
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e);
+                        }
+                    });
                 }
             }
             Trace.endSection();
         }
 
+        private void assertBackgroundThread() {
+            if (!mBackgroundHandler.getLooper().isCurrentThread()) {
+                throw new IllegalStateException(
+                        "ProcessLocalColors should be called from the background thread");
+            }
+        }
+
         private RectF generateSubRect(RectF in, int pageInx, int numPages) {
             float minLeft = (float) (pageInx) / (float) (numPages);
             float maxRight = (float) (pageInx + 1) / (float) (numPages);
@@ -1802,7 +1868,6 @@
             if (supportsLocalColorExtraction()) return;
             if (!mResetWindowPages) return;
             mResetWindowPages = false;
-            mLastWindowPage = -1;
             for (int i = 0; i < mWindowPages.length; i++) {
                 mWindowPages[i].setLastUpdateTime(0L);
             }
@@ -1828,12 +1893,10 @@
             if (DEBUG) {
                 Log.d(TAG, "addLocalColorsAreas adding local color areas " + regions);
             }
-            mHandler.post(() -> {
+            mBackgroundHandler.post(() -> {
                 mLocalColorsToAdd.addAll(regions);
-                processLocalColors(mPendingXOffset, mPendingYOffset);
+                processLocalColors();
             });
-
-
         }
 
         /**
@@ -1843,7 +1906,7 @@
          */
         public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
             if (supportsLocalColorExtraction()) return;
-            mHandler.post(() -> {
+            mBackgroundHandler.post(() -> {
                 float step = mPendingXOffsetStep;
                 mLocalColorsToAdd.removeAll(regions);
                 mLocalColorAreas.removeAll(regions);
@@ -2474,6 +2537,9 @@
     @Override
     public void onCreate() {
         Trace.beginSection("WPMS.onCreate");
+        mBackgroundThread = new HandlerThread("DefaultWallpaperLocalColorExtractor");
+        mBackgroundThread.start();
+        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
         super.onCreate();
         Trace.endSection();
     }
@@ -2486,6 +2552,7 @@
             mActiveEngines.get(i).detach();
         }
         mActiveEngines.clear();
+        mBackgroundThread.quitSafely();
         Trace.endSection();
     }
 
diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java
index a1d6cc8..6da0b63 100644
--- a/core/java/android/text/TextShaper.java
+++ b/core/java/android/text/TextShaper.java
@@ -173,7 +173,7 @@
     private TextShaper() {}
 
     /**
-     * An consumer interface for accepting text shape result.
+     * A consumer interface for accepting text shape result.
      */
     public interface GlyphsConsumer {
         /**
diff --git a/core/java/android/util/SparseSetArray.java b/core/java/android/util/SparseSetArray.java
index b7873b7..61f29a4 100644
--- a/core/java/android/util/SparseSetArray.java
+++ b/core/java/android/util/SparseSetArray.java
@@ -139,4 +139,9 @@
     public T valueAt(int intIndex, int valueIndex) {
         return mData.valueAt(intIndex).valueAt(valueIndex);
     }
+
+    /** @return The set of values for key at position {@code intIndex}. */
+    public ArraySet<T> valuesAt(int intIndex) {
+        return mData.valueAt(intIndex);
+    }
 }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index b100891..35350e8 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.content.res.Resources.ID_NULL;
+import static android.os.Trace.TRACE_TAG_APP;
 import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
 import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
@@ -985,6 +986,22 @@
     private static boolean sAcceptZeroSizeDragShadow;
 
     /**
+     * When true, measure and layout passes of all the newly attached views will be logged with
+     * {@link Trace}, so we can better debug jank due to complex view hierarchies.
+     */
+    private static boolean sTraceLayoutSteps;
+
+    /**
+     * When not null, emits a {@link Trace} instant event and the stacktrace every time a relayout
+     * of a class having this name happens.
+     */
+    private static String sTraceRequestLayoutClass;
+
+    /** Used to avoid computing the full strings each time when layout tracing is enabled. */
+    @Nullable
+    private ViewTraversalTracingStrings mTracingStrings;
+
+    /**
      * Prior to R, {@link #dispatchApplyWindowInsets} had an issue:
      * <p>The modified insets changed by {@link #onApplyWindowInsets} were passed to the
      * entire view hierarchy in prefix order, including siblings as well as siblings of parents
@@ -3532,6 +3549,8 @@
      *                  1               PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE
      *                 1                PFLAG4_DRAG_A11Y_STARTED
      *                1                 PFLAG4_AUTO_HANDWRITING_INITIATION_ENABLED
+     *             1                    PFLAG4_TRAVERSAL_TRACING_ENABLED
+     *            1                     PFLAG4_RELAYOUT_TRACING_ENABLED
      * |-------|-------|-------|-------|
      */
 
@@ -3612,6 +3631,19 @@
      * Indicates that the view enables auto handwriting initiation.
      */
     private static final int PFLAG4_AUTO_HANDWRITING_ENABLED = 0x000010000;
+
+    /**
+     * When set, measure and layout passes of this view will be logged with {@link Trace}, so we
+     * can better debug jank due to complex view hierarchies.
+     */
+    private static final int PFLAG4_TRAVERSAL_TRACING_ENABLED = 0x000040000;
+
+    /**
+     * When set, emits a {@link Trace} instant event and stacktrace every time a requestLayout of
+     * this class happens.
+     */
+    private static final int PFLAG4_RELAYOUT_TRACING_ENABLED = 0x000080000;
+
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -6537,6 +6569,15 @@
         out.append(mRight);
         out.append(',');
         out.append(mBottom);
+        appendId(out);
+        if (mAutofillId != null) {
+            out.append(" aid="); out.append(mAutofillId);
+        }
+        out.append("}");
+        return out.toString();
+    }
+
+    void appendId(StringBuilder out) {
         final int id = getId();
         if (id != NO_ID) {
             out.append(" #");
@@ -6568,11 +6609,6 @@
                 }
             }
         }
-        if (mAutofillId != null) {
-            out.append(" aid="); out.append(mAutofillId);
-        }
-        out.append("}");
-        return out.toString();
     }
 
     /**
@@ -20771,6 +20807,14 @@
         if (isFocused()) {
             notifyFocusChangeToImeFocusController(true /* hasFocus */);
         }
+
+        if (sTraceLayoutSteps) {
+            setTraversalTracingEnabled(true);
+        }
+        if (sTraceRequestLayoutClass != null
+                && sTraceRequestLayoutClass.equals(getClass().getSimpleName())) {
+            setRelayoutTracingEnabled(true);
+        }
     }
 
     /**
@@ -23670,6 +23714,30 @@
         return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
     }
 
+    /**
+     * Enable measure/layout debugging on traces.
+     *
+     * @see Trace
+     * @hide
+     */
+    public static void setTraceLayoutSteps(boolean traceLayoutSteps) {
+        sTraceLayoutSteps = traceLayoutSteps;
+    }
+
+    /**
+     * Enable request layout tracing classes with {@code s} simple name.
+     * <p>
+     * When set, a {@link Trace} instant event and a log with the stacktrace is emitted every
+     * time a requestLayout of a class matching {@code s} name happens.
+     * This applies only to views attached from this point onwards.
+     *
+     * @see Trace#instant(long, String)
+     * @hide
+     */
+    public static void setTracedRequestLayoutClassClass(String s) {
+        sTraceRequestLayoutClass = s;
+    }
+
     private boolean setOpticalFrame(int left, int top, int right, int bottom) {
         Insets parentInsets = mParent instanceof View ?
                 ((View) mParent).getOpticalInsets() : Insets.NONE;
@@ -23704,7 +23772,13 @@
     @SuppressWarnings({"unchecked"})
     public void layout(int l, int t, int r, int b) {
         if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
+            if (isTraversalTracingEnabled()) {
+                Trace.beginSection(mTracingStrings.onMeasureBeforeLayout);
+            }
             onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
+            if (isTraversalTracingEnabled()) {
+                Trace.endSection();
+            }
             mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
         }
 
@@ -23717,7 +23791,13 @@
                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
 
         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
+            if (isTraversalTracingEnabled()) {
+                Trace.beginSection(mTracingStrings.onLayout);
+            }
             onLayout(changed, l, t, r, b);
+            if (isTraversalTracingEnabled()) {
+                Trace.endSection();
+            }
 
             if (shouldDrawRoundScrollbar()) {
                 if(mRoundScrollbarRenderer == null) {
@@ -26274,6 +26354,25 @@
         return (viewRoot != null && viewRoot.isInLayout());
     }
 
+    /** To be used only for debugging purposes. */
+    private void printStackStrace(String name) {
+        Log.d(VIEW_LOG_TAG, "---- ST:" + name);
+
+        StringBuilder sb = new StringBuilder();
+        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+        int startIndex = 1;
+        int endIndex = Math.min(stackTraceElements.length, startIndex + 20); // max 20 entries.
+        for (int i = startIndex; i < endIndex; i++) {
+            StackTraceElement s = stackTraceElements[i];
+            sb.append(s.getMethodName())
+                    .append("(")
+                    .append(s.getFileName())
+                    .append(":")
+                    .append(s.getLineNumber())
+                    .append(") <- ");
+        }
+        Log.d(VIEW_LOG_TAG, name + ": " + sb);
+    }
     /**
      * Call this when something has changed which has invalidated the
      * layout of this view. This will schedule a layout pass of the view
@@ -26287,6 +26386,12 @@
      */
     @CallSuper
     public void requestLayout() {
+        if (isRelayoutTracingEnabled()) {
+            Trace.instantForTrack(TRACE_TAG_APP, "requestLayoutTracing",
+                    mTracingStrings.classSimpleName);
+            printStackStrace(mTracingStrings.requestLayoutStacktracePrefix);
+        }
+
         if (mMeasureCache != null) mMeasureCache.clear();
 
         if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
@@ -26380,8 +26485,14 @@
 
             int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
             if (cacheIndex < 0 || sIgnoreMeasureCache) {
+                if (isTraversalTracingEnabled()) {
+                    Trace.beginSection(mTracingStrings.onMeasure);
+                }
                 // measure ourselves, this should set the measured dimension flag back
                 onMeasure(widthMeasureSpec, heightMeasureSpec);
+                if (isTraversalTracingEnabled()) {
+                    Trace.endSection();
+                }
                 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
             } else {
                 long value = mMeasureCache.valueAt(cacheIndex);
@@ -31551,6 +31662,38 @@
                 == PFLAG4_AUTO_HANDWRITING_ENABLED;
     }
 
+    private void setTraversalTracingEnabled(boolean enabled) {
+        if (enabled) {
+            if (mTracingStrings == null) {
+                mTracingStrings = new ViewTraversalTracingStrings(this);
+            }
+            mPrivateFlags4 |= PFLAG4_TRAVERSAL_TRACING_ENABLED;
+        } else {
+            mPrivateFlags4 &= ~PFLAG4_TRAVERSAL_TRACING_ENABLED;
+        }
+    }
+
+    private boolean isTraversalTracingEnabled() {
+        return (mPrivateFlags4 & PFLAG4_TRAVERSAL_TRACING_ENABLED)
+                == PFLAG4_TRAVERSAL_TRACING_ENABLED;
+    }
+
+    private void setRelayoutTracingEnabled(boolean enabled) {
+        if (enabled) {
+            if (mTracingStrings == null) {
+                mTracingStrings = new ViewTraversalTracingStrings(this);
+            }
+            mPrivateFlags4 |= PFLAG4_RELAYOUT_TRACING_ENABLED;
+        } else {
+            mPrivateFlags4 &= ~PFLAG4_RELAYOUT_TRACING_ENABLED;
+        }
+    }
+
+    private boolean isRelayoutTracingEnabled() {
+        return (mPrivateFlags4 & PFLAG4_RELAYOUT_TRACING_ENABLED)
+                == PFLAG4_RELAYOUT_TRACING_ENABLED;
+    }
+
     /**
      * Collects a {@link ViewTranslationRequest} which represents the content to be translated in
      * the view.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 5f04d58..5cd290a 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -73,11 +73,11 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
-import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
@@ -711,6 +711,7 @@
 
     // These are accessed by multiple threads.
     final Rect mWinFrame; // frame given by window manager.
+    private final Rect mLastLayoutFrame;
     Rect mOverrideInsetsFrame;
 
     final Rect mPendingBackDropFrame = new Rect();
@@ -931,6 +932,7 @@
         mHeight = -1;
         mDirty = new Rect();
         mWinFrame = new Rect();
+        mLastLayoutFrame = new Rect();
         mWindow = new W(this);
         mLeashToken = new Binder();
         mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
@@ -1112,6 +1114,8 @@
         // Update the last resource config in case the resource configuration was changed while
         // activity relaunched.
         updateLastConfigurationFromResources(getConfiguration());
+        // Make sure to report the completion of draw for relaunch with preserved window.
+        reportNextDraw("rebuilt");
     }
 
     private Configuration getConfiguration() {
@@ -1273,7 +1277,7 @@
                     mTmpFrames.attachedFrame = attachedFrame;
                     mTmpFrames.compatScale = compatScale[0];
                     mInvCompatScale = 1f / compatScale[0];
-                } catch (RemoteException e) {
+                } catch (RemoteException | RuntimeException e) {
                     mAdded = false;
                     mView = null;
                     mAttachInfo.mRootView = null;
@@ -1301,7 +1305,7 @@
                         UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
                         mInsetsController.getRequestedVisibilities(), 1f /* compactScale */,
                         mTmpFrames);
-                setFrame(mTmpFrames.frame);
+                setFrame(mTmpFrames.frame, true /* withinRelayout */);
                 registerBackCallbackOnWindow();
                 if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
                     // For apps requesting legacy back behavior, we add a compat callback that
@@ -1389,6 +1393,8 @@
                                 listener, listener.data, mHandler, true /*waitForPresentTime*/);
                         mAttachInfo.mThreadedRenderer.addObserver(mHardwareRendererObserver);
                     }
+                    // Update unbuffered request when set the root view.
+                    mUnbufferedInputSource = mView.mUnbufferedInputSource;
                 }
 
                 view.assignParent(this);
@@ -1825,7 +1831,7 @@
             onMovedToDisplay(displayId, mLastConfigurationFromResources);
         }
 
-        setFrame(frame);
+        setFrame(frame, false /* withinRelayout */);
         mTmpFrames.displayFrame.set(displayFrame);
         if (mTmpFrames.attachedFrame != null && attachedFrame != null) {
             mTmpFrames.attachedFrame.set(attachedFrame);
@@ -2768,7 +2774,7 @@
      * TODO(b/260382739): Apply this to all windows.
      */
     private static boolean shouldOptimizeMeasure(final WindowManager.LayoutParams lp) {
-        return lp.type == TYPE_NOTIFICATION_SHADE;
+        return (lp.privateFlags & PRIVATE_FLAG_OPTIMIZE_MEASURE) != 0;
     }
 
     private Rect getWindowBoundsInsetSystemBars() {
@@ -5741,7 +5747,7 @@
                         mTmpFrames.frame.right = l + w;
                         mTmpFrames.frame.top = t;
                         mTmpFrames.frame.bottom = t + h;
-                        setFrame(mTmpFrames.frame);
+                        setFrame(mTmpFrames.frame, false /* withinRelayout */);
                         maybeHandleWindowMove(mWinFrame);
                     }
                     break;
@@ -8211,7 +8217,7 @@
             // If the position and the size of the frame are both changed, it will trigger a BLAST
             // sync, and we still need to call relayout to obtain the syncSeqId. Otherwise, we just
             // need to send attributes via relayoutAsync.
-            final Rect oldFrame = mWinFrame;
+            final Rect oldFrame = mLastLayoutFrame;
             final Rect newFrame = mTmpFrames.frame;
             final boolean positionChanged =
                     newFrame.top != oldFrame.top || newFrame.left != oldFrame.left;
@@ -8341,7 +8347,7 @@
             params.restore();
         }
 
-        setFrame(mTmpFrames.frame);
+        setFrame(mTmpFrames.frame, true /* withinRelayout */);
         return relayoutResult;
     }
 
@@ -8376,8 +8382,18 @@
         mIsSurfaceOpaque = opaque;
     }
 
-    private void setFrame(Rect frame) {
+    /**
+     * Set the mWinFrame of this window.
+     * @param frame the new frame of this window.
+     * @param withinRelayout {@code true} if this setting is within the relayout, or is the initial
+     *                       setting. That will make sure in the relayout process, we always compare
+     *                       the window frame with the last processed window frame.
+     */
+    private void setFrame(Rect frame, boolean withinRelayout) {
         mWinFrame.set(frame);
+        if (withinRelayout) {
+            mLastLayoutFrame.set(frame);
+        }
 
         final WindowConfiguration winConfig = getCompatWindowConfiguration();
         mPendingBackDropFrame.set(mPendingDragResizing && !winConfig.useWindowFrameForBackdrop()
@@ -8618,6 +8634,8 @@
 
         mInsetsController.dump(prefix, writer);
 
+        mOnBackInvokedDispatcher.dump(prefix, writer);
+
         writer.println(prefix + "View Hierarchy:");
         dumpViewHierarchy(innerPrefix, writer, mView);
     }
@@ -8749,6 +8767,10 @@
             mAdded = false;
             AnimationHandler.removeRequestor(this);
         }
+        if (mSyncBufferCallback != null) {
+            mSyncBufferCallback.onBufferReady(null);
+            mSyncBufferCallback = null;
+        }
         WindowManagerGlobal.getInstance().doRemoveView(this);
     }
 
@@ -9863,9 +9885,12 @@
     }
 
     void checkThread() {
-        if (mThread != Thread.currentThread()) {
+        Thread current = Thread.currentThread();
+        if (mThread != current) {
             throw new CalledFromWrongThreadException(
-                    "Only the original thread that created a view hierarchy can touch its views.");
+                    "Only the original thread that created a view hierarchy can touch its views."
+                            + " Expected: " + mThread.getName()
+                            + " Calling: " + current.getName());
         }
     }
 
diff --git a/core/java/android/view/ViewTraversalTracingStrings.java b/core/java/android/view/ViewTraversalTracingStrings.java
new file mode 100644
index 0000000..7dde87b
--- /dev/null
+++ b/core/java/android/view/ViewTraversalTracingStrings.java
@@ -0,0 +1,63 @@
+/*
+ * 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 android.view;
+
+import android.os.Trace;
+
+/**
+ * Keeps and caches strings used to trace {@link View} traversals.
+ * <p>
+ * This is done to avoid expensive computations of them every time, which can improve performance.
+ */
+class ViewTraversalTracingStrings {
+
+    /** {@link Trace} tag used to mark {@link View#onMeasure(int, int)}. */
+    public final String onMeasure;
+
+    /** {@link Trace} tag used to mark {@link View#onLayout(boolean, int, int, int, int)}. */
+    public final String onLayout;
+
+    /** Caches the view simple name to avoid re-computations. */
+    public final String classSimpleName;
+
+    /** Prefix for request layout stacktraces output in logs. */
+    public final String requestLayoutStacktracePrefix;
+
+    /** {@link Trace} tag used to mark {@link View#onMeasure(int, int)} happening before layout. */
+    public final String onMeasureBeforeLayout;
+
+    /**
+     * @param v {@link View} from where to get the class name.
+     */
+    ViewTraversalTracingStrings(View v) {
+        String className = v.getClass().getSimpleName();
+        classSimpleName = className;
+        onMeasureBeforeLayout = getTraceName("onMeasureBeforeLayout", className, v);
+        onMeasure = getTraceName("onMeasure", className, v);
+        onLayout = getTraceName("onLayout", className, v);
+        requestLayoutStacktracePrefix = "requestLayout " + className;
+    }
+
+    private String getTraceName(String sectionName, String className, View v) {
+        StringBuilder out = new StringBuilder();
+        out.append(sectionName);
+        out.append(" ");
+        out.append(className);
+        v.appendId(out);
+        return out.substring(0, Math.min(out.length() - 1, Trace.MAX_SECTION_NAME_LEN - 1));
+    }
+}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index ed9cb00..17df585 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -814,8 +814,8 @@
     }
 
     /**
-     * Activity level {@link android.content.pm.PackageManager.Property PackageManager
-     * .Property} for an app to inform the system that the activity can be opted-in or opted-out
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the app can be opted-in or opted-out
      * from the compatibility treatment that avoids {@link
      * android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by
      * ignoreRequestedOrientation display setting enabled on the device or by the landscape natural
@@ -833,17 +833,17 @@
      *     <li>Camera compatibility force rotation treatment is active for the package.
      * </ul>
      *
-     * <p>Setting this property to {@code false} informs the system that the activity must be
+     * <p>Setting this property to {@code false} informs the system that the app must be
      * opted-out from the compatibility treatment even if the device manufacturer has opted the app
      * into the treatment.
      *
      * <p><b>Syntax:</b>
      * <pre>
-     * &lt;activity&gt;
+     * &lt;application&gt;
      *   &lt;property
      *     android:name="android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"
      *     android:value="true|false"/&gt;
-     * &lt;/activity&gt;
+     * &lt;/application&gt;
      * </pre>
      *
      * @hide
@@ -853,6 +853,251 @@
             "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
 
     /**
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the application can be opted-in or opted-out
+     * from the compatibility treatment that enables sending a fake focus event for unfocused
+     * resumed split screen activities. This is needed because some game engines wait to get
+     * focus before drawing the content of the app which isn't guaranteed by default in multi-window
+     * modes.
+     *
+     * <p>Device manufacturers can enable this treatment using their discretion on a per-device
+     * basis to improve display compatibility. The treatment also needs to be specifically enabled
+     * on a per-app basis afterwards. This can either be done by device manufacturers or developers.
+     *
+     * <p>With this property set to {@code true}, the system will apply the treatment only if the
+     * device manufacturer had previously enabled it on the device. A fake focus event will be sent
+     * to the app after it is resumed only if the app is in split-screen.
+     *
+     * <p>Setting this property to {@code false} informs the system that the activity must be
+     * opted-out from the compatibility treatment even if the device manufacturer has opted the app
+     * into the treatment.
+     *
+     * <p>If the property remains unset the system will apply the treatment only if it had
+     * previously been enabled both at the device and app level by the device manufacturer.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;application&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"
+     *     android:value="true|false"/&gt;
+     * &lt;/application&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
+
+    /**
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the app should be excluded from the
+     * camera compatibility force rotation treatment.
+     *
+     * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+     * orientation of the device and set opposite to natural orientation for a landscape app
+     * window. Mismatch between them can lead to camera issues like sideways or stretched
+     * viewfinder since this is one of the strongest assumptions that apps make when they implement
+     * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+     * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+     * camera and is removed once camera is closed.
+     *
+     * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>With this property set to {@code true} or unset, the system may apply the force rotation
+     * treatment to fixed orientation activities. Device manufacturers can exclude packages from the
+     * treatment using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code false}, the system will not apply the force rotation
+     * treatment.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;application&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"
+     *     android:value="true|false"/&gt;
+     * &lt;/application&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION =
+            "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION";
+
+    /**
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the app should be excluded
+     * from the activity "refresh" after the camera compatibility force rotation treatment.
+     *
+     * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+     * orientation of the device and set opposite to natural orientation for a landscape app
+     * window. Mismatch between them can lead to camera issues like sideways or stretched
+     * viewfinder since this is one of the strongest assumptions that apps make when they implement
+     * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+     * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+     * camera and is removed once camera is closed.
+     *
+     * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed ->
+     * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle
+     * (if overridden, see {@link #PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE} for context).
+     * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+     * camera preview and can lead to sideways or stretching issues persisting even after force
+     * rotation.
+     *
+     * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>With this property set to {@code true} or unset, the system may "refresh" activity after
+     * the force rotation treatment. Device manufacturers can exclude packages from the "refresh"
+     * using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code false}, the system will not "refresh" activity after the
+     * force rotation treatment.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;application&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"
+     *     android:value="true|false"/&gt;
+     * &lt;/application&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH =
+            "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH";
+
+    /**
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the activity should be or shouldn't be
+     * "refreshed" after the camera compatibility force rotation treatment using "paused ->
+     * resumed" cycle rather than "stopped -> resumed".
+     *
+     * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+     * orientation of the device and set opposite to natural orientation for a landscape app
+     * window. Mismatch between them can lead to camera issues like sideways or stretched
+     * viewfinder since this is one of the strongest assumptions that apps make when they implement
+     * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+     * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+     * camera and is removed once camera is closed.
+     *
+     * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed ->
+     * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle
+     * (if overridden by device manufacturers or using this property). This allows to clear cached
+     * values in apps (e.g., display or camera rotation) that influence camera preview and can lead
+     * to sideways or stretching issues persisting even after force rotation.
+     *
+     * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed"
+     * cycle using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code true}, the system will "refresh" activity after the
+     * force rotation treatment using "resumed -> paused -> resumed" cycle.
+     *
+     * <p>With this property set to {@code false}, the system will not "refresh" activity after the
+     * force rotation treatment using "resumed -> paused -> resumed" cycle even if the device
+     * manufacturer adds the corresponding override.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;application&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"
+     *     android:value="true|false"/&gt;
+     * &lt;/application&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE =
+            "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE";
+
+    /**
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the app should be excluded from the
+     * compatibility override for orientation set by the device manufacturer.
+     *
+     * <p>With this property set to {@code true} or unset, device manufacturers can override
+     * orientation for the app using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code false}, device manufactured per-app override for
+     * orientation won't be applied.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;application&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"
+     *     android:value="true|false"/&gt;
+     * &lt;/application&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE =
+            "android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE";
+
+    /**
+     * Application level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the app should be opted-out from the
+     * compatibility override that fixes display orientation to landscape natural orientation when
+     * an activity is fullscreen.
+     *
+     * <p>When this compat override is enabled and while display is fixed to the landscape natural
+     * orientation, the orientation requested by the activity will be still respected by bounds
+     * resolution logic. For instance, if an activity requests portrait orientation, then activity
+     * will appear in the letterbox mode for fixed orientation with the display rotated to the
+     * lanscape natural orientation.
+     *
+     * <p>The treatment is disabled by default but device manufacturers can enable the treatment
+     * using their discretion to improve display compatibility on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>With this property set to {@code true} or unset, the system wiil use landscape display
+     * orientation when the following conditions are met:
+     * <ul>
+     *     <li>Natural orientation of the display is landscape
+     *     <li>ignoreOrientationRequest display setting is enabled
+     *     <li>Activity is fullscreen.
+     *     <li>Device manufacturer enabled the treatment.
+     * </ul>
+     *
+     * <p>With this property set to {@code false}, device manufactured per-app override for
+     * display orientation won't be applied.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;application&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"
+     *     android:value="true|false"/&gt;
+     * &lt;/application&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE =
+            "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE";
+
+    /**
      * @hide
      */
     public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array";
@@ -2443,6 +2688,15 @@
         public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
 
         /**
+         * Flag to indicate that the view hierarchy of the window can only be measured when
+         * necessary. If a window size can be known by the LayoutParams, we can use the size to
+         * relayout window, and we don't have to measure the view hierarchy before laying out the
+         * views. This reduces the chances to perform measure.
+         * {@hide}
+         */
+        public static final int PRIVATE_FLAG_OPTIMIZE_MEASURE = 0x00000200;
+
+        /**
          * Flag that prevents the wallpaper behind the current window from receiving touch events.
          *
          * {@hide}
@@ -2644,6 +2898,7 @@
                 PRIVATE_FLAG_NO_MOVE_ANIMATION,
                 PRIVATE_FLAG_COMPATIBLE_WINDOW,
                 PRIVATE_FLAG_SYSTEM_ERROR,
+                PRIVATE_FLAG_OPTIMIZE_MEASURE,
                 PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
                 PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
                 PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
@@ -2704,6 +2959,10 @@
                         equals = PRIVATE_FLAG_SYSTEM_ERROR,
                         name = "SYSTEM_ERROR"),
                 @ViewDebug.FlagToString(
+                        mask = PRIVATE_FLAG_OPTIMIZE_MEASURE,
+                        equals = PRIVATE_FLAG_OPTIMIZE_MEASURE,
+                        name = "OPTIMIZE_MEASURE"),
+                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
                         equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
                         name = "DISABLE_WALLPAPER_TOUCH_EVENTS"),
diff --git a/core/java/android/view/autofill/OWNERS b/core/java/android/view/autofill/OWNERS
index 26c59a6..622b0e2 100644
--- a/core/java/android/view/autofill/OWNERS
+++ b/core/java/android/view/autofill/OWNERS
@@ -1,10 +1,6 @@
 # Bug component: 351486
 
-augale@google.com
-haoranzhang@google.com
-joannechung@google.com
-markpun@google.com
-lpeter@google.com
 simranjit@google.com
-tymtsai@google.com
+haoranzhang@google.com
+skxu@google.com
 yunicorn@google.com
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index d067d4b..497f066 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -66,8 +66,7 @@
 import java.util.function.Consumer;
 
 /**
- * <p>The {@link ContentCaptureManager} provides additional ways for for apps to
- * integrate with the content capture subsystem.
+ * <p>Provides additional ways for apps to integrate with the content capture subsystem.
  *
  * <p>Content capture provides real-time, continuous capture of application activity, display and
  * events to an intelligence service that is provided by the Android system. The intelligence
diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java
index 11f1b6f1..4c87489 100644
--- a/core/java/android/webkit/WebResourceError.java
+++ b/core/java/android/webkit/WebResourceError.java
@@ -19,7 +19,7 @@
 import android.annotation.SystemApi;
 
 /**
- * Encapsulates information about errors occured during loading of web resources. See
+ * Encapsulates information about errors that occurred during loading of web resources. See
  * {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)}
  */
 public abstract class WebResourceError {
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 1024e2e..940b133 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -18,10 +18,8 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.view.RemoteAnimationTarget;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -52,8 +50,6 @@
 
     @SwipeEdge
     private final int mSwipeEdge;
-    @Nullable
-    private final RemoteAnimationTarget mDepartingAnimationTarget;
 
     /**
      * Creates a new {@link BackEvent} instance.
@@ -62,16 +58,12 @@
      * @param touchY Absolute Y location of the touch point of this event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param swipeEdge Indicates which edge the swipe starts from.
-     * @param departingAnimationTarget The remote animation target of the departing application
-     *                                 window.
      */
-    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
-            @Nullable RemoteAnimationTarget departingAnimationTarget) {
+    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) {
         mTouchX = touchX;
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
-        mDepartingAnimationTarget = departingAnimationTarget;
     }
 
     private BackEvent(@NonNull Parcel in) {
@@ -79,7 +71,6 @@
         mTouchY = in.readFloat();
         mProgress = in.readFloat();
         mSwipeEdge = in.readInt();
-        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
     }
 
     public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() {
@@ -105,11 +96,24 @@
         dest.writeFloat(mTouchY);
         dest.writeFloat(mProgress);
         dest.writeInt(mSwipeEdge);
-        dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
 
     /**
-     * Returns a value between 0 and 1 on how far along the back gesture is.
+     * Returns a value between 0 and 1 on how far along the back gesture is. This value is
+     * driven by the horizontal location of the touch point, and should be used as the fraction to
+     * seek the predictive back animation with. Specifically,
+     * <ol>
+     * <li>The progress is 0 when the touch is at the starting edge of the screen (left or right),
+     * and animation should seek to its start state.
+     * <li>The progress is approximately 1 when the touch is at the opposite side of the screen,
+     * and animation should seek to its end state. Exact end value may vary depending on
+     * screen size.
+     * </ol>
+     * <li> After the gesture finishes in cancel state, this method keeps getting invoked until the
+     * progress value animates back to 0.
+     * </ol>
+     * In-between locations are linearly interpolated based on horizontal distance from the starting
+     * edge and smooth clamped to 1 when the distance exceeds a system-wide threshold.
      */
     public float getProgress() {
         return mProgress;
@@ -136,16 +140,6 @@
         return mSwipeEdge;
     }
 
-    /**
-     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
-     * or {@code null} if the top window should not be moved for the current type of back
-     * destination.
-     */
-    @Nullable
-    public RemoteAnimationTarget getDepartingAnimationTarget() {
-        return mDepartingAnimationTarget;
-    }
-
     @Override
     public String toString() {
         return "BackEvent{"
@@ -153,7 +147,6 @@
                 + ", mTouchY=" + mTouchY
                 + ", mProgress=" + mProgress
                 + ", mSwipeEdge" + mSwipeEdge
-                + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
                 + "}";
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/core/java/android/window/BackMotionEvent.aidl
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
rename to core/java/android/window/BackMotionEvent.aidl
index 67733e9..7c675c3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/core/java/android/window/BackMotionEvent.aidl
@@ -11,15 +11,12 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package android.window;
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+/**
+ * @hide
+ */
+parcelable BackMotionEvent;
diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java
new file mode 100644
index 0000000..8012a1c
--- /dev/null
+++ b/core/java/android/window/BackMotionEvent.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.RemoteAnimationTarget;
+
+/**
+ * Object used to report back gesture progress. Holds information about a {@link BackEvent} plus
+ * any {@link RemoteAnimationTarget} the gesture manipulates.
+ *
+ * @see BackEvent
+ * @hide
+ */
+public final class BackMotionEvent implements Parcelable {
+    private final float mTouchX;
+    private final float mTouchY;
+    private final float mProgress;
+
+    @BackEvent.SwipeEdge
+    private final int mSwipeEdge;
+    @Nullable
+    private final RemoteAnimationTarget mDepartingAnimationTarget;
+
+    /**
+     * Creates a new {@link BackMotionEvent} instance.
+     *
+     * @param touchX Absolute X location of the touch point of this event.
+     * @param touchY Absolute Y location of the touch point of this event.
+     * @param progress Value between 0 and 1 on how far along the back gesture is.
+     * @param swipeEdge Indicates which edge the swipe starts from.
+     * @param departingAnimationTarget The remote animation target of the departing
+     *                                 application window.
+     */
+    public BackMotionEvent(float touchX, float touchY, float progress,
+            @BackEvent.SwipeEdge int swipeEdge,
+            @Nullable RemoteAnimationTarget departingAnimationTarget) {
+        mTouchX = touchX;
+        mTouchY = touchY;
+        mProgress = progress;
+        mSwipeEdge = swipeEdge;
+        mDepartingAnimationTarget = departingAnimationTarget;
+    }
+
+    private BackMotionEvent(@NonNull Parcel in) {
+        mTouchX = in.readFloat();
+        mTouchY = in.readFloat();
+        mProgress = in.readFloat();
+        mSwipeEdge = in.readInt();
+        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
+    }
+
+    @NonNull
+    public static final Creator<BackMotionEvent> CREATOR = new Creator<BackMotionEvent>() {
+        @Override
+        public BackMotionEvent createFromParcel(Parcel in) {
+            return new BackMotionEvent(in);
+        }
+
+        @Override
+        public BackMotionEvent[] newArray(int size) {
+            return new BackMotionEvent[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeFloat(mTouchX);
+        dest.writeFloat(mTouchY);
+        dest.writeFloat(mProgress);
+        dest.writeInt(mSwipeEdge);
+        dest.writeTypedObject(mDepartingAnimationTarget, flags);
+    }
+
+    /**
+     * Returns the progress of a {@link BackEvent}.
+     *
+     * @see BackEvent#getProgress()
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getProgress() {
+        return mProgress;
+    }
+
+    /**
+     * Returns the absolute X location of the touch point.
+     */
+    public float getTouchX() {
+        return mTouchX;
+    }
+
+    /**
+     * Returns the absolute Y location of the touch point.
+     */
+    public float getTouchY() {
+        return mTouchY;
+    }
+
+    /**
+     * Returns the screen edge that the swipe starts from.
+     */
+    @BackEvent.SwipeEdge
+    public int getSwipeEdge() {
+        return mSwipeEdge;
+    }
+
+    /**
+     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
+     * or {@code null} if the top window should not be moved for the current type of back
+     * destination.
+     */
+    @Nullable
+    public RemoteAnimationTarget getDepartingAnimationTarget() {
+        return mDepartingAnimationTarget;
+    }
+
+    @Override
+    public String toString() {
+        return "BackMotionEvent{"
+                + "mTouchX=" + mTouchX
+                + ", mTouchY=" + mTouchY
+                + ", mProgress=" + mProgress
+                + ", mSwipeEdge" + mSwipeEdge
+                + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
+                + "}";
+    }
+}
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
index dd4385c..b22f967 100644
--- a/core/java/android/window/BackProgressAnimator.java
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -16,8 +16,10 @@
 
 package android.window;
 
+import android.annotation.NonNull;
 import android.util.FloatProperty;
 
+import com.android.internal.dynamicanimation.animation.DynamicAnimation;
 import com.android.internal.dynamicanimation.animation.SpringAnimation;
 import com.android.internal.dynamicanimation.animation.SpringForce;
 
@@ -40,7 +42,7 @@
     private final SpringAnimation mSpring;
     private ProgressCallback mCallback;
     private float mProgress = 0;
-    private BackEvent mLastBackEvent;
+    private BackMotionEvent mLastBackEvent;
     private boolean mStarted = false;
 
     private void setProgress(float progress) {
@@ -82,9 +84,9 @@
     /**
      * Sets a new target position for the back progress.
      *
-     * @param event the {@link BackEvent} containing the latest target progress.
+     * @param event the {@link BackMotionEvent} containing the latest target progress.
      */
-    public void onBackProgressed(BackEvent event) {
+    public void onBackProgressed(BackMotionEvent event) {
         if (!mStarted) {
             return;
         }
@@ -95,11 +97,11 @@
     /**
      * Starts the back progress animation.
      *
-     * @param event the {@link BackEvent} that started the gesture.
+     * @param event the {@link BackMotionEvent} that started the gesture.
      * @param callback the back callback to invoke for the gesture. It will receive back progress
      *                 dispatches as the progress animation updates.
      */
-    public void onBackStarted(BackEvent event, ProgressCallback callback) {
+    public void onBackStarted(BackMotionEvent event, ProgressCallback callback) {
         reset();
         mLastBackEvent = event;
         mCallback = callback;
@@ -123,14 +125,34 @@
         mProgress = 0;
     }
 
+    /**
+     * Animate the back progress animation from current progress to start position.
+     * This should be called when back is cancelled.
+     *
+     * @param finishCallback the callback to be invoked when the progress is reach to 0.
+     */
+    public void onBackCancelled(@NonNull Runnable finishCallback) {
+        final DynamicAnimation.OnAnimationEndListener listener =
+                new DynamicAnimation.OnAnimationEndListener() {
+            @Override
+            public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
+                    float velocity) {
+                mSpring.removeEndListener(this);
+                finishCallback.run();
+                reset();
+            }
+        };
+        mSpring.addEndListener(listener);
+        mSpring.animateToFinalPosition(0);
+    }
+
     private void updateProgressValue(float progress) {
         if (mLastBackEvent == null || mCallback == null || !mStarted) {
             return;
         }
         mCallback.onProgressUpdate(
                 new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
-                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(),
-                        mLastBackEvent.getDepartingAnimationTarget()));
+                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge()));
     }
 
 }
diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl
index 6af8ddd..159c0e8 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -17,7 +17,7 @@
 
 package android.window;
 
-import android.window.BackEvent;
+import android.window.BackMotionEvent;
 
 /**
  * Interface that wraps a {@link OnBackInvokedCallback} object, to be stored in window manager
@@ -30,18 +30,19 @@
     * Called when a back gesture has been started, or back button has been pressed down.
     * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}.
     *
-    * @param backEvent The {@link BackEvent} containing information about the touch or button press.
+    * @param backMotionEvent The {@link BackMotionEvent} containing information about the touch
+    *        or button press.
     */
-    void onBackStarted(in BackEvent backEvent);
+    void onBackStarted(in BackMotionEvent backMotionEvent);
 
     /**
      * Called on back gesture progress.
      * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}.
      *
-     * @param backEvent The {@link BackEvent} containing information about the latest touch point
-     *                  and the progress that the back animation should seek to.
+     * @param backMotionEvent The {@link BackMotionEvent} containing information about the latest
+     *                        touch point and the progress that the back animation should seek to.
      */
-    void onBackProgressed(in BackEvent backEvent);
+    void onBackProgressed(in BackMotionEvent backMotionEvent);
 
     /**
      * Called when a back gesture or back button press has been cancelled.
diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl
index e6bb1f6..0032b9c 100644
--- a/core/java/android/window/ITaskOrganizerController.aidl
+++ b/core/java/android/window/ITaskOrganizerController.aidl
@@ -40,7 +40,8 @@
     void unregisterTaskOrganizer(ITaskOrganizer organizer);
 
     /** Creates a persistent root task in WM for a particular windowing-mode. */
-    void createRootTask(int displayId, int windowingMode, IBinder launchCookie);
+    void createRootTask(int displayId, int windowingMode, IBinder launchCookie,
+            boolean removeWithTaskOrganizer);
 
     /** Deletes a persistent root task in WM */
     boolean deleteRootTask(in WindowContainerToken task);
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/TaskFragmentAnimationParams.java b/core/java/android/window/TaskFragmentAnimationParams.java
index 12ad914..c8f6327 100644
--- a/core/java/android/window/TaskFragmentAnimationParams.java
+++ b/core/java/android/window/TaskFragmentAnimationParams.java
@@ -33,6 +33,13 @@
     public static final TaskFragmentAnimationParams DEFAULT =
             new TaskFragmentAnimationParams.Builder().build();
 
+    /**
+     * The default value for animation background color, which means to use the theme window
+     * background color.
+     */
+    @ColorInt
+    public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0;
+
     @ColorInt
     private final int mAnimationBackgroundColor;
 
@@ -104,12 +111,13 @@
     public static final class Builder {
 
         @ColorInt
-        private int mAnimationBackgroundColor = 0;
+        private int mAnimationBackgroundColor = DEFAULT_ANIMATION_BACKGROUND_COLOR;
 
         /**
          * Sets the {@link ColorInt} to use for the background during the animation with this
          * TaskFragment if the animation requires a background. The default value is
-         * {@code 0}, which is to use the theme window background.
+         * {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}, which is to use the theme window background
+         * color.
          *
          * @param color a packed color int, {@code AARRGGBB}, for the animation background color.
          * @return this {@link Builder}.
diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java
index c9ddf92..203d79a 100644
--- a/core/java/android/window/TaskFragmentCreationParams.java
+++ b/core/java/android/window/TaskFragmentCreationParams.java
@@ -71,20 +71,42 @@
      *
      * This is needed in case we need to launch a placeholder Activity to split below a transparent
      * always-expand Activity.
+     *
+     * This should not be used with {@link #mPairedActivityToken}.
      */
     @Nullable
     private final IBinder mPairedPrimaryFragmentToken;
 
+    /**
+     * The Activity token to place the new TaskFragment on top of.
+     * When it is set, the new TaskFragment will be positioned right above the target Activity.
+     * Otherwise, the new TaskFragment will be positioned on the top of the Task by default.
+     *
+     * This is needed in case we need to place an Activity into TaskFragment to launch placeholder
+     * below a transparent always-expand Activity, or when there is another Intent being started in
+     * a TaskFragment above.
+     *
+     * This should not be used with {@link #mPairedPrimaryFragmentToken}.
+     */
+    @Nullable
+    private final IBinder mPairedActivityToken;
+
     private TaskFragmentCreationParams(
             @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken,
             @NonNull IBinder ownerToken, @NonNull Rect initialBounds,
-            @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken) {
+            @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken,
+            @Nullable IBinder pairedActivityToken) {
+        if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) {
+            throw new IllegalArgumentException("pairedPrimaryFragmentToken and"
+                    + " pairedActivityToken should not be set at the same time.");
+        }
         mOrganizer = organizer;
         mFragmentToken = fragmentToken;
         mOwnerToken = ownerToken;
         mInitialBounds.set(initialBounds);
         mWindowingMode = windowingMode;
         mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken;
+        mPairedActivityToken = pairedActivityToken;
     }
 
     @NonNull
@@ -121,6 +143,15 @@
         return mPairedPrimaryFragmentToken;
     }
 
+    /**
+     * TODO(b/232476698): remove the hide with adding CTS for this in next release.
+     * @hide
+     */
+    @Nullable
+    public IBinder getPairedActivityToken() {
+        return mPairedActivityToken;
+    }
+
     private TaskFragmentCreationParams(Parcel in) {
         mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in);
         mFragmentToken = in.readStrongBinder();
@@ -128,6 +159,7 @@
         mInitialBounds.readFromParcel(in);
         mWindowingMode = in.readInt();
         mPairedPrimaryFragmentToken = in.readStrongBinder();
+        mPairedActivityToken = in.readStrongBinder();
     }
 
     /** @hide */
@@ -139,6 +171,7 @@
         mInitialBounds.writeToParcel(dest, flags);
         dest.writeInt(mWindowingMode);
         dest.writeStrongBinder(mPairedPrimaryFragmentToken);
+        dest.writeStrongBinder(mPairedActivityToken);
     }
 
     @NonNull
@@ -164,6 +197,7 @@
                 + " initialBounds=" + mInitialBounds
                 + " windowingMode=" + mWindowingMode
                 + " pairedFragmentToken=" + mPairedPrimaryFragmentToken
+                + " pairedActivityToken=" + mPairedActivityToken
                 + "}";
     }
 
@@ -194,6 +228,9 @@
         @Nullable
         private IBinder mPairedPrimaryFragmentToken;
 
+        @Nullable
+        private IBinder mPairedActivityToken;
+
         public Builder(@NonNull TaskFragmentOrganizerToken organizer,
                 @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
             mOrganizer = organizer;
@@ -224,6 +261,8 @@
          * This is needed in case we need to launch a placeholder Activity to split below a
          * transparent always-expand Activity.
          *
+         * This should not be used with {@link #setPairedActivityToken}.
+         *
          * TODO(b/232476698): remove the hide with adding CTS for this in next release.
          * @hide
          */
@@ -233,11 +272,32 @@
             return this;
         }
 
+        /**
+         * Sets the Activity token to place the new TaskFragment on top of.
+         * When it is set, the new TaskFragment will be positioned right above the target Activity.
+         * Otherwise, the new TaskFragment will be positioned on the top of the Task by default.
+         *
+         * This is needed in case we need to place an Activity into TaskFragment to launch
+         * placeholder below a transparent always-expand Activity, or when there is another Intent
+         * being started in a TaskFragment above.
+         *
+         * This should not be used with {@link #setPairedPrimaryFragmentToken}.
+         *
+         * TODO(b/232476698): remove the hide with adding CTS for this in next release.
+         * @hide
+         */
+        @NonNull
+        public Builder setPairedActivityToken(@Nullable IBinder activityToken) {
+            mPairedActivityToken = activityToken;
+            return this;
+        }
+
         /** Constructs the options to create TaskFragment with. */
         @NonNull
         public TaskFragmentCreationParams build() {
             return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken,
-                    mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken);
+                    mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken,
+                    mPairedActivityToken);
         }
     }
 }
diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java
index bffd4e4..02878f8 100644
--- a/core/java/android/window/TaskOrganizer.java
+++ b/core/java/android/window/TaskOrganizer.java
@@ -152,15 +152,31 @@
      * @param windowingMode Windowing mode to put the root task in.
      * @param launchCookie Launch cookie to associate with the task so that is can be identified
      *                     when the {@link ITaskOrganizer#onTaskAppeared} callback is called.
+     * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+    public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie,
+            boolean removeWithTaskOrganizer) {
+        try {
+            mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie,
+                    removeWithTaskOrganizer);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Creates a persistent root task in WM for a particular windowing-mode.
+     * @param displayId The display to create the root task on.
+     * @param windowingMode Windowing mode to put the root task in.
+     * @param launchCookie Launch cookie to associate with the task so that is can be identified
+     *                     when the {@link ITaskOrganizer#onTaskAppeared} callback is called.
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
     @Nullable
     public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) {
-        try {
-            mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        createRootTask(displayId, windowingMode, launchCookie, false /* removeWithTaskOrganizer */);
     }
 
     /** Deletes a persistent root task in WM */
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index fda39c1..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;
 
@@ -229,19 +250,21 @@
         }
 
         @Override
-        public void onBackStarted(BackEvent backEvent) {
+        public void onBackStarted(BackMotionEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     mProgressAnimator.onBackStarted(backEvent, event ->
                             callback.onBackProgressed(event));
-                    callback.onBackStarted(backEvent);
+                    callback.onBackStarted(new BackEvent(
+                            backEvent.getTouchX(), backEvent.getTouchY(),
+                            backEvent.getProgress(), backEvent.getSwipeEdge()));
                 }
             });
         }
 
         @Override
-        public void onBackProgressed(BackEvent backEvent) {
+        public void onBackProgressed(BackMotionEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
@@ -253,11 +276,12 @@
         @Override
         public void onBackCancelled() {
             Handler.getMain().post(() -> {
-                mProgressAnimator.reset();
-                final OnBackAnimationCallback callback = getBackAnimationCallback();
-                if (callback != null) {
-                    callback.onBackCancelled();
-                }
+                mProgressAnimator.onBackCancelled(() -> {
+                    final OnBackAnimationCallback callback = getBackAnimationCallback();
+                    if (callback != null) {
+                        callback.onBackCancelled();
+                    }
+                });
             });
         }
 
diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java
index 2a80d02..740fbac 100644
--- a/core/java/android/window/WindowOrganizer.java
+++ b/core/java/android/window/WindowOrganizer.java
@@ -61,9 +61,7 @@
      * Apply multiple WindowContainer operations at once.
      *
      * Note that using this API requires the caller to hold
-     * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}, unless the caller is using
-     * {@link TaskFragmentOrganizer}, in which case it is allowed to change TaskFragment that is
-     * created by itself.
+     * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}.
      *
      * @param t The transaction to apply.
      * @param callback This transaction will use the synchronization scheme described in
@@ -72,8 +70,7 @@
      * @return An ID for the sync operation which will later be passed to transactionReady callback.
      *         This lets the caller differentiate overlapping sync operations.
      */
-    @RequiresPermission(value = android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
-            conditional = true)
+    @RequiresPermission(value = android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
     public int applySyncTransaction(@NonNull WindowContainerTransaction t,
             @NonNull WindowContainerTransactionCallback callback) {
         try {
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 43be031..1b901f5 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
 
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
+import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE;
 import static com.android.internal.util.ArrayUtils.convertToLongArray;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
@@ -147,11 +148,13 @@
                             Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
                             "1" /* Value to enable */, "0" /* Value to disable */,
                             R.string.color_correction_feature_name));
-            featuresMap.put(ONE_HANDED_COMPONENT_NAME,
-                    new ToggleableFrameworkFeatureInfo(
-                            Settings.Secure.ONE_HANDED_MODE_ACTIVATED,
-                            "1" /* Value to enable */, "0" /* Value to disable */,
-                            R.string.one_handed_mode_feature_name));
+            if (SUPPORT_ONE_HANDED_MODE) {
+                featuresMap.put(ONE_HANDED_COMPONENT_NAME,
+                        new ToggleableFrameworkFeatureInfo(
+                                Settings.Secure.ONE_HANDED_MODE_ACTIVATED,
+                                "1" /* Value to enable */, "0" /* Value to disable */,
+                                R.string.one_handed_mode_feature_name));
+            }
             featuresMap.put(REDUCE_BRIGHT_COLORS_COMPONENT_NAME,
                     new ToggleableFrameworkFeatureInfo(
                             Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index fc2c8cc..2d87745 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -25,6 +25,7 @@
 import static com.android.internal.accessibility.AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME;
 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
 import static com.android.internal.accessibility.util.ShortcutUtils.isShortcutContained;
+import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityShortcutInfo;
@@ -209,6 +210,7 @@
                         context.getString(R.string.accessibility_magnification_chooser_text),
                         context.getDrawable(R.drawable.ic_accessibility_magnification),
                         Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
+        targets.add(magnification);
 
         final ToggleAllowListingFeatureTarget daltonizer =
                 new ToggleAllowListingFeatureTarget(context,
@@ -219,6 +221,7 @@
                         context.getString(R.string.color_correction_feature_name),
                         context.getDrawable(R.drawable.ic_accessibility_color_correction),
                         Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED);
+        targets.add(daltonizer);
 
         final ToggleAllowListingFeatureTarget colorInversion =
                 new ToggleAllowListingFeatureTarget(context,
@@ -229,16 +232,20 @@
                         context.getString(R.string.color_inversion_feature_name),
                         context.getDrawable(R.drawable.ic_accessibility_color_inversion),
                         Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
+        targets.add(colorInversion);
 
-        final ToggleAllowListingFeatureTarget oneHandedMode =
-                new ToggleAllowListingFeatureTarget(context,
-                        shortcutType,
-                        isShortcutContained(context, shortcutType,
-                                ONE_HANDED_COMPONENT_NAME.flattenToString()),
-                        ONE_HANDED_COMPONENT_NAME.flattenToString(),
-                        context.getString(R.string.one_handed_mode_feature_name),
-                        context.getDrawable(R.drawable.ic_accessibility_one_handed),
-                        Settings.Secure.ONE_HANDED_MODE_ACTIVATED);
+        if (SUPPORT_ONE_HANDED_MODE) {
+            final ToggleAllowListingFeatureTarget oneHandedMode =
+                    new ToggleAllowListingFeatureTarget(context,
+                            shortcutType,
+                            isShortcutContained(context, shortcutType,
+                                    ONE_HANDED_COMPONENT_NAME.flattenToString()),
+                            ONE_HANDED_COMPONENT_NAME.flattenToString(),
+                            context.getString(R.string.one_handed_mode_feature_name),
+                            context.getDrawable(R.drawable.ic_accessibility_one_handed),
+                            Settings.Secure.ONE_HANDED_MODE_ACTIVATED);
+            targets.add(oneHandedMode);
+        }
 
         final ToggleAllowListingFeatureTarget reduceBrightColors =
                 new ToggleAllowListingFeatureTarget(context,
@@ -249,11 +256,6 @@
                         context.getString(R.string.reduce_bright_colors_feature_name),
                         context.getDrawable(R.drawable.ic_accessibility_reduce_bright_colors),
                         Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED);
-
-        targets.add(magnification);
-        targets.add(daltonizer);
-        targets.add(colorInversion);
-        targets.add(oneHandedMode);
         targets.add(reduceBrightColors);
 
         return targets;
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 1fcfe7d..011232f 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2953,12 +2953,24 @@
 
     private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
         return shouldShowTabs()
-                && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
-                UserHandle.of(UserHandle.myUserId())).getCount() > 0
+                && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+                        UserHandle.of(UserHandle.myUserId())).getCount() > 0
+                    || shouldShowContentPreviewWhenEmpty())
                 && shouldShowContentPreview();
     }
 
     /**
+     * This method could be used to override the default behavior when we hide the preview area
+     * when the current tab doesn't have any items.
+     *
+     * @return true if we want to show the content preview area even if the tab for the current
+     *         user is empty
+     */
+    protected boolean shouldShowContentPreviewWhenEmpty() {
+        return false;
+    }
+
+    /**
      * @return true if we want to show the content preview area
      */
     protected boolean shouldShowContentPreview() {
diff --git a/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java b/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java
index e3cc4f1..d0b5811 100644
--- a/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java
+++ b/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java
@@ -47,7 +47,9 @@
                 /* num_app_provided_app_targets = 6 */ appProvidedApp,
                 /* is_workprofile = 7 */ isWorkprofile,
                 /* previewType = 8 */ typeFromPreviewInt(previewType),
-                /* intentType = 9 */ typeFromIntentString(intent));
+                /* intentType = 9 */ typeFromIntentString(intent),
+                /* num_provided_custom_actions = 10 */ 0,
+                /* reselection_action_provided = 11 */ false);
     }
 
     @Override
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index f8b764b..19e4ba4 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -209,7 +209,7 @@
      * <p>Can only be used if there is a work profile.
      * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
      */
-    static final String EXTRA_SELECTED_PROFILE =
+    protected static final String EXTRA_SELECTED_PROFILE =
             "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
 
     /**
@@ -224,8 +224,8 @@
     static final String EXTRA_CALLING_USER =
             "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
 
-    static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
-    static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+    protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+    protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
 
     private BroadcastReceiver mWorkProfileStateReceiver;
     private UserHandle mHeaderCreatorUser;
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index b916878..3303c0e 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -562,9 +562,9 @@
             "task_manager_show_user_visible_jobs";
 
     /**
-     * (boolean) Whether the clipboard overlay is enabled.
+     * (boolean) Whether to show notification volume control slider separate from ring.
      */
-    public static final String CLIPBOARD_OVERLAY_ENABLED = "clipboard_overlay_enabled";
+    public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification";
 
     /**
      * (boolean) Whether widget provider info would be saved to / loaded from system persistence
@@ -573,13 +573,6 @@
     public static final String PERSISTS_WIDGET_PROVIDER_INFO = "persists_widget_provider_info";
 
     /**
-     * (boolean) Whether the clipboard overlay shows an edit button (as opposed to requiring tapping
-     * the preview to send an edit intent).
-     */
-    public static final String CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON =
-            "clipboard_overlay_show_edit_button";
-
-    /**
      * (boolean) Whether to show smart chips (based on TextClassifier) in the clipboard overlay.
      */
     public static final String CLIPBOARD_OVERLAY_SHOW_ACTIONS = "clipboard_overlay_show_actions";
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
new file mode 100644
index 0000000..c946db1
--- /dev/null
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -0,0 +1,188 @@
+/**
+ * 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.internal.config.sysui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Provides a central definition of debug SystemUI's SystemProperties flags, and their defaults.
+ *
+ * The main feature of this class is that it encodes a system-wide default for each flag which can
+ *  be updated by engineers with a single-line CL.
+ *
+ * NOTE: Because flag values returned by this class are not cached, it is important that developers
+ *  understand the intricacies of changing values and how that applies to their own code.
+ *  Generally, the best practice is to set the property, and then restart the device so that any
+ *  processes with stale state can be updated.  However, if your code has no state derived from the
+ *  flag value and queries it any time behavior is relevant, then it may be safe to change the flag
+ *  and not immediately reboot.
+ *
+ * To enable flags in debuggable builds, use the following commands:
+ *
+ * $ adb shell setprop persist.sysui.whatever_the_flag true
+ * $ adb reboot
+ *
+ * @hide
+ */
+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? */
+        boolean isEnabled(Flag flag);
+    }
+
+    /** The primary, immutable resolver returned by getResolver() */
+    private static final FlagResolver
+            MAIN_RESOLVER =
+            Build.IS_DEBUGGABLE ? new DebugResolver() : new ProdResolver();
+
+    /**
+     * On debuggable builds, this can be set to override the resolver returned by getResolver().
+     * This can be useful to override flags when testing components that do not allow injecting the
+     * SystemUiPropertiesFlags resolver they use.
+     * Always set this to null when tests tear down.
+     */
+    @VisibleForTesting
+    public static FlagResolver TEST_RESOLVER = null;
+
+    /** Get the resolver for this device configuration. */
+    public static FlagResolver getResolver() {
+        if (Build.IS_DEBUGGABLE && TEST_RESOLVER != null) {
+            Log.i("SystemUiSystemPropertiesFlags", "Returning debug resolver " + TEST_RESOLVER);
+            return TEST_RESOLVER;
+        }
+        return MAIN_RESOLVER;
+    }
+
+    /**
+     * Creates a flag that is enabled by default in debuggable builds.
+     * It can be enabled by setting this flag's SystemProperty to 1.
+     *
+     * This flag is ALWAYS disabled in release builds.
+     */
+    @VisibleForTesting
+    public static Flag devFlag(String name) {
+        return new Flag(name, false, null);
+    }
+
+    /**
+     * Creates a flag that is disabled by default in debuggable builds.
+     * It can be enabled or force-disabled by setting this flag's SystemProperty to 1 or 0.
+     * If this flag's SystemProperty is not set, the flag can be enabled by setting the
+     * TEAMFOOD flag's SystemProperty to 1.
+     *
+     * This flag is ALWAYS disabled in release builds.
+     */
+    @VisibleForTesting
+    public static Flag teamfoodFlag(String name) {
+        return new Flag(name, false, TEAMFOOD);
+    }
+
+    /**
+     * Creates a flag that is enabled by default in debuggable builds.
+     * It can be enabled by setting this flag's SystemProperty to 0.
+     *
+     * This flag is ALWAYS enabled in release builds.
+     */
+    @VisibleForTesting
+    public static Flag releasedFlag(String name) {
+        return new Flag(name, true, null);
+    }
+
+    /** Represents a developer-switchable gate for a feature. */
+    public static final class Flag {
+        public final String mSysPropKey;
+        public final boolean mDefaultValue;
+        @Nullable
+        public final Flag mDebugDefault;
+
+        /** constructs a new flag.  only visible for testing the class */
+        @VisibleForTesting
+        public Flag(@NonNull String sysPropKey, boolean defaultValue, @Nullable Flag debugDefault) {
+            mSysPropKey = sysPropKey;
+            mDefaultValue = defaultValue;
+            mDebugDefault = debugDefault;
+        }
+    }
+
+    /** Implementation of the interface used in release builds. */
+    @VisibleForTesting
+    public static final class ProdResolver implements
+            FlagResolver {
+        @Override
+        public boolean isEnabled(Flag flag) {
+            return flag.mDefaultValue;
+        }
+    }
+
+    /** Implementation of the interface used in debuggable builds. */
+    @VisibleForTesting
+    public static class DebugResolver implements FlagResolver {
+        @Override
+        public final boolean isEnabled(Flag flag) {
+            if (flag.mDebugDefault == null) {
+                return getBoolean(flag.mSysPropKey, flag.mDefaultValue);
+            }
+            return getBoolean(flag.mSysPropKey, isEnabled(flag.mDebugDefault));
+        }
+
+        /** Look up the value; overridable for tests to avoid needing to set SystemProperties */
+        @VisibleForTesting
+        public boolean getBoolean(String key, boolean defaultValue) {
+            return SystemProperties.getBoolean(key, defaultValue);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 05c6842..ae2fe4c 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -81,6 +81,7 @@
 import android.telephony.SignalStrength;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
@@ -201,6 +202,7 @@
     public static final int RESET_REASON_ADB_COMMAND = 2;
     public static final int RESET_REASON_FULL_CHARGE = 3;
     public static final int RESET_REASON_MEASURED_ENERGY_BUCKETS_CHANGE = 4;
+    public static final int RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION = 5;
 
     protected Clock mClock;
 
@@ -387,6 +389,89 @@
         }
     }
 
+    /** Provide BatteryStatsImpl configuration choices */
+    public static class BatteryStatsConfig {
+        static final int RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG = 1 << 0;
+        static final int RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG = 1 << 1;
+
+        private final int mFlags;
+
+        private BatteryStatsConfig(Builder builder) {
+            int flags = 0;
+            if (builder.mResetOnUnplugHighBatteryLevel) {
+                flags |= RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG;
+            }
+            if (builder.mResetOnUnplugAfterSignificantCharge) {
+                flags |= RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
+            }
+            mFlags = flags;
+        }
+
+        /**
+         * Returns whether a BatteryStats reset should occur on unplug when the battery level is
+         * high.
+         */
+        boolean shouldResetOnUnplugHighBatteryLevel() {
+            return (mFlags & RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG)
+                    == RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG;
+        }
+
+        /**
+         * Returns whether a BatteryStats reset should occur on unplug if the battery charge a
+         * significant amount since it has been plugged in.
+         */
+        boolean shouldResetOnUnplugAfterSignificantCharge() {
+            return (mFlags & RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG)
+                    == RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
+        }
+
+        /**
+         * Builder for BatteryStatsConfig
+         */
+        public static class Builder {
+            private boolean mResetOnUnplugHighBatteryLevel;
+            private boolean mResetOnUnplugAfterSignificantCharge;
+            public Builder() {
+                mResetOnUnplugHighBatteryLevel = true;
+                mResetOnUnplugAfterSignificantCharge = true;
+            }
+
+            /**
+             * Build the BatteryStatsConfig.
+             */
+            public BatteryStatsConfig build() {
+                return new BatteryStatsConfig(this);
+            }
+
+            /**
+             * Set whether a BatteryStats reset should occur on unplug when the battery level is
+             * high.
+             */
+            public Builder setResetOnUnplugHighBatteryLevel(boolean reset) {
+                mResetOnUnplugHighBatteryLevel = reset;
+                return this;
+            }
+
+            /**
+             * Set whether a BatteryStats reset should occur on unplug if the battery charge a
+             * significant amount since it has been plugged in.
+             */
+            public Builder setResetOnUnplugAfterSignificantCharge(boolean reset) {
+                mResetOnUnplugAfterSignificantCharge = reset;
+                return this;
+            }
+        }
+
+    }
+
+    /** Handles calls to AlarmManager */
+    public interface AlarmInterface {
+        /** Schedule an RTC alarm */
+        void schedule(long rtcTimeMs, long windowLengthMs);
+        /** Cancel the previously scheduled alarm */
+        void cancel();
+    }
+
     private final PlatformIdleStateCallback mPlatformIdleStateCallback;
 
     private final Runnable mDeferSetCharging = new Runnable() {
@@ -717,6 +802,7 @@
     protected boolean mHaveBatteryLevel = false;
     protected boolean mRecordingHistory = false;
     int mNumHistoryItems;
+    private long mBatteryPluggedInRealTimeMs = 0;
 
     private static final int HISTORY_TAG_INDEX_LIMIT = 0x7ffe;
     private static final int MAX_HISTORY_TAG_STRING_LENGTH = 1024;
@@ -1481,6 +1567,13 @@
     @GuardedBy("this")
     protected final Constants mConstants;
 
+    @VisibleForTesting
+    @GuardedBy("this")
+    protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
+
+    @VisibleForTesting
+    protected AlarmInterface mLongPlugInAlarmInterface = null;
+
     /*
      * Holds a SamplingTimer associated with each Resource Power Manager state and voter,
      * recording their times when on-battery (regardless of screen state).
@@ -1647,12 +1740,13 @@
     public BatteryStatsImpl(Clock clock, File historyDirectory) {
         init(clock);
         mStartClockTimeMs = clock.currentTimeMillis();
-        mCheckinFile = null;
         mDailyFile = null;
         if (historyDirectory == null) {
+            mCheckinFile = null;
             mStatsFile = null;
             mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer);
         } else {
+            mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
             mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
             mBatteryStatsHistory = new BatteryStatsHistory(this, historyDirectory, mHistoryBuffer);
         }
@@ -12582,6 +12676,27 @@
     }
 
     /**
+     * Injects BatteryStatsConfig
+     */
+    public void setBatteryStatsConfig(BatteryStatsConfig config) {
+        synchronized (this) {
+            mBatteryStatsConfig = config;
+        }
+    }
+
+    /**
+     * Injects a LongPlugInAlarmHandler
+     */
+    public void setLongPlugInAlarmInterface(AlarmInterface longPlugInAlarmInterface) {
+        synchronized (this) {
+            mLongPlugInAlarmInterface = longPlugInAlarmInterface;
+            if (!mOnBattery) {
+                scheduleNextResetWhilePluggedInCheck();
+            }
+        }
+    }
+
+    /**
      * Starts tracking CPU time-in-state for threads of the system server process,
      * keeping a separate account of threads receiving incoming binder calls.
      */
@@ -13053,12 +13168,12 @@
     }
 
     @GuardedBy("this")
-    public void resetAllStatsCmdLocked() {
+    public void resetAllStatsAndHistoryLocked(int reason) {
         final long mSecUptime = mClock.uptimeMillis();
         long uptimeUs = mSecUptime * 1000;
         long mSecRealtime = mClock.elapsedRealtime();
         long realtimeUs = mSecRealtime * 1000;
-        resetAllStatsLocked(mSecUptime, mSecRealtime, RESET_REASON_ADB_COMMAND);
+        resetAllStatsLocked(mSecUptime, mSecRealtime, reason);
         mDischargeStartLevel = mHistoryCur.batteryLevel;
         pullPendingStateUpdatesLocked();
         addHistoryRecordLocked(mSecRealtime, mSecUptime);
@@ -15532,6 +15647,73 @@
     }
 
     /**
+     * Might reset battery stats if conditions are met. Assumed the device is currently plugged in.
+     */
+    @GuardedBy("this")
+    public void maybeResetWhilePluggedInLocked() {
+        final long elapsedRealtimeMs = mClock.elapsedRealtime();
+        if (shouldResetWhilePluggedInLocked(elapsedRealtimeMs)) {
+            Slog.i(TAG,
+                    "Resetting due to long plug in duration. elapsed time = " + elapsedRealtimeMs
+                            + " ms, last plug in time = " + mBatteryPluggedInRealTimeMs
+                            + " ms, last reset time = " + mRealtimeStartUs / 1000);
+            resetAllStatsAndHistoryLocked(RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION);
+        }
+
+        scheduleNextResetWhilePluggedInCheck();
+    }
+
+    @GuardedBy("this")
+    private void scheduleNextResetWhilePluggedInCheck() {
+        if (mLongPlugInAlarmInterface != null) {
+            final long timeoutMs = mClock.currentTimeMillis()
+                    + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+                    * DateUtils.HOUR_IN_MILLIS;
+            Calendar nextAlarm = Calendar.getInstance();
+            nextAlarm.setTimeInMillis(timeoutMs);
+
+            // Find the 2 AM the same day as the end of the minimum duration.
+            // This logic does not handle a Daylight Savings transition, or a timezone change
+            // while the alarm has been set. The need to reset after a long period while plugged
+            // in is not strict enough to warrant a well architected out solution.
+            nextAlarm.set(Calendar.MILLISECOND, 0);
+            nextAlarm.set(Calendar.SECOND, 0);
+            nextAlarm.set(Calendar.MINUTE, 0);
+            nextAlarm.set(Calendar.HOUR_OF_DAY, 2);
+            long nextTimeMs = nextAlarm.getTimeInMillis();
+            if (nextTimeMs < timeoutMs) {
+                // The 2AM on the day of the timeout, move on the next day.
+                nextTimeMs += DateUtils.DAY_IN_MILLIS;
+            }
+            mLongPlugInAlarmInterface.schedule(nextTimeMs, DateUtils.HOUR_IN_MILLIS);
+        }
+    }
+
+
+    @GuardedBy("this")
+    private boolean shouldResetWhilePluggedInLocked(long elapsedRealtimeMs) {
+        if (mNoAutoReset) return false;
+        if (!mSystemReady) return false;
+
+        final long pluggedInThresholdMs = mBatteryPluggedInRealTimeMs
+                + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+                * DateUtils.HOUR_IN_MILLIS;
+        if (elapsedRealtimeMs >= pluggedInThresholdMs) {
+            // The device has been plugged in for a long time.
+            final long resetThresholdMs = mRealtimeStartUs / 1000
+                    + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+                    * DateUtils.HOUR_IN_MILLIS;
+            if (elapsedRealtimeMs >= resetThresholdMs) {
+                // And it has been a long time since the last reset.
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
      * Notifies BatteryStatsImpl that the system server is ready.
      */
     public void onSystemReady() {
@@ -15539,6 +15721,32 @@
     }
 
     @GuardedBy("this")
+    private boolean shouldResetOnUnplugLocked(int batteryStatus, int batteryLevel) {
+        if (mNoAutoReset) return false;
+        if (!mSystemReady) return false;
+        if (mBatteryStatsConfig.shouldResetOnUnplugHighBatteryLevel()) {
+            // Allow resetting due to currently being at high battery level
+            if (batteryStatus == BatteryManager.BATTERY_STATUS_FULL) return true;
+            if (batteryLevel >= 90) return true;
+        }
+        if (mBatteryStatsConfig.shouldResetOnUnplugAfterSignificantCharge()) {
+            // Allow resetting after a significant charge (from a very low level to a now very
+            // high level).
+            if (mDischargePlugLevel < 20 && batteryLevel >= 80) return true;
+        }
+        if (getHighDischargeAmountSinceCharge() >= 200) {
+            // Reset the stats if battery got partially charged and discharged repeatedly without
+            // ever reaching the full charge.
+            // This reset is done in order to prevent stats sessions from going on forever.
+            // Exceedingly long battery sessions would lead to an overflow of
+            // data structures such as mWakeupReasonStats.
+            return true;
+        }
+
+        return false;
+    }
+
+    @GuardedBy("this")
     protected void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime,
             final boolean onBattery, final int oldStatus, final int level, final int chargeUah) {
         boolean doWrite = false;
@@ -15550,23 +15758,10 @@
         final long realtimeUs = mSecRealtime * 1000;
         final int screenState = mScreenState;
         if (onBattery) {
-            // We will reset our status if we are unplugging after the
-            // battery was last full, or the level is at 100, or
-            // we have gone through a significant charge (from a very low
-            // level to a now very high level).
-            // Also, we will reset the stats if battery got partially charged
-            // and discharged repeatedly without ever reaching the full charge.
-            // This reset is done in order to prevent stats sessions from going on forever.
-            // Exceedingly long battery sessions would lead to an overflow of
-            // data structures such as mWakeupReasonStats.
             boolean reset = false;
-            if (!mNoAutoReset && mSystemReady
-                    && (oldStatus == BatteryManager.BATTERY_STATUS_FULL
-                    || level >= 90
-                    || (mDischargeCurrentLevel < 20 && level >= 80)
-                    || getHighDischargeAmountSinceCharge() >= 200)) {
+            if (shouldResetOnUnplugLocked(oldStatus, level)) {
                 Slog.i(TAG, "Resetting battery stats: level=" + level + " status=" + oldStatus
-                        + " dischargeLevel=" + mDischargeCurrentLevel
+                        + " dischargeLevel=" + mDischargePlugLevel
                         + " lowAmount=" + getLowDischargeAmountSinceCharge()
                         + " highAmount=" + getHighDischargeAmountSinceCharge());
                 // Before we write, collect a snapshot of the final aggregated
@@ -15623,6 +15818,9 @@
             mInitStepMode = mCurStepMode;
             mModStepMode = 0;
             pullPendingStateUpdatesLocked();
+            if (mLongPlugInAlarmInterface != null) {
+                mLongPlugInAlarmInterface.cancel();
+            }
             mHistoryCur.batteryLevel = (byte)level;
             mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
             if (DEBUG_HISTORY) Slog.v(TAG, "Battery unplugged to: "
@@ -15654,6 +15852,7 @@
             mLastChargingStateLevel = level;
             mOnBattery = mOnBatteryInternal = false;
             pullPendingStateUpdatesLocked();
+            mBatteryPluggedInRealTimeMs = mSecRealtime;
             mHistoryCur.batteryLevel = (byte)level;
             mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
             if (DEBUG_HISTORY) Slog.v(TAG, "Battery plugged to: "
@@ -15671,6 +15870,7 @@
             mMaxChargeStepLevel = level;
             mInitStepMode = mCurStepMode;
             mModStepMode = 0;
+            scheduleNextResetWhilePluggedInCheck();
         }
         if (doWrite || (mLastWriteTimeMs + (60 * 1000)) < mSecRealtime) {
             if (mStatsFile != null && mBatteryStatsHistory.getActiveFile() != null) {
@@ -16685,6 +16885,8 @@
         public static final String KEY_MAX_HISTORY_BUFFER_KB = "max_history_buffer_kb";
         public static final String KEY_BATTERY_CHARGED_DELAY_MS =
                 "battery_charged_delay_ms";
+        public static final String KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS =
+                "reset_while_plugged_in_minimum_duration_hours";
 
         private static final boolean DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME = true;
         private static final long DEFAULT_KERNEL_UID_READERS_THROTTLE_TIME = 1_000;
@@ -16697,6 +16899,8 @@
         private static final int DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE = 64;
         private static final int DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB = 64; /*Kilo Bytes*/
         private static final int DEFAULT_BATTERY_CHARGED_DELAY_MS = 900000; /* 15 min */
+        // Little less than 2 days
+        private static final int DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = 47;
 
         public boolean TRACK_CPU_ACTIVE_CLUSTER_TIME = DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME;
         /* Do not set default value for KERNEL_UID_READERS_THROTTLE_TIME. Need to trigger an
@@ -16712,6 +16916,8 @@
         public int MAX_HISTORY_FILES;
         public int MAX_HISTORY_BUFFER; /*Bytes*/
         public int BATTERY_CHARGED_DELAY_MS = DEFAULT_BATTERY_CHARGED_DELAY_MS;
+        public int RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS =
+                DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS;
 
         private ContentResolver mResolver;
         private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -16788,6 +16994,11 @@
                                 DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB
                                 : DEFAULT_MAX_HISTORY_BUFFER_KB)
                         * 1024;
+
+                RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = mParser.getInt(
+                        KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS,
+                        DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS);
+
                 updateBatteryChargedDelayMsLocked();
             }
         }
@@ -16842,6 +17053,8 @@
             pw.println(MAX_HISTORY_BUFFER/1024);
             pw.print(KEY_BATTERY_CHARGED_DELAY_MS); pw.print("=");
             pw.println(BATTERY_CHARGED_DELAY_MS);
+            pw.print(KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); pw.print("=");
+            pw.println(RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS);
         }
     }
 
diff --git a/core/java/com/android/internal/os/BatteryUsageStatsProvider.java b/core/java/com/android/internal/os/BatteryUsageStatsProvider.java
index 09e409b..ac4976f 100644
--- a/core/java/com/android/internal/os/BatteryUsageStatsProvider.java
+++ b/core/java/com/android/internal/os/BatteryUsageStatsProvider.java
@@ -298,18 +298,16 @@
                 BatteryStats.Uid.PROCESS_STATE_FOREGROUND, realtimeUs,
                 BatteryStats.STATS_SINCE_CHARGED);
 
-        totalForegroundDurationUs += uid.getProcessStateTime(
-                BatteryStats.Uid.PROCESS_STATE_FOREGROUND_SERVICE, realtimeUs,
-                BatteryStats.STATS_SINCE_CHARGED);
-
         return totalForegroundDurationUs / 1000;
     }
 
     private long getProcessBackgroundTimeMs(BatteryStats.Uid uid, long realtimeUs) {
-        return uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_BACKGROUND, realtimeUs,
-                BatteryStats.STATS_SINCE_CHARGED) / 1000;
+        return (uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_BACKGROUND,
+                realtimeUs, BatteryStats.STATS_SINCE_CHARGED)
+                + uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_FOREGROUND_SERVICE,
+                realtimeUs, BatteryStats.STATS_SINCE_CHARGED))
+                / 1000;
     }
-
     private BatteryUsageStats getAggregatedBatteryUsageStats(BatteryUsageStatsQuery query) {
         final boolean includePowerModels = (query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
diff --git a/core/java/com/android/internal/os/RoSystemProperties.java b/core/java/com/android/internal/os/RoSystemProperties.java
index 98d81c9..cccd80e 100644
--- a/core/java/com/android/internal/os/RoSystemProperties.java
+++ b/core/java/com/android/internal/os/RoSystemProperties.java
@@ -31,6 +31,8 @@
             SystemProperties.getInt("ro.factorytest", 0);
     public static final String CONTROL_PRIVAPP_PERMISSIONS =
             SystemProperties.get("ro.control_privapp_permissions");
+    public static final boolean SUPPORT_ONE_HANDED_MODE =
+            SystemProperties.getBoolean("ro.support_one_handed_mode", /* def= */ false);
 
     // ------ ro.hdmi.* -------- //
     /**
diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
index 205c5fd..f1ed3be 100644
--- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
+++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
@@ -56,6 +56,9 @@
         }
     };
 
+    /**
+     * Registers the observer for all users.
+     */
     public void register() {
         ContentResolver r = mContext.getContentResolver();
         r.registerContentObserver(
@@ -73,6 +76,26 @@
                 mOnPropertiesChangedListener);
     }
 
+    /**
+     * Registers the observer for the calling user.
+     */
+    public void registerForCallingUser() {
+        ContentResolver r = mContext.getContentResolver();
+        r.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT),
+                false, this);
+        r.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT),
+                false, this);
+        r.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
+                false, this);
+        DeviceConfig.addOnPropertiesChangedListener(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                runnable -> mMainHandler.post(runnable),
+                mOnPropertiesChangedListener);
+    }
+
     public void unregister() {
         mContext.getContentResolver().unregisterContentObserver(this);
         DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
@@ -86,12 +109,46 @@
         }
     }
 
+    /**
+     * Returns the left sensitivity for the current user.  To be used in code that runs primarily
+     * in one user's process.
+     */
     public int getLeftSensitivity(Resources userRes) {
-        return getSensitivity(userRes, Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT);
+        final float scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
+                Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, 1.0f, UserHandle.USER_CURRENT);
+        return (int) (getUnscaledInset(userRes) * scale);
     }
 
+    /**
+     * Returns the left sensitivity for the calling user.  To be used in code that runs in a
+     * per-user process.
+     */
+    @SuppressWarnings("NonUserGetterCalled")
+    public int getLeftSensitivityForCallingUser(Resources userRes) {
+        final float scale = Settings.Secure.getFloat(mContext.getContentResolver(),
+                Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, 1.0f);
+        return (int) (getUnscaledInset(userRes) * scale);
+    }
+
+    /**
+     * Returns the right sensitivity for the current user.  To be used in code that runs primarily
+     * in one user's process.
+     */
     public int getRightSensitivity(Resources userRes) {
-        return getSensitivity(userRes, Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT);
+        final float scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
+                Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, 1.0f, UserHandle.USER_CURRENT);
+        return (int) (getUnscaledInset(userRes) * scale);
+    }
+
+    /**
+     * Returns the right sensitivity for the calling user.  To be used in code that runs in a
+     * per-user process.
+     */
+    @SuppressWarnings("NonUserGetterCalled")
+    public int getRightSensitivityForCallingUser(Resources userRes) {
+        final float scale = Settings.Secure.getFloat(mContext.getContentResolver(),
+                Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, 1.0f);
+        return (int) (getUnscaledInset(userRes) * scale);
     }
 
     public boolean areNavigationButtonForcedVisible() {
@@ -99,7 +156,7 @@
                 Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0;
     }
 
-    private int getSensitivity(Resources userRes, String side) {
+    private float getUnscaledInset(Resources userRes) {
         final DisplayMetrics dm = userRes.getDisplayMetrics();
         final float defaultInset = userRes.getDimension(
                 com.android.internal.R.dimen.config_backGestureInset) / dm.density;
@@ -110,8 +167,6 @@
                 : defaultInset;
         final float inset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, backGestureInset,
                 dm);
-        final float scale = Settings.Secure.getFloatForUser(
-                mContext.getContentResolver(), side, 1.0f, UserHandle.USER_CURRENT);
-        return (int) (inset * scale);
+        return inset;
     }
 }
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/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 017bf3f..04fc4a6 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -338,4 +338,11 @@
      * @param leftOrTop indicates where the stage split is.
      */
     void enterStageSplitFromRunningApp(boolean leftOrTop);
+
+    /**
+     * Shows the media output switcher dialog.
+     *
+     * @param packageName of the session for which the output switcher is shown.
+     */
+    void showMediaOutputSwitcher(String packageName);
 }
diff --git a/core/java/com/android/internal/util/ObservableServiceConnection.java b/core/java/com/android/internal/util/ObservableServiceConnection.java
index 3165d29..45256fd 100644
--- a/core/java/com/android/internal/util/ObservableServiceConnection.java
+++ b/core/java/com/android/internal/util/ObservableServiceConnection.java
@@ -165,6 +165,13 @@
     }
 
     /**
+     * Executes code on the executor specified at construction.
+     */
+    public void execute(Runnable runnable) {
+        mExecutor.execute(runnable);
+    }
+
+    /**
      * Initiate binding to the service.
      *
      * @return {@code true} if initiating binding succeed, {@code false} if the binding failed or
diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java
index 79c5196..3a393b6 100644
--- a/core/java/com/android/internal/util/ScreenshotHelper.java
+++ b/core/java/com/android/internal/util/ScreenshotHelper.java
@@ -1,7 +1,7 @@
 package com.android.internal.util;
 
 import static android.content.Intent.ACTION_USER_SWITCHED;
-import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -11,29 +11,18 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
-import android.graphics.Bitmap;
-import android.graphics.ColorSpace;
-import android.graphics.Insets;
-import android.graphics.ParcelableColorSpace;
-import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.WindowManager.ScreenshotSource;
-import android.view.WindowManager.ScreenshotType;
 
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.Objects;
 import java.util.function.Consumer;
 
 public class ScreenshotHelper {
@@ -41,212 +30,6 @@
     public static final int SCREENSHOT_MSG_URI = 1;
     public static final int SCREENSHOT_MSG_PROCESS_COMPLETE = 2;
 
-    /**
-     * Describes a screenshot request.
-     */
-    public static class ScreenshotRequest implements Parcelable {
-        @ScreenshotType
-        private final int mType;
-
-        @ScreenshotSource
-        private final int mSource;
-
-        private final Bundle mBitmapBundle;
-        private final Rect mBoundsInScreen;
-        private final Insets mInsets;
-        private final int mTaskId;
-        private final int mUserId;
-        private final ComponentName mTopComponent;
-
-
-        public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source) {
-            this(type, source, /* topComponent */ null);
-        }
-
-        public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source,
-                ComponentName topComponent) {
-            this(type,
-                source,
-                /* bitmapBundle*/ null,
-                /* boundsInScreen */ null,
-                /* insets */ null,
-                /* taskId */ -1,
-                /* userId */ -1,
-                topComponent);
-        }
-
-        public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source,
-                Bundle bitmapBundle, Rect boundsInScreen, Insets insets, int taskId, int userId,
-                ComponentName topComponent) {
-            mType = type;
-            mSource = source;
-            mBitmapBundle = bitmapBundle;
-            mBoundsInScreen = boundsInScreen;
-            mInsets = insets;
-            mTaskId = taskId;
-            mUserId = userId;
-            mTopComponent = topComponent;
-        }
-
-        ScreenshotRequest(Parcel in) {
-            mType = in.readInt();
-            mSource = in.readInt();
-            if (in.readInt() == 1) {
-                mBitmapBundle = in.readBundle(getClass().getClassLoader());
-                mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), Rect.class);
-                mInsets = in.readParcelable(Insets.class.getClassLoader(), Insets.class);
-                mTaskId = in.readInt();
-                mUserId = in.readInt();
-                mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(),
-                        ComponentName.class);
-            } else {
-                mBitmapBundle = null;
-                mBoundsInScreen = null;
-                mInsets = null;
-                mTaskId = -1;
-                mUserId = -1;
-                mTopComponent = null;
-            }
-        }
-
-        @ScreenshotType
-        public int getType() {
-            return mType;
-        }
-
-        @ScreenshotSource
-        public int getSource() {
-            return mSource;
-        }
-
-        public Bundle getBitmapBundle() {
-            return mBitmapBundle;
-        }
-
-        public Rect getBoundsInScreen() {
-            return mBoundsInScreen;
-        }
-
-        public Insets getInsets() {
-            return mInsets;
-        }
-
-        public int getTaskId() {
-            return mTaskId;
-        }
-
-        public int getUserId() {
-            return mUserId;
-        }
-
-        public ComponentName getTopComponent() {
-            return mTopComponent;
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel dest, int flags) {
-            dest.writeInt(mType);
-            dest.writeInt(mSource);
-            if (mBitmapBundle == null) {
-                dest.writeInt(0);
-            } else {
-                dest.writeInt(1);
-                dest.writeBundle(mBitmapBundle);
-                dest.writeParcelable(mBoundsInScreen, 0);
-                dest.writeParcelable(mInsets, 0);
-                dest.writeInt(mTaskId);
-                dest.writeInt(mUserId);
-                dest.writeParcelable(mTopComponent, 0);
-            }
-        }
-
-        @NonNull
-        public static final Parcelable.Creator<ScreenshotRequest> CREATOR =
-                new Parcelable.Creator<ScreenshotRequest>() {
-
-                    @Override
-                    public ScreenshotRequest createFromParcel(Parcel source) {
-                        return new ScreenshotRequest(source);
-                    }
-
-                    @Override
-                    public ScreenshotRequest[] newArray(int size) {
-                        return new ScreenshotRequest[size];
-                    }
-                };
-    }
-
-    /**
-     * Bundler used to convert between a hardware bitmap and a bundle without copying the internal
-     * content. This is expected to be used together with {@link #provideScreenshot} to handle a
-     * hardware bitmap as a screenshot.
-     */
-    public static final class HardwareBitmapBundler {
-        private static final String KEY_BUFFER = "bitmap_util_buffer";
-        private static final String KEY_COLOR_SPACE = "bitmap_util_color_space";
-
-        private HardwareBitmapBundler() {
-        }
-
-        /**
-         * Creates a Bundle that represents the given Bitmap.
-         * <p>The Bundle will contain a wrapped version of the Bitmaps HardwareBuffer, so will avoid
-         * copies when passing across processes, only pass to processes you trust.
-         *
-         * <p>Returns a new Bundle rather than modifying an exiting one to avoid key collisions, the
-         * returned Bundle should be treated as a standalone object.
-         *
-         * @param bitmap to convert to bundle
-         * @return a Bundle representing the bitmap, should only be parsed by
-         * {@link #bundleToHardwareBitmap(Bundle)}
-         */
-        public static Bundle hardwareBitmapToBundle(Bitmap bitmap) {
-            if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
-                throw new IllegalArgumentException(
-                        "Passed bitmap must have hardware config, found: " + bitmap.getConfig());
-            }
-
-            // Bitmap assumes SRGB for null color space
-            ParcelableColorSpace colorSpace =
-                    bitmap.getColorSpace() == null
-                            ? new ParcelableColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
-                            : new ParcelableColorSpace(bitmap.getColorSpace());
-
-            Bundle bundle = new Bundle();
-            bundle.putParcelable(KEY_BUFFER, bitmap.getHardwareBuffer());
-            bundle.putParcelable(KEY_COLOR_SPACE, colorSpace);
-
-            return bundle;
-        }
-
-        /**
-         * Extracts the Bitmap added to a Bundle with {@link #hardwareBitmapToBundle(Bitmap)} .}
-         *
-         * <p>This Bitmap contains the HardwareBuffer from the original caller, be careful passing
-         * this Bitmap on to any other source.
-         *
-         * @param bundle containing the bitmap
-         * @return a hardware Bitmap
-         */
-        public static Bitmap bundleToHardwareBitmap(Bundle bundle) {
-            if (!bundle.containsKey(KEY_BUFFER) || !bundle.containsKey(KEY_COLOR_SPACE)) {
-                throw new IllegalArgumentException("Bundle does not contain a hardware bitmap");
-            }
-
-            HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER, HardwareBuffer.class);
-            ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE,
-                    ParcelableColorSpace.class);
-
-            return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),
-                    colorSpace.getColorSpace());
-        }
-    }
-
     private static final String TAG = "ScreenshotHelper";
 
     // Time until we give up on the screenshot & show an error instead.
@@ -277,20 +60,35 @@
     /**
      * Request a screenshot be taken.
      * <p>
-     * Added to support reducing unit test duration; the method variant without a timeout argument
-     * is recommended for general use.
+     * Convenience method for taking a full screenshot with provided source.
      *
-     * @param type The type of screenshot, defined by {@link ScreenshotType}
-     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
-     * @param handler used to process messages received from the screenshot service
+     * @param source             source of the screenshot request, defined by {@link
+     *                           ScreenshotSource}
+     * @param handler            used to process messages received from the screenshot service
      * @param completionConsumer receives the URI of the captured screenshot, once saved or
-     *         null if no screenshot was saved
+     *                           null if no screenshot was saved
      */
-    public void takeScreenshot(@ScreenshotType int type, @ScreenshotSource int source,
-            @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
-        ScreenshotRequest screenshotRequest = new ScreenshotRequest(type, source);
-        takeScreenshot(handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS,
-                completionConsumer);
+    public void takeScreenshot(@ScreenshotSource int source, @NonNull Handler handler,
+            @Nullable Consumer<Uri> completionConsumer) {
+        ScreenshotRequest request =
+                new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, source).build();
+        takeScreenshot(request, handler, completionConsumer);
+    }
+
+    /**
+     * Request a screenshot be taken.
+     * <p>
+     *
+     * @param request            description of the screenshot request, either for taking a
+     *                           screenshot or
+     *                           providing a bitmap
+     * @param handler            used to process messages received from the screenshot service
+     * @param completionConsumer receives the URI of the captured screenshot, once saved or
+     *                           null if no screenshot was saved
+     */
+    public void takeScreenshot(ScreenshotRequest request, @NonNull Handler handler,
+            @Nullable Consumer<Uri> completionConsumer) {
+        takeScreenshotInternal(request, handler, completionConsumer, SCREENSHOT_TIMEOUT_MS);
     }
 
     /**
@@ -299,46 +97,16 @@
      * Added to support reducing unit test duration; the method variant without a timeout argument
      * is recommended for general use.
      *
-     * @param type The type of screenshot, defined by {@link ScreenshotType}
-     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
-     * @param handler used to process messages received from the screenshot service
-     * @param timeoutMs time limit for processing, intended only for testing
+     * @param request            description of the screenshot request, either for taking a
+     *                           screenshot or providing a bitmap
+     * @param handler            used to process messages received from the screenshot service
+     * @param timeoutMs          time limit for processing, intended only for testing
      * @param completionConsumer receives the URI of the captured screenshot, once saved or
-     *         null if no screenshot was saved
+     *                           null if no screenshot was saved
      */
     @VisibleForTesting
-    public void takeScreenshot(@ScreenshotType int type, @ScreenshotSource int source,
-            @NonNull Handler handler, long timeoutMs, @Nullable Consumer<Uri> completionConsumer) {
-        ScreenshotRequest screenshotRequest = new ScreenshotRequest(type, source);
-        takeScreenshot(handler, screenshotRequest, timeoutMs, completionConsumer);
-    }
-
-    /**
-     * Request that provided image be handled as if it was a screenshot.
-     *
-     * @param screenshotBundle Bundle containing the buffer and color space of the screenshot.
-     * @param boundsInScreen The bounds in screen coordinates that the bitmap originated from.
-     * @param insets The insets that the image was shown with, inside the screen bounds.
-     * @param taskId The taskId of the task that the screen shot was taken of.
-     * @param userId The userId of user running the task provided in taskId.
-     * @param topComponent The component name of the top component running in the task.
-     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
-     * @param handler A handler used in case the screenshot times out
-     * @param completionConsumer receives the URI of the captured screenshot, once saved or
-     *         null if no screenshot was saved
-     */
-    public void provideScreenshot(@NonNull Bundle screenshotBundle, @NonNull Rect boundsInScreen,
-            @NonNull Insets insets, int taskId, int userId, ComponentName topComponent,
-            @ScreenshotSource int source, @NonNull Handler handler,
-            @Nullable Consumer<Uri> completionConsumer) {
-        ScreenshotRequest screenshotRequest = new ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE,
-                source, screenshotBundle, boundsInScreen, insets, taskId, userId, topComponent);
-        takeScreenshot(handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS, completionConsumer);
-    }
-
-    private void takeScreenshot(@NonNull Handler handler,
-            ScreenshotRequest screenshotRequest, long timeoutMs,
-            @Nullable Consumer<Uri> completionConsumer) {
+    public void takeScreenshotInternal(ScreenshotRequest request, @NonNull Handler handler,
+            @Nullable Consumer<Uri> completionConsumer, long timeoutMs) {
         synchronized (mScreenshotLock) {
 
             final Runnable mScreenshotTimeout = () -> {
@@ -354,7 +122,7 @@
                 }
             };
 
-            Message msg = Message.obtain(null, 0, screenshotRequest);
+            Message msg = Message.obtain(null, 0, request);
 
             Handler h = new Handler(handler.getLooper()) {
                 @Override
@@ -471,5 +239,4 @@
                 Intent.FLAG_RECEIVER_FOREGROUND);
         mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
     }
-
 }
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.aidl b/core/java/com/android/internal/util/ScreenshotRequest.aidl
new file mode 100644
index 0000000..b08905d
--- /dev/null
+++ b/core/java/com/android/internal/util/ScreenshotRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.internal.util;
+
+parcelable ScreenshotRequest;
\ No newline at end of file
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
new file mode 100644
index 0000000..c8b7def
--- /dev/null
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.internal.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.UserHandle.USER_NULL;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.Insets;
+import android.graphics.ParcelableColorSpace;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.WindowManager;
+
+import java.util.Objects;
+
+/**
+ * Describes a screenshot request.
+ */
+public class ScreenshotRequest implements Parcelable {
+    private static final String TAG = "ScreenshotRequest";
+
+    @WindowManager.ScreenshotType
+    private final int mType;
+    @WindowManager.ScreenshotSource
+    private final int mSource;
+    private final ComponentName mTopComponent;
+    private final int mTaskId;
+    private final int mUserId;
+    private final Bitmap mBitmap;
+    private final Rect mBoundsInScreen;
+    private final Insets mInsets;
+
+    private ScreenshotRequest(
+            @WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
+            ComponentName topComponent, int taskId, int userId,
+            Bitmap bitmap, Rect boundsInScreen, Insets insets) {
+        mType = type;
+        mSource = source;
+        mTopComponent = topComponent;
+        mTaskId = taskId;
+        mUserId = userId;
+        mBitmap = bitmap;
+        mBoundsInScreen = boundsInScreen;
+        mInsets = insets;
+    }
+
+    ScreenshotRequest(Parcel in) {
+        mType = in.readInt();
+        mSource = in.readInt();
+        mTopComponent = in.readTypedObject(ComponentName.CREATOR);
+        mTaskId = in.readInt();
+        mUserId = in.readInt();
+        mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
+        mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
+        mInsets = in.readTypedObject(Insets.CREATOR);
+    }
+
+    @WindowManager.ScreenshotType
+    public int getType() {
+        return mType;
+    }
+
+    @WindowManager.ScreenshotSource
+    public int getSource() {
+        return mSource;
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    public Rect getBoundsInScreen() {
+        return mBoundsInScreen;
+    }
+
+    public Insets getInsets() {
+        return mInsets;
+    }
+
+    public int getTaskId() {
+        return mTaskId;
+    }
+
+    public int getUserId() {
+        return mUserId;
+    }
+
+    public ComponentName getTopComponent() {
+        return mTopComponent;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mType);
+        dest.writeInt(mSource);
+        dest.writeTypedObject(mTopComponent, 0);
+        dest.writeInt(mTaskId);
+        dest.writeInt(mUserId);
+        dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
+        dest.writeTypedObject(mBoundsInScreen, 0);
+        dest.writeTypedObject(mInsets, 0);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<ScreenshotRequest> CREATOR =
+            new Parcelable.Creator<ScreenshotRequest>() {
+
+                @Override
+                public ScreenshotRequest createFromParcel(Parcel source) {
+                    return new ScreenshotRequest(source);
+                }
+
+                @Override
+                public ScreenshotRequest[] newArray(int size) {
+                    return new ScreenshotRequest[size];
+                }
+            };
+
+    /**
+     * Builder class for {@link ScreenshotRequest} objects.
+     */
+    public static class Builder {
+        @WindowManager.ScreenshotType
+        private final int mType;
+
+        @WindowManager.ScreenshotSource
+        private final int mSource;
+
+        private Bitmap mBitmap;
+        private Rect mBoundsInScreen;
+        private Insets mInsets = Insets.NONE;
+        private int mTaskId = INVALID_TASK_ID;
+        private int mUserId = USER_NULL;
+        private ComponentName mTopComponent;
+
+        /**
+         * Begin building a ScreenshotRequest.
+         *
+         * @param type   The type of the screenshot request, defined by {@link
+         *               WindowManager.ScreenshotType}
+         * @param source The source of the screenshot request, defined by {@link
+         *               WindowManager.ScreenshotSource}
+         */
+        public Builder(
+                @WindowManager.ScreenshotType int type,
+                @WindowManager.ScreenshotSource int source) {
+            if (type != TAKE_SCREENSHOT_FULLSCREEN && type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+                throw new IllegalArgumentException("Invalid screenshot type requested!");
+            }
+            mType = type;
+            mSource = source;
+        }
+
+        /**
+         * Construct a new {@link ScreenshotRequest} with the set parameters.
+         */
+        public ScreenshotRequest build() {
+            if (mType == TAKE_SCREENSHOT_FULLSCREEN && mBitmap != null) {
+                Log.w(TAG, "Bitmap provided, but request is fullscreen. Bitmap will be ignored.");
+            }
+            if (mType == TAKE_SCREENSHOT_PROVIDED_IMAGE && mBitmap == null) {
+                throw new IllegalStateException(
+                        "Request is PROVIDED_IMAGE, but no bitmap is provided!");
+            }
+
+            return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
+                    mBoundsInScreen, mInsets);
+        }
+
+        /**
+         * Set the top component associated with this request.
+         *
+         * @param topComponent The component name of the top component running in the task.
+         */
+        public Builder setTopComponent(ComponentName topComponent) {
+            mTopComponent = topComponent;
+            return this;
+        }
+
+        /**
+         * Set the task id associated with this request.
+         *
+         * @param taskId The taskId of the task that the screenshot was taken of.
+         */
+        public Builder setTaskId(int taskId) {
+            mTaskId = taskId;
+            return this;
+        }
+
+        /**
+         * Set the user id associated with this request.
+         *
+         * @param userId The userId of user running the task provided in taskId.
+         */
+        public Builder setUserId(int userId) {
+            mUserId = userId;
+            return this;
+        }
+
+        /**
+         * Set the bitmap associated with this request.
+         *
+         * @param bitmap The provided screenshot.
+         */
+        public Builder setBitmap(Bitmap bitmap) {
+            mBitmap = bitmap;
+            return this;
+        }
+
+        /**
+         * Set the bounds for the provided bitmap.
+         *
+         * @param bounds The bounds in screen coordinates that the bitmap originated from.
+         */
+        public Builder setBoundsOnScreen(Rect bounds) {
+            mBoundsInScreen = bounds;
+            return this;
+        }
+
+        /**
+         * Set the insets for the provided bitmap.
+         *
+         * @param insets The insets that the image was shown with, inside the screen bounds.
+         */
+        public Builder setInsets(@NonNull Insets insets) {
+            mInsets = insets;
+            return this;
+        }
+    }
+
+    /**
+     * Bundler used to convert between a hardware bitmap and a bundle without copying the internal
+     * content. This is used together with a fully-defined ScreenshotRequest to handle a hardware
+     * bitmap as a screenshot.
+     */
+    private static final class HardwareBitmapBundler {
+        private static final String KEY_BUFFER = "bitmap_util_buffer";
+        private static final String KEY_COLOR_SPACE = "bitmap_util_color_space";
+
+        private HardwareBitmapBundler() {
+        }
+
+        /**
+         * Creates a Bundle that represents the given Bitmap.
+         * <p>The Bundle will contain a wrapped version of the Bitmaps HardwareBuffer, so will
+         * avoid
+         * copies when passing across processes, only pass to processes you trust.
+         *
+         * <p>Returns a new Bundle rather than modifying an exiting one to avoid key collisions,
+         * the
+         * returned Bundle should be treated as a standalone object.
+         *
+         * @param bitmap to convert to bundle
+         * @return a Bundle representing the bitmap, should only be parsed by
+         * {@link #bundleToHardwareBitmap(Bundle)}
+         */
+        private static Bundle hardwareBitmapToBundle(Bitmap bitmap) {
+            if (bitmap == null) {
+                return null;
+            }
+            if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
+                throw new IllegalArgumentException(
+                        "Passed bitmap must have hardware config, found: "
+                                + bitmap.getConfig());
+            }
+
+            // Bitmap assumes SRGB for null color space
+            ParcelableColorSpace colorSpace =
+                    bitmap.getColorSpace() == null
+                            ? new ParcelableColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
+                            : new ParcelableColorSpace(bitmap.getColorSpace());
+
+            Bundle bundle = new Bundle();
+            bundle.putParcelable(KEY_BUFFER, bitmap.getHardwareBuffer());
+            bundle.putParcelable(KEY_COLOR_SPACE, colorSpace);
+
+            return bundle;
+        }
+
+        /**
+         * Extracts the Bitmap added to a Bundle with {@link #hardwareBitmapToBundle(Bitmap)}.
+         *
+         * <p>This Bitmap contains the HardwareBuffer from the original caller, be careful
+         * passing
+         * this Bitmap on to any other source.
+         *
+         * @param bundle containing the bitmap
+         * @return a hardware Bitmap
+         */
+        private static Bitmap bundleToHardwareBitmap(Bundle bundle) {
+            if (bundle == null) {
+                return null;
+            }
+            if (!bundle.containsKey(KEY_BUFFER) || !bundle.containsKey(KEY_COLOR_SPACE)) {
+                throw new IllegalArgumentException("Bundle does not contain a hardware bitmap");
+            }
+
+            HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER, HardwareBuffer.class);
+            ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE,
+                    ParcelableColorSpace.class);
+
+            return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),
+                    colorSpace.getColorSpace());
+        }
+    }
+}
diff --git a/core/java/com/android/internal/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java
index 869da1f..058c6ec 100644
--- a/core/java/com/android/internal/view/RotationPolicy.java
+++ b/core/java/com/android/internal/view/RotationPolicy.java
@@ -106,7 +106,9 @@
      * Enables or disables rotation lock from the system UI toggle.
      */
     public static void setRotationLock(Context context, final boolean enabled) {
-        final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION;
+        final int rotation = areAllRotationsAllowed(context)
+                || useCurrentRotationOnRotationLockChange(context) ? CURRENT_ROTATION
+                : NATURAL_ROTATION;
         setRotationLockAtAngle(context, enabled, rotation);
     }
 
@@ -139,6 +141,11 @@
         return context.getResources().getBoolean(R.bool.config_allowAllRotations);
     }
 
+    private static boolean useCurrentRotationOnRotationLockChange(Context context) {
+        return context.getResources().getBoolean(
+                R.bool.config_useCurrentRotationOnRotationLockChange);
+    }
+
     private static void setRotationLock(final boolean enabled, final int rotation) {
         AsyncTask.execute(new Runnable() {
             @Override
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index b1610d7..8952f37 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -428,7 +428,7 @@
         jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
         return 0;
     } else if (err != NO_ERROR) {
-        jniThrowException(env, OutOfResourcesException, NULL);
+        jniThrowException(env, OutOfResourcesException, statusToString(err).c_str());
         return 0;
     }
 
diff --git a/core/proto/android/nfc/Android.bp b/core/proto/android/nfc/Android.bp
new file mode 100644
index 0000000..6a62c91
--- /dev/null
+++ b/core/proto/android/nfc/Android.bp
@@ -0,0 +1,43 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "srcs_nfc_proto",
+    srcs: [
+        "*.proto",
+    ],
+}
+
+// Will be statically linked by `framework-nfc`.
+java_library {
+    name: "nfc-proto-java-gen",
+    installable: false,
+    proto: {
+        type: "stream",
+        include_dirs: [
+            "external/protobuf/src",
+        ],
+    },
+    srcs: [
+        ":srcs_nfc_proto",
+    ],
+    sdk_version: "current",
+    min_sdk_version: "current",
+}
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index 8e4006a..e029af4 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -110,11 +110,20 @@
     // All of this type/caption enabled for current profiles.
     repeated android.content.ComponentNameProto enabled = 3;
 
-
     repeated ManagedServiceInfoProto live_services = 4;
 
+    // Was: repeated ComponentNameProto, when snoozed services were not per-user-id.
+    reserved 5;
+
+    message SnoozedServices {
+        option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
+        optional int32 user_id = 1;
+        repeated android.content.ComponentNameProto snoozed = 2;
+    }
+
     // Snoozed for current profiles.
-    repeated android.content.ComponentNameProto snoozed = 5;
+    repeated SnoozedServices snoozed = 6;
 }
 
 message RankingHelperProto {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7002a52..b2526dd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -315,6 +315,7 @@
     <protected-broadcast android:name="android.media.MASTER_BALANCE_CHANGED_ACTION" />
     <protected-broadcast android:name="android.media.SCO_AUDIO_STATE_CHANGED" />
     <protected-broadcast android:name="android.media.ACTION_SCO_AUDIO_STATE_UPDATED" />
+    <protected-broadcast android:name="com.android.server.audio.action.CHECK_MUSIC_ACTIVE" />
 
     <protected-broadcast android:name="android.intent.action.MEDIA_REMOVED" />
     <protected-broadcast android:name="android.intent.action.MEDIA_UNMOUNTED" />
@@ -3884,7 +3885,7 @@
          <p>Should only be requested by the System, should be required by
          TileService declarations.-->
     <permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|recents" />
 
     <!-- Allows SystemUI to request third party controls.
          <p>Should only be requested by the System and required by
diff --git a/core/res/res/layout/autofill_fill_dialog.xml b/core/res/res/layout/autofill_fill_dialog.xml
index c382a65..2e65800 100644
--- a/core/res/res/layout/autofill_fill_dialog.xml
+++ b/core/res/res/layout/autofill_fill_dialog.xml
@@ -93,7 +93,7 @@
             android:layout_height="36dp"
             android:layout_marginTop="6dp"
             android:layout_marginBottom="6dp"
-            style="@style/AutofillHalfSheetOutlinedButton"
+            style="?android:attr/borderlessButtonStyle"
             android:text="@string/autofill_save_no">
         </Button>
 
diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml
index fd08241..3c0b789 100644
--- a/core/res/res/layout/autofill_save.xml
+++ b/core/res/res/layout/autofill_save.xml
@@ -81,7 +81,7 @@
                 android:layout_height="36dp"
                 android:layout_marginTop="6dp"
                 android:layout_marginBottom="6dp"
-                style="@style/AutofillHalfSheetOutlinedButton"
+                style="?android:attr/borderlessButtonStyle"
                 android:text="@string/autofill_save_no">
             </Button>
 
diff --git a/core/res/res/layout/notification_material_action_emphasized_tombstone.xml b/core/res/res/layout/notification_material_action_emphasized_tombstone.xml
new file mode 100644
index 0000000..60f10db
--- /dev/null
+++ b/core/res/res/layout/notification_material_action_emphasized_tombstone.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+
+<com.android.internal.widget.EmphasizedNotificationButton
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/NotificationEmphasizedAction"
+    android:id="@+id/action0"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:layout_marginStart="12dp"
+    android:drawablePadding="6dp"
+    android:enabled="false"
+    android:gravity="center"
+    android:singleLine="true"
+    android:ellipsize="end"
+/>
diff --git a/core/res/res/layout/resolve_grid_item.xml b/core/res/res/layout/resolve_grid_item.xml
index 50e6f33..a5ff470 100644
--- a/core/res/res/layout/resolve_grid_item.xml
+++ b/core/res/res/layout/resolve_grid_item.xml
@@ -17,6 +17,7 @@
 */
 -->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@+id/item"
               android:orientation="vertical"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
diff --git a/core/res/res/values/bools.xml b/core/res/res/values/bools.xml
index 4b27bf2..fe296c7 100644
--- a/core/res/res/values/bools.xml
+++ b/core/res/res/values/bools.xml
@@ -18,7 +18,6 @@
     <bool name="kg_enable_camera_default_widget">true</bool>
     <bool name="kg_center_small_widgets_vertically">false</bool>
     <bool name="kg_top_align_page_shrink_on_bouncer_visible">true</bool>
-    <bool name="kg_wake_on_acquire_start">false</bool>
     <bool name="action_bar_embed_tabs">true</bool>
     <bool name="split_action_bar_is_narrow">true</bool>
     <bool name="preferences_prefer_dual_pane">false</bool>
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index d5875f5..b83d3b4e 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -150,6 +150,8 @@
     <color name="notification_default_color">#757575</color> <!-- Gray 600 -->
 
     <color name="notification_action_button_text_color">@color/notification_default_color</color>
+    <item  name="notification_action_disabled_content_alpha" format="float" type="dimen">0.38</item>
+    <item  name="notification_action_disabled_container_alpha" format="float" type="dimen">0.12</item>
 
     <color name="notification_progress_background_color">@color/notification_secondary_text_color_current</color>
 
diff --git a/core/res/res/values/colors_material.xml b/core/res/res/values/colors_material.xml
index ea6e1f1..a99ba15 100644
--- a/core/res/res/values/colors_material.xml
+++ b/core/res/res/values/colors_material.xml
@@ -72,8 +72,8 @@
     <item name="secondary_content_alpha_material_dark" format="float" type="dimen">.7</item>
     <item name="secondary_content_alpha_material_light" format="float" type="dimen">0.60</item>
 
-    <item name="highlight_alpha_material_light" format="float" type="dimen">0.10</item>
-    <item name="highlight_alpha_material_dark" format="float" type="dimen">0.10</item>
+    <item name="highlight_alpha_material_light" format="float" type="dimen">0.5</item>
+    <item name="highlight_alpha_material_dark" format="float" type="dimen">0.5</item>
     <item name="highlight_alpha_material_colored" format="float" type="dimen">0.10</item>
 
     <!-- Primary & accent colors -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1c2f4ee..9aaf78b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -560,6 +560,10 @@
          rotations as the default behavior. -->
     <bool name="config_allowAllRotations">false</bool>
 
+    <!-- If false and config_allowAllRotations is false, the screen will rotate to the natural
+         orientation of the device when the auto-rotate policy is toggled. -->
+    <bool name="config_useCurrentRotationOnRotationLockChange">false</bool>
+
     <!-- If true, the direction rotation is applied to get to an application's requested
          orientation is reversed.  Normally, the model is that landscape is
          clockwise from portrait; thus on a portrait device an app requesting
@@ -632,6 +636,16 @@
          The default is false. -->
     <bool name="config_lidControlsSleep">false</bool>
 
+    <!-- The device states (supplied by DeviceStateManager) that should be treated as open by the
+         device fold controller. Default is empty. -->
+    <integer-array name="config_openDeviceStates">
+        <!-- Example:
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+        -->
+    </integer-array>
+
     <!-- The device states (supplied by DeviceStateManager) that should be treated as folded by the
          display fold controller. Default is empty. -->
     <integer-array name="config_foldedDeviceStates">
@@ -652,6 +666,16 @@
         -->
     </integer-array>
 
+    <!-- The device states (supplied by DeviceStateManager) that should be treated as a rear display
+     state. Default is empty. -->
+    <integer-array name="config_rearDisplayDeviceStates">
+        <!-- Example:
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+        -->
+    </integer-array>
+
     <!-- Indicates whether the window manager reacts to half-fold device states by overriding
      rotation. -->
     <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool>
@@ -944,6 +968,15 @@
     <!-- Boolean indicating whether light mode is allowed when DWB is turned on. -->
     <bool name="config_displayWhiteBalanceLightModeAllowed">true</bool>
 
+    <!-- Duration, in milliseconds, of the display white balance animated transitions. -->
+    <integer name="config_displayWhiteBalanceTransitionTime">3000</integer>
+
+    <!-- Device states where the sensor based rotation values should be reversed around the Z axis
+         for the default display.
+         TODO(b/265312193): Remove this workaround when this bug is fixed.-->
+    <integer-array name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis">
+    </integer-array>
+
     <!-- Indicate available ColorDisplayManager.COLOR_MODE_xxx. -->
     <integer-array name="config_availableColorModes">
         <!-- Example:
@@ -1118,14 +1151,25 @@
     <integer name="config_triplePressOnStemPrimaryBehavior">0</integer>
 
     <!-- Control the behavior when the user short presses the stem primary button.
-        Stem primary button is only used on watch form factor. If a device is not
-        a watch, setting this config is no-op.
-           0 - Nothing
-           1 - Go to launch all apps
+         Stem primary button is only used on watch form factor. If a device is not
+         a watch, setting this config is no-op.
+            0 - Nothing
+            1 - Go to launch all apps
     -->
     <integer name="config_shortPressOnStemPrimaryBehavior">0</integer>
 
 
+    <!-- Control the behavior of the search key.
+            0 - Launch default search activity
+            1 - Launch target activity defined by config_searchKeyTargetActivity
+    -->
+    <integer name="config_searchKeyBehavior">0</integer>
+
+    <!-- Component name for the default target activity to be launched when user
+         presses the global search key. [DO NOT TRANSLATE]
+    -->
+    <string name="config_searchKeyTargetActivity" translatable="false"></string>
+
     <!-- Time to wait while a button is pressed before triggering a very long press. -->
     <integer name="config_veryLongPressTimeout">3500</integer>
 
@@ -1264,6 +1308,13 @@
     <!-- Default LED off time for notification LED in milliseconds. -->
     <integer name="config_defaultNotificationLedOff">2000</integer>
 
+    <!-- LED behavior when battery is low.
+         Color for solid is taken from config_notificationsBatteryLowARGB
+          0 - default, solid when charging, flashing when not charging
+          1 - always solid when battery is low
+          2 - always flashing when battery is low -->
+    <integer name="config_notificationsBatteryLowBehavior">0</integer>
+
     <!-- Default value for led color when battery is low on charge -->
     <integer name="config_notificationsBatteryLowARGB">0xFFFF0000</integer>
 
@@ -2018,9 +2069,8 @@
          STREAM_MUSIC as if it's on TV platform. -->
     <bool name="config_single_volume">false</bool>
 
-    <!-- Flag indicating whether notification and ringtone volumes
-         are controlled together (aliasing is true) or not. -->
-    <bool name="config_alias_ring_notif_stream_types">true</bool>
+    <!-- Volume policy -->
+    <bool name="config_volume_down_to_enter_silent">false</bool>
 
     <!-- The number of volume steps for the notification stream -->
     <integer name="config_audio_notif_vol_steps">7</integer>
@@ -4926,9 +4976,8 @@
     <!-- If face auth sends the user directly to home/last open app, or stays on keyguard -->
     <bool name="config_faceAuthDismissesKeyguard">true</bool>
 
-    <!-- Default value for whether a SFPS device is required to be interactive for fingerprint auth
-    to unlock the device.  -->
-    <bool name="config_requireScreenOnToAuthEnabled">false</bool>
+    <!-- Default value for performant auth feature. -->
+    <bool name="config_performantAuthDefault">false</bool>
 
     <!-- The component name for the default profile supervisor, which can be set as a profile owner
     even after user setup is complete. The defined component should be used for supervision purposes
@@ -5307,6 +5356,9 @@
     <!-- Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. -->
     <bool name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled">false</bool>
 
+    <!-- Whether using display aspect ratio as a default aspect ratio for all letterboxed apps. -->
+    <bool name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled">false</bool>
+
     <!-- Whether the specific behaviour for translucent activities letterboxing is enabled.
          TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
     <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
@@ -6003,4 +6055,18 @@
     <!-- List of certificate to be used for font fs-verity integrity verification -->
     <string-array translatable="false" name="config_fontManagerServiceCerts">
     </string-array>
+
+    <!-- Whether the vendor power press code need to be mapped. -->
+    <bool name="config_powerPressMapping">false</bool>
+
+    <!-- Power press vendor code. -->
+    <integer name="config_powerPressCode">-1</integer>
+
+    <!-- Whether to show weather on the lock screen by default. -->
+    <bool name="config_lockscreenWeatherEnabledByDefault">false</bool>
+
+    <!-- Whether to reset Battery Stats on unplug when the battery level is high. -->
+    <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool>
+    <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged -->
+    <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool>
 </resources>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index ea2b988..a1d73ff 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -113,4 +113,8 @@
          new network. -->
     <bool name="config_enhanced_iwlan_handover_check">true</bool>
     <java-symbol type="bool" name="config_enhanced_iwlan_handover_check" />
+
+    <!-- Whether using the new SubscriptionManagerService or the old SubscriptionController -->
+    <bool name="config_using_subscription_manager_service">false</bool>
+    <java-symbol type="bool" name="config_using_subscription_manager_service" />
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 4d2aeaa..430e916 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -963,6 +963,11 @@
     <!-- Description for the capability of an accessibility service to take screenshot. [CHAR LIMIT=NONE] -->
     <string name="capability_desc_canTakeScreenshot">Can take a screenshot of the display.</string>
 
+    <!-- Dream -->
+
+    <!-- The title to use when a dream is opened in preview mode. [CHAR LIMIT=NONE] -->
+    <string name="dream_preview_title">Preview, <xliff:g id="dream_name" example="Clock">%1$s</xliff:g></string>
+
     <!--  Permissions -->
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
@@ -5142,6 +5147,14 @@
     <!-- Cling help message confirmation button when hiding the navigation bar entering immersive mode [CHAR LIMIT=30] -->
     <string name="immersive_cling_positive">Got it</string>
 
+    <!-- Text on a toast shown after the system rotates the screen for camera app
+         compatibility. [CHAR LIMIT=NONE] -->
+    <string name="display_rotation_camera_compat_toast_after_rotation">Rotate for a better view</string>
+
+    <!-- Text on a toast shown when a camera view is started within the app that may not be able
+         to display the camera preview correctly while in split screen. [CHAR LIMIT=NONE] -->
+    <string name="display_rotation_camera_compat_toast_in_split_screen">Exit split screen for a better view</string>
+
     <!-- Label for button to confirm chosen date or time [CHAR LIMIT=30] -->
     <string name="done_label">Done</string>
     <!--
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index db9d979..691f731 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -278,7 +278,6 @@
   <java-symbol type="attr" name="autofillSaveCustomSubtitleMaxHeight"/>
   <java-symbol type="bool" name="action_bar_embed_tabs" />
   <java-symbol type="bool" name="action_bar_expanded_action_views_exclusive" />
-  <java-symbol type="bool" name="config_alias_ring_notif_stream_types" />
   <java-symbol type="integer" name="config_audio_notif_vol_default" />
   <java-symbol type="integer" name="config_audio_notif_vol_steps" />
   <java-symbol type="integer" name="config_audio_ring_vol_default" />
@@ -322,6 +321,7 @@
   <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_kazakhstan" />
   <java-symbol type="integer" name="config_phonenumber_compare_min_match" />
   <java-symbol type="bool" name="config_single_volume" />
+  <java-symbol type="bool" name="config_volume_down_to_enter_silent" />
   <java-symbol type="bool" name="config_voice_capable" />
   <java-symbol type="bool" name="config_requireCallCapableAccountForHandle" />
   <java-symbol type="bool" name="config_user_notification_of_restrictied_mobile_access" />
@@ -451,6 +451,8 @@
   <java-symbol type="integer" name="config_doublePressOnStemPrimaryBehavior" />
   <java-symbol type="integer" name="config_triplePressOnStemPrimaryBehavior" />
   <java-symbol type="string" name="config_doublePressOnPowerTargetActivity" />
+  <java-symbol type="integer" name="config_searchKeyBehavior" />
+  <java-symbol type="string" name="config_searchKeyTargetActivity" />
   <java-symbol type="integer" name="config_windowOutsetBottom" />
   <java-symbol type="integer" name="db_connection_pool_size" />
   <java-symbol type="integer" name="db_journal_size_limit" />
@@ -1722,6 +1724,7 @@
   <java-symbol type="attr" name="dialogTitleDecorLayout" />
   <java-symbol type="attr" name="dialogTitleIconsDecorLayout" />
   <java-symbol type="bool" name="config_allowAllRotations" />
+  <java-symbol type="bool" name="config_useCurrentRotationOnRotationLockChange"/>
   <java-symbol type="bool" name="config_annoy_dianne" />
   <java-symbol type="bool" name="config_startDreamImmediatelyOnDock" />
   <java-symbol type="bool" name="config_carDockEnablesAccelerometer" />
@@ -2042,6 +2045,7 @@
   <java-symbol type="integer" name="config_notificationsBatteryFullARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryLedOff" />
   <java-symbol type="integer" name="config_notificationsBatteryLedOn" />
+  <java-symbol type="integer" name="config_notificationsBatteryLowBehavior" />
   <java-symbol type="integer" name="config_notificationsBatteryLowARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryMediumARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" />
@@ -2550,6 +2554,8 @@
   <java-symbol type="string" name="zen_mode_default_weekends_name" />
   <java-symbol type="string" name="zen_mode_default_events_name" />
   <java-symbol type="string" name="zen_mode_default_every_night_name" />
+  <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
+  <java-symbol type="string" name="display_rotation_camera_compat_toast_in_split_screen" />
   <java-symbol type="array" name="config_system_condition_providers" />
   <java-symbol type="string" name="muted_by" />
   <java-symbol type="string" name="zen_mode_alarm" />
@@ -2641,6 +2647,8 @@
   <java-symbol type="integer" name="config_sideFpsToastTimeout"/>
   <java-symbol type="integer" name="config_sidefpsSkipWaitForPowerAcquireMessage"/>
   <java-symbol type="integer" name="config_sidefpsSkipWaitForPowerVendorAcquireMessage"/>
+  <java-symbol type="integer" name="config_powerPressCode"/>
+  <java-symbol type="bool" name="config_powerPressMapping"/>
 
   <!-- Clickable toast used during sidefps enrollment -->
   <java-symbol type="layout" name="side_fps_toast" />
@@ -2714,7 +2722,7 @@
   <java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" />
   <java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" />
   <java-symbol type="bool" name="config_faceAuthDismissesKeyguard" />
-  <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" />
+  <java-symbol type="bool" name="config_performantAuthDefault" />
 
   <!-- Face config -->
   <java-symbol type="integer" name="config_faceMaxTemplatesPerUser" />
@@ -2829,7 +2837,6 @@
   <java-symbol type="dimen" name="fast_scroller_minimum_touch_target" />
   <java-symbol type="array" name="config_cdma_international_roaming_indicators" />
   <java-symbol type="string" name="kg_text_message_separator" />
-  <java-symbol type="bool" name="kg_wake_on_acquire_start" />
 
   <java-symbol type="bool" name="config_use_sim_language_file" />
   <java-symbol type="bool" name="config_LTE_eri_for_network_name" />
@@ -3293,7 +3300,10 @@
   <java-symbol type="id" name="notification_action_list_margin_target" />
   <java-symbol type="dimen" name="notification_actions_padding_start"/>
   <java-symbol type="dimen" name="notification_actions_collapsed_priority_width"/>
+  <!--prefer to use disabled content and surface alpha values for disabled actions-->
   <java-symbol type="dimen" name="notification_action_disabled_alpha" />
+  <java-symbol type="dimen" name="notification_action_disabled_content_alpha" />
+  <java-symbol type="dimen" name="notification_action_disabled_container_alpha" />
   <java-symbol type="id" name="tag_margin_end_when_icon_visible" />
   <java-symbol type="id" name="tag_margin_end_when_icon_gone" />
   <java-symbol type="id" name="tag_uses_right_icon_drawable" />
@@ -3345,6 +3355,7 @@
   <java-symbol type="string" name="unsupported_display_size_message" />
 
   <java-symbol type="layout" name="notification_material_action_emphasized" />
+  <java-symbol type="layout" name="notification_material_action_emphasized_tombstone" />
 
   <!-- Package name for the device provisioning package -->
   <java-symbol type="string" name="config_deviceProvisioningPackage" />
@@ -3426,6 +3437,12 @@
   <java-symbol type="array" name="config_displayWhiteBalanceDisplayPrimaries" />
   <java-symbol type="array" name="config_displayWhiteBalanceDisplayNominalWhite" />
   <java-symbol type="bool" name="config_displayWhiteBalanceLightModeAllowed" />
+  <java-symbol type="integer" name="config_displayWhiteBalanceTransitionTime" />
+
+  <!-- Device states where the sensor based rotation values should be reversed around the Z axis
+       for the default display.
+       TODO(b/265312193): Remove this workaround when this bug is fixed.-->
+  <java-symbol type="array" name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis" />
 
   <!-- Default first user restrictions -->
   <java-symbol type="array" name="config_defaultFirstUserRestrictions" />
@@ -4005,8 +4022,10 @@
   <java-symbol type="integer" name="config_maxScanTasksForHomeVisibility" />
 
   <!-- For Foldables -->
+  <java-symbol type="array" name="config_openDeviceStates" />
   <java-symbol type="array" name="config_foldedDeviceStates" />
   <java-symbol type="array" name="config_halfFoldedDeviceStates" />
+  <java-symbol type="array" name="config_rearDisplayDeviceStates" />
   <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" />
   <java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" />
   <java-symbol type="array" name="config_deviceStatesOnWhichToSleep" />
@@ -4262,6 +4281,8 @@
   <java-symbol type="string" name="capability_desc_canTakeScreenshot" />
   <java-symbol type="string" name="capability_title_canTakeScreenshot" />
 
+  <java-symbol type="string" name="dream_preview_title" />
+
   <java-symbol type="string" name="config_servicesExtensionPackage" />
 
   <!-- For app process exit info tracking -->
@@ -4488,6 +4509,7 @@
   <java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
   <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
   <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
+  <java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" />
   <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
   <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
@@ -4906,4 +4928,10 @@
   <java-symbol type="id" name="language_picker_header" />
 
   <java-symbol type="dimen" name="status_bar_height_default" />
+
+  <!-- Whether to show weather on the lockscreen by default. -->
+  <java-symbol type="bool" name="config_lockscreenWeatherEnabledByDefault" />
+
+  <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" />
+  <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" />
 </resources>
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 3e4b1cc..e96c642 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1414,6 +1414,7 @@
         <activity android:name="com.android.internal.app.ChooserWrapperActivity"/>
         <activity android:name="com.android.internal.app.ResolverWrapperActivity"/>
         <activity android:name="com.android.internal.app.IntentForwarderActivityTest$IntentForwarderWrapperActivity"/>
+        <activity android:name="com.android.internal.accessibility.AccessibilityShortcutChooserActivityTest$TestAccessibilityShortcutChooserActivity"/>
 
         <receiver android:name="android.app.activity.AbortReceiver"
             android:exported="true">
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index ed2b101..3768063 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -368,4 +368,20 @@
             PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
         assertEquals(n1, "cache_key.bluetooth.get_state");
     }
+
+    @Test
+    public void testOnTrimMemory() {
+        TestCache cache = new TestCache(MODULE, "trimMemoryTest");
+        // The cache is not active until it has been invalidated once.
+        cache.invalidateCache();
+        // Populate the cache with six entries.
+        for (int i = 0; i < 6; i++) {
+            cache.query(i);
+        }
+        // The maximum number of entries in TestCache is 4, so even though six entries were
+        // created, only four are retained.
+        assertEquals(4, cache.size());
+        PropertyInvalidatedCache.onTrimMemory();
+        assertEquals(0, cache.size());
+    }
 }
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index f370ebd..9d6b29e 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -17,6 +17,7 @@
 package android.window;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -60,8 +61,8 @@
     private OnBackAnimationCallback mCallback1;
     @Mock
     private OnBackAnimationCallback mCallback2;
-    @Mock
-    private BackEvent mBackEvent;
+    private final BackMotionEvent mBackEvent = new BackMotionEvent(
+            0, 0, 0, BackEvent.EDGE_LEFT, null);
 
     @Before
     public void setUp() throws Exception {
@@ -89,12 +90,12 @@
                 captor.capture());
         captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted(mBackEvent);
+        verify(mCallback1).onBackStarted(any(BackEvent.class));
         verifyZeroInteractions(mCallback2);
 
         captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted(mBackEvent);
+        verify(mCallback2).onBackStarted(any(BackEvent.class));
         verifyNoMoreInteractions(mCallback1);
     }
 
@@ -114,7 +115,7 @@
         assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY);
         captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted(mBackEvent);
+        verify(mCallback1).onBackStarted(any(BackEvent.class));
     }
 
     @Test
@@ -152,6 +153,6 @@
         verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture());
         captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted(mBackEvent);
+        verify(mCallback2).onBackStarted(any(BackEvent.class));
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
new file mode 100644
index 0000000..973b904
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.accessibility;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.doubleClick;
+import static androidx.test.espresso.action.ViewActions.scrollTo;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.RootMatchers.isDialog;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.endsWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityManager;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+/**
+ * Tests for {@link AccessibilityShortcutChooserActivity}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityShortcutChooserActivityTest {
+    private static final String ONE_HANDED_MODE = "One-Handed mode";
+    private static final String TEST_LABEL = "TEST_LABEL";
+    private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class");
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock
+    private AccessibilityServiceInfo mAccessibilityServiceInfo;
+    @Mock
+    private ResolveInfo mResolveInfo;
+    @Mock
+    private ServiceInfo mServiceInfo;
+    @Mock
+    private ApplicationInfo mApplicationInfo;
+    @Mock
+    private IAccessibilityManager mAccessibilityManagerService;
+
+    @Test
+    public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist()
+            throws Exception {
+        configureTestService();
+        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
+                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
+        scenario.moveToState(Lifecycle.State.CREATED);
+        scenario.moveToState(Lifecycle.State.STARTED);
+        scenario.moveToState(Lifecycle.State.RESUMED);
+
+        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
+                isDialog()).check(matches(isDisplayed()));
+        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
+        onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick());
+        onView(withId(R.id.accessibility_permission_enable_deny_button)).perform(scrollTo(),
+                click());
+
+        onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check(
+                doesNotExist());
+        scenario.moveToState(Lifecycle.State.DESTROYED);
+    }
+
+    @Test
+    public void popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true);
+        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
+                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
+        scenario.moveToState(Lifecycle.State.CREATED);
+        scenario.moveToState(Lifecycle.State.STARTED);
+        scenario.moveToState(Lifecycle.State.RESUMED);
+
+        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
+                isDialog()).check(matches(isDisplayed()));
+        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
+        onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(matches(isDisplayed()));
+        scenario.moveToState(Lifecycle.State.DESTROYED);
+    }
+
+    @Test
+    public void popEditShortcutMenuList_oneHandedModeDisabled_shouldNotBeInListView() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false);
+        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
+                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
+        scenario.moveToState(Lifecycle.State.CREATED);
+        scenario.moveToState(Lifecycle.State.STARTED);
+        scenario.moveToState(Lifecycle.State.RESUMED);
+
+        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
+                isDialog()).check(matches(isDisplayed()));
+        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
+        onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(doesNotExist());
+        scenario.moveToState(Lifecycle.State.DESTROYED);
+    }
+
+    private void configureTestService() throws Exception {
+        when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo);
+        mResolveInfo.serviceInfo = mServiceInfo;
+        mServiceInfo.applicationInfo = mApplicationInfo;
+        when(mResolveInfo.loadLabel(any(PackageManager.class))).thenReturn(TEST_LABEL);
+        when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME);
+        when(mAccessibilityManagerService.getInstalledAccessibilityServiceList(
+                anyInt())).thenReturn(Collections.singletonList(mAccessibilityServiceInfo));
+
+        TestAccessibilityShortcutChooserActivity.setupForTesting(mAccessibilityManagerService);
+    }
+
+    /**
+     * Used for testing.
+     */
+    public static class TestAccessibilityShortcutChooserActivity extends
+            AccessibilityShortcutChooserActivity {
+        private static IAccessibilityManager sAccessibilityManagerService;
+
+        public static void setupForTesting(IAccessibilityManager accessibilityManagerService) {
+            sAccessibilityManagerService = accessibilityManagerService;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (Context.ACCESSIBILITY_SERVICE.equals(name)
+                    && sAccessibilityManagerService != null) {
+                return new AccessibilityManager(this, new Handler(getMainLooper()),
+                        sAccessibilityManagerService, /* userId= */ 0, /* serviceConnect= */ true);
+            }
+
+            return super.getSystemService(name);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
index 6baf305..c92ae2c 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -21,6 +21,11 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
 
+import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -203,6 +208,17 @@
         when(mAlertDialog.getWindow()).thenReturn(window);
 
         when(mTextToSpeech.getVoice()).thenReturn(mVoice);
+
+        // Clears the sFrameworkShortcutFeaturesMap field which was not properly initialized
+        // during testing.
+        try {
+            Field field = AccessibilityShortcutController.class.getDeclaredField(
+                    "sFrameworkShortcutFeaturesMap");
+            field.setAccessible(true);
+            field.set(window, null);
+        } catch (Exception e) {
+            throw new RuntimeException("Unable to set sFrameworkShortcutFeaturesMap", e);
+        }
     }
 
     @AfterClass
@@ -428,11 +444,10 @@
     }
 
     @Test
-    public void getFrameworkFeatureMap_shouldBeNonNullAndUnmodifiable() {
-        Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+    public void getFrameworkFeatureMap_shouldBeUnmodifiable() {
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
                 frameworkFeatureMap =
                 AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
-        assertTrue("Framework features not supported", frameworkFeatureMap.size() > 0);
 
         try {
             frameworkFeatureMap.clear();
@@ -443,6 +458,39 @@
     }
 
     @Test
+    public void getFrameworkFeatureMap_containsExpectedDefaultKeys() {
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+                frameworkFeatureMap =
+                AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
+
+        assertTrue(frameworkFeatureMap.containsKey(COLOR_INVERSION_COMPONENT_NAME));
+        assertTrue(frameworkFeatureMap.containsKey(DALTONIZER_COMPONENT_NAME));
+        assertTrue(frameworkFeatureMap.containsKey(REDUCE_BRIGHT_COLORS_COMPONENT_NAME));
+    }
+
+    @Test
+    public void getFrameworkFeatureMap_oneHandedModeEnabled_containsExpectedKey() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true);
+
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+                frameworkFeatureMap =
+                AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
+
+        assertTrue(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME));
+    }
+
+    @Test
+    public void getFrameworkFeatureMap_oneHandedModeDisabled_containsExpectedKey() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false);
+
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+                frameworkFeatureMap =
+                AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
+
+        assertFalse(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME));
+    }
+
+    @Test
     public void testOnAccessibilityShortcut_forServiceWithNoSummary_doesNotCrash()
             throws Exception {
         configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java b/core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java
new file mode 100644
index 0000000..ff014ad
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.accessibility;
+
+import com.android.internal.os.RoSystemProperties;
+
+import java.lang.reflect.Field;
+
+/**
+ * Test utility methods.
+ */
+public class TestUtils {
+
+    /**
+     * Sets the {@code enabled} of the given OneHandedMode flags to simulate device behavior.
+     */
+    public static void setOneHandedModeEnabled(Object obj, boolean enabled) {
+        try {
+            final Field field = RoSystemProperties.class.getDeclaredField(
+                    "SUPPORT_ONE_HANDED_MODE");
+            field.setAccessible(true);
+            field.setBoolean(obj, enabled);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS
new file mode 100644
index 0000000..2e96c97
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS
@@ -0,0 +1 @@
+include /packages/SystemUI/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java
new file mode 100644
index 0000000..6b9d39c
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.internal.config.sysui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@SmallTest
+public class SystemUiSystemPropertiesFlagsTest extends TestCase {
+
+    public class TestableDebugResolver extends SystemUiSystemPropertiesFlags.DebugResolver {
+        final Map<String, Boolean> mTestData = new HashMap<>();
+
+        @Override
+        public boolean getBoolean(String key, boolean defaultValue) {
+            Boolean testValue = mTestData.get(key);
+            return testValue == null ? defaultValue : testValue;
+        }
+
+        public void set(Flag flag, Boolean value) {
+            mTestData.put(flag.mSysPropKey, value);
+        }
+    }
+
+    private FlagResolver mProdResolver;
+    private TestableDebugResolver mDebugResolver;
+
+    private Flag mReleasedFlag;
+    private Flag mTeamfoodFlag;
+    private Flag mDevFlag;
+
+    public void setUp() {
+        mProdResolver = new SystemUiSystemPropertiesFlags.ProdResolver();
+        mDebugResolver = new TestableDebugResolver();
+        mReleasedFlag = SystemUiSystemPropertiesFlags.releasedFlag("mReleasedFlag");
+        mTeamfoodFlag = SystemUiSystemPropertiesFlags.teamfoodFlag("mTeamfoodFlag");
+        mDevFlag = SystemUiSystemPropertiesFlags.devFlag("mDevFlag");
+    }
+
+    public void tearDown() {
+        SystemUiSystemPropertiesFlags.TEST_RESOLVER = null;
+    }
+
+    public void testProdResolverReturnsDefault() {
+        assertThat(mProdResolver.isEnabled(mReleasedFlag)).isTrue();
+        assertThat(mProdResolver.isEnabled(mTeamfoodFlag)).isFalse();
+        assertThat(mProdResolver.isEnabled(mDevFlag)).isFalse();
+    }
+
+    public void testDebugResolverAndReleasedFlag() {
+        assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue();
+
+        mDebugResolver.set(mReleasedFlag, false);
+        assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isFalse();
+
+        mDebugResolver.set(mReleasedFlag, true);
+        assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue();
+    }
+
+    private void assertTeamfoodFlag(Boolean flagValue, Boolean teamfood, boolean expected) {
+        mDebugResolver.set(mTeamfoodFlag, flagValue);
+        mDebugResolver.set(SystemUiSystemPropertiesFlags.TEAMFOOD, teamfood);
+        assertThat(mDebugResolver.isEnabled(mTeamfoodFlag)).isEqualTo(expected);
+    }
+
+    public void testDebugResolverAndTeamfoodFlag() {
+        assertTeamfoodFlag(null, null, false);
+        assertTeamfoodFlag(true, null, true);
+        assertTeamfoodFlag(false, null, false);
+        assertTeamfoodFlag(null, true, true);
+        assertTeamfoodFlag(true, true, true);
+        assertTeamfoodFlag(false, true, false);
+        assertTeamfoodFlag(null, false, false);
+        assertTeamfoodFlag(true, false, true);
+        assertTeamfoodFlag(false, false, false);
+    }
+
+    public void testDebugResolverAndDevFlag() {
+        assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse();
+
+        mDebugResolver.set(mDevFlag, true);
+        assertThat(mDebugResolver.isEnabled(mDevFlag)).isTrue();
+
+        mDebugResolver.set(mDevFlag, false);
+        assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse();
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
index 52feac5..4c9b2b7 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
@@ -360,6 +360,7 @@
         // map of ActivityManager process states and how long to simulate run time in each state
         Map<Integer, Integer> stateRuntimeMap = new HashMap<Integer, Integer>();
         stateRuntimeMap.put(ActivityManager.PROCESS_STATE_TOP, 1111);
+        stateRuntimeMap.put(ActivityManager.PROCESS_STATE_BOUND_TOP, 7382);
         stateRuntimeMap.put(ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE, 1234);
         stateRuntimeMap.put(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 2468);
         stateRuntimeMap.put(ActivityManager.PROCESS_STATE_TOP_SLEEPING, 7531);
@@ -396,7 +397,8 @@
 
         actualRunTimeUs = uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_FOREGROUND_SERVICE,
                 elapsedTimeUs, STATS_SINCE_CHARGED);
-        expectedRunTimeMs = stateRuntimeMap.get(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        expectedRunTimeMs = stateRuntimeMap.get(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE)
+                + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
         assertEquals(expectedRunTimeMs * 1000, actualRunTimeUs);
 
         actualRunTimeUs = uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_TOP_SLEEPING,
@@ -406,8 +408,7 @@
 
         actualRunTimeUs = uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_FOREGROUND,
                 elapsedTimeUs, STATS_SINCE_CHARGED);
-        expectedRunTimeMs = stateRuntimeMap.get(ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND)
-                + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+        expectedRunTimeMs = stateRuntimeMap.get(ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
         assertEquals(expectedRunTimeMs * 1000, actualRunTimeUs);
 
         actualRunTimeUs = uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_BACKGROUND,
@@ -415,7 +416,8 @@
         expectedRunTimeMs = stateRuntimeMap.get(ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND)
                 + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_BACKUP)
                 + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_SERVICE)
-                + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_RECEIVER);
+                + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_RECEIVER)
+                + stateRuntimeMap.get(ActivityManager.PROCESS_STATE_BOUND_TOP);
         assertEquals(expectedRunTimeMs * 1000, actualRunTimeUs);
 
         actualRunTimeUs = uid.getProcessStateTime(BatteryStats.Uid.PROCESS_STATE_CACHED,
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java
new file mode 100644
index 0000000..9c2d332
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java
@@ -0,0 +1,362 @@
+/*
+ * 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.internal.os;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BatteryStatsResetTest {
+
+    private static final int BATTERY_NOMINAL_VOLTAGE_MV = 3700;
+    private static final int BATTERY_CAPACITY_UAH = 4_000_000;
+    private static final int BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL = 100;
+
+    private MockClock mMockClock;
+    private MockBatteryStatsImpl mBatteryStatsImpl;
+
+
+    /**
+     * Battery status. Must be one of the following:
+     * {@link BatteryManager#BATTERY_STATUS_UNKNOWN}
+     * {@link BatteryManager#BATTERY_STATUS_CHARGING}
+     * {@link BatteryManager#BATTERY_STATUS_DISCHARGING}
+     * {@link BatteryManager#BATTERY_STATUS_NOT_CHARGING}
+     * {@link BatteryManager#BATTERY_STATUS_FULL}
+     */
+    private int mBatteryStatus;
+    /**
+     * Battery health. Must be one of the following:
+     * {@link BatteryManager#BATTERY_HEALTH_UNKNOWN}
+     * {@link BatteryManager#BATTERY_HEALTH_GOOD}
+     * {@link BatteryManager#BATTERY_HEALTH_OVERHEAT}
+     * {@link BatteryManager#BATTERY_HEALTH_DEAD}
+     * {@link BatteryManager#BATTERY_HEALTH_OVER_VOLTAGE}
+     * {@link BatteryManager#BATTERY_HEALTH_UNSPECIFIED_FAILURE}
+     * {@link BatteryManager#BATTERY_HEALTH_COLD}
+     */
+    private int mBatteryHealth;
+    /**
+     * Battery plug type. Can be the union of any number of the following flags:
+     * {@link BatteryManager#BATTERY_PLUGGED_AC}
+     * {@link BatteryManager#BATTERY_PLUGGED_USB}
+     * {@link BatteryManager#BATTERY_PLUGGED_WIRELESS}
+     * {@link BatteryManager#BATTERY_PLUGGED_DOCK}
+     *
+     * Zero means the device is unplugged.
+     */
+    private int mBatteryPlugType;
+    private int mBatteryLevel;
+    private int mBatteryTemp;
+    private int mBatteryVoltageMv;
+    private int mBatteryChargeUah;
+    private int mBatteryChargeFullUah;
+    private long mBatteryChargeTimeToFullSeconds;
+
+    @Before
+    public void setUp() {
+        final Context context = InstrumentationRegistry.getContext();
+
+        mMockClock = new MockClock();
+        mBatteryStatsImpl = new MockBatteryStatsImpl(mMockClock, context.getFilesDir());
+        mBatteryStatsImpl.onSystemReady();
+
+
+        // Set up the battery state. Start off with a fully charged plugged in battery.
+        mBatteryStatus = BatteryManager.BATTERY_STATUS_FULL;
+        mBatteryHealth = BatteryManager.BATTERY_HEALTH_GOOD;
+        mBatteryPlugType = BatteryManager.BATTERY_PLUGGED_USB;
+        mBatteryLevel = 100;
+        mBatteryTemp = 70; // Arbitrary reasonable temperature.
+        mBatteryVoltageMv = BATTERY_NOMINAL_VOLTAGE_MV;
+        mBatteryChargeUah = BATTERY_CAPACITY_UAH;
+        mBatteryChargeFullUah = BATTERY_CAPACITY_UAH;
+        mBatteryChargeTimeToFullSeconds = 0;
+    }
+
+    @Test
+    public void testResetOnUnplug_highBatteryLevel() {
+        mBatteryStatsImpl.setBatteryStatsConfig(
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugHighBatteryLevel(true)
+                        .build());
+
+        long expectedResetTimeUs = 0;
+
+        unplugBattery();
+        dischargeToLevel(60);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(80);
+        unplugBattery();
+        // Reset should not occur until battery level above 90.
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(95);
+        // Reset should not occur until unplug.
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        unplugBattery();
+        // Reset should occur on unplug now that battery level is high (>=90)
+        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // disable high battery level reset on unplug.
+        mBatteryStatsImpl.setBatteryStatsConfig(
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugHighBatteryLevel(false)
+                        .build());
+
+        dischargeToLevel(60);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(95);
+        unplugBattery();
+        // Reset should not occur since the high battery level logic has been disabled.
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+    }
+
+    @Test
+    public void testResetOnUnplug_significantCharge() {
+        mBatteryStatsImpl.setBatteryStatsConfig(
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugAfterSignificantCharge(true)
+                        .build());
+        long expectedResetTimeUs = 0;
+
+        unplugBattery();
+        // Battery level dropped below 20%.
+        dischargeToLevel(15);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(50);
+        unplugBattery();
+        // Reset should not occur until battery level above 80
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(85);
+        unplugBattery();
+        // Reset should not occur because the charge session did not go from 20% to 80%
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Battery level dropped below 20%.
+        dischargeToLevel(15);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(85);
+        unplugBattery();
+        // Reset should occur after significant charge amount.
+        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // disable reset on unplug after significant charge.
+        mBatteryStatsImpl.setBatteryStatsConfig(
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugAfterSignificantCharge(false)
+                        .build());
+
+        // Battery level dropped below 20%.
+        dischargeToLevel(15);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(85);
+        unplugBattery();
+        // Reset should not occur after significant charge amount.
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+    }
+
+    @Test
+    public void testResetOnUnplug_manyPartialCharges() {
+        long expectedResetTimeUs = 0;
+
+        unplugBattery();
+        // Cumulative battery discharged: 60%.
+        dischargeToLevel(40);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(80);
+        unplugBattery();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Cumulative battery discharged: 100%.
+        dischargeToLevel(40);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(80);
+        unplugBattery();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Cumulative battery discharged: 140%.
+        dischargeToLevel(40);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(80);
+        unplugBattery();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Cumulative battery discharged: 180%.
+        dischargeToLevel(40);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(80);
+        unplugBattery();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Cumulative battery discharged: 220%.
+        dischargeToLevel(40);
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        chargeToLevel(80);
+        unplugBattery();
+        // Should reset after >200% of cumulative battery discharge
+        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+    }
+
+    @Test
+    public void testResetWhilePluggedIn_longPlugIn() {
+        // disable high battery level reset on unplug.
+        mBatteryStatsImpl.setBatteryStatsConfig(
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugHighBatteryLevel(false)
+                        .setResetOnUnplugAfterSignificantCharge(false)
+                        .build());
+        long expectedResetTimeUs = 0;
+
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset should still not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset 47 hour threshold crossed, reset should occur.
+        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset another 47 hour threshold crossed, reset should occur.
+        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset should not occur
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        unplugBattery();
+        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset should not occur, since unplug occurred recently.
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+        // Increment time a day
+        incTimeMs(24L * 60L * 60L * 1000L);
+        mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+        // Reset another 47 hour threshold crossed, reset should occur.
+        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+    }
+
+    private void dischargeToLevel(int targetLevel) {
+        mBatteryStatus = BatteryManager.BATTERY_STATUS_DISCHARGING;
+        for (int level = mBatteryLevel - 1; level >= targetLevel; level--) {
+            prepareBatteryLevel(level);
+            incTimeMs(5000); // Arbitrary discharge rate.
+            updateBatteryState();
+        }
+    }
+
+    private void chargeToLevel(int targetLevel) {
+        mBatteryStatus = BatteryManager.BATTERY_STATUS_CHARGING;
+        for (int level = mBatteryLevel + 1; level <= targetLevel; level++) {
+            if (level >= 100) mBatteryStatus = BatteryManager.BATTERY_STATUS_FULL;
+            prepareBatteryLevel(level);
+            incTimeMs(BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL * 1000);
+            updateBatteryState();
+        }
+    }
+
+    private void unplugBattery() {
+        mBatteryPlugType = 0;
+        updateBatteryState();
+    }
+
+    private void plugBattery(int type) {
+        mBatteryPlugType |= type;
+        updateBatteryState();
+    }
+
+    private void prepareBatteryLevel(int level) {
+        mBatteryLevel = level;
+        mBatteryChargeUah = mBatteryLevel * mBatteryChargeFullUah / 100;
+        mBatteryChargeTimeToFullSeconds =
+                (100 - mBatteryLevel) * BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL;
+    }
+
+    private void incTimeMs(long milliseconds) {
+        mMockClock.realtime += milliseconds;
+        mMockClock.uptime += milliseconds / 2; // Arbitrary slower uptime accumulation
+        mMockClock.currentTime += milliseconds;
+    }
+
+    private void updateBatteryState() {
+        mBatteryStatsImpl.setBatteryStateLocked(mBatteryStatus, mBatteryHealth, mBatteryPlugType,
+                mBatteryLevel, mBatteryTemp, mBatteryVoltageMv, mBatteryChargeUah,
+                mBatteryChargeFullUah, mBatteryChargeTimeToFullSeconds,
+                mMockClock.elapsedRealtime(), mMockClock.uptimeMillis(),
+                mMockClock.currentTimeMillis());
+    }
+}
+
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
index 354b937..ae2d1af 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
@@ -78,9 +78,9 @@
                 batteryUsageStats.getUidBatteryConsumers();
         final UidBatteryConsumer uidBatteryConsumer = uidBatteryConsumers.get(0);
         assertThat(uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND))
-                .isEqualTo(60 * MINUTE_IN_MS);
+                .isEqualTo(20 * MINUTE_IN_MS);
         assertThat(uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND))
-                .isEqualTo(10 * MINUTE_IN_MS);
+                .isEqualTo(40 * MINUTE_IN_MS);
         assertThat(uidBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_AUDIO))
                 .isWithin(PRECISION).of(2.0);
         assertThat(
@@ -121,22 +121,44 @@
     private BatteryStatsImpl prepareBatteryStats() {
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.noteActivityResumedLocked(APP_UID,
-                10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
-        batteryStats.noteUidProcessStateLocked(APP_UID, ActivityManager.PROCESS_STATE_TOP,
-                10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
-        batteryStats.noteActivityPausedLocked(APP_UID,
-                30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
-        batteryStats.noteUidProcessStateLocked(APP_UID, ActivityManager.PROCESS_STATE_SERVICE,
-                30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
-        batteryStats.noteUidProcessStateLocked(APP_UID,
-                ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE,
-                40 * MINUTE_IN_MS, 40 * MINUTE_IN_MS);
-        batteryStats.noteUidProcessStateLocked(APP_UID,
-                ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE,
-                50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
-        batteryStats.noteUidProcessStateLocked(APP_UID, ActivityManager.PROCESS_STATE_CACHED_EMPTY,
-                80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        mStatsRule.setTime(10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteActivityResumedLocked(APP_UID);
+        }
+
+        mStatsRule.setTime(10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteUidProcessStateLocked(APP_UID, ActivityManager.PROCESS_STATE_TOP);
+        }
+        mStatsRule.setTime(30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteActivityPausedLocked(APP_UID);
+        }
+        mStatsRule.setTime(30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteUidProcessStateLocked(APP_UID,
+                    ActivityManager.PROCESS_STATE_SERVICE);
+        }
+        mStatsRule.setTime(40 * MINUTE_IN_MS, 40 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteUidProcessStateLocked(APP_UID,
+                    ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        }
+        mStatsRule.setTime(50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteUidProcessStateLocked(APP_UID,
+                    ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+        }
+        mStatsRule.setTime(60 * MINUTE_IN_MS, 60 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteUidProcessStateLocked(APP_UID,
+                    ActivityManager.PROCESS_STATE_BOUND_TOP);
+        }
+        mStatsRule.setTime(70 * MINUTE_IN_MS, 70 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.noteUidProcessStateLocked(APP_UID,
+                    ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+        }
 
         batteryStats.noteFlashlightOnLocked(APP_UID, 1000, 1000);
         batteryStats.noteFlashlightOffLocked(APP_UID, 5000, 5000);
@@ -320,7 +342,7 @@
         Context context = InstrumentationRegistry.getContext();
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
         mStatsRule.setCurrentTime(5 * MINUTE_IN_MS);
-        batteryStats.resetAllStatsCmdLocked();
+        batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
 
         BatteryUsageStatsStore batteryUsageStatsStore = new BatteryUsageStatsStore(context,
                 batteryStats, new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
@@ -335,14 +357,14 @@
         batteryStats.noteFlashlightOffLocked(APP_UID,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
         mStatsRule.setCurrentTime(25 * MINUTE_IN_MS);
-        batteryStats.resetAllStatsCmdLocked();
+        batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
 
         batteryStats.noteFlashlightOnLocked(APP_UID,
                 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
         batteryStats.noteFlashlightOffLocked(APP_UID,
                 50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
         mStatsRule.setCurrentTime(55 * MINUTE_IN_MS);
-        batteryStats.resetAllStatsCmdLocked();
+        batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
 
         // This section should be ignored because the timestamp is out or range
         batteryStats.noteFlashlightOnLocked(APP_UID,
@@ -350,7 +372,7 @@
         batteryStats.noteFlashlightOffLocked(APP_UID,
                 70 * MINUTE_IN_MS, 70 * MINUTE_IN_MS);
         mStatsRule.setCurrentTime(75 * MINUTE_IN_MS);
-        batteryStats.resetAllStatsCmdLocked();
+        batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
 
         // This section should be ignored because it represents the current stats session
         batteryStats.noteFlashlightOnLocked(APP_UID,
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java
index c9729fa..11b9047 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java
@@ -84,7 +84,7 @@
 
         mMockClock.realtime = 1_000_000;
         mMockClock.uptime = 1_000_000;
-        mBatteryStats.resetAllStatsCmdLocked();
+        mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
 
         final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
         assertThat(timestamps).hasLength(1);
@@ -114,7 +114,7 @@
         final int numberOfSnapshots =
                 (int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / snapshotFileSize);
         for (int i = 0; i < numberOfSnapshots + 2; i++) {
-            mBatteryStats.resetAllStatsCmdLocked();
+            mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
 
             mMockClock.realtime += 10_000_000;
             mMockClock.uptime += 10_000_000;
@@ -141,7 +141,7 @@
             mMockClock.currentTime += 10_000_000;
             prepareBatteryStats();
 
-            mBatteryStats.resetAllStatsCmdLocked();
+            mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
 
         assertThat(getDirectorySize(mStoreDirectory)).isNotEqualTo(0);
diff --git a/core/tests/screenshothelpertests/Android.bp b/core/tests/screenshothelpertests/Android.bp
index 37af99c..3c71e6e 100644
--- a/core/tests/screenshothelpertests/Android.bp
+++ b/core/tests/screenshothelpertests/Android.bp
@@ -13,7 +13,7 @@
     srcs: [
         "src/**/*.java",
     ],
-    
+
     static_libs: [
         "frameworks-base-testutils",
         "androidx.test.runner",
@@ -21,6 +21,7 @@
         "androidx.test.ext.junit",
         "mockito-target-minus-junit4",
         "platform-test-annotations",
+        "testng",
     ],
 
     libs: [
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
index 2719431..5c9894e 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
@@ -17,6 +17,7 @@
 package com.android.internal.util;
 
 import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
 
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.fail;
@@ -31,9 +32,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
 import android.graphics.Insets;
 import android.graphics.Rect;
-import android.os.Bundle;
+import android.hardware.HardwareBuffer;
 import android.os.Handler;
 import android.os.Looper;
 import android.view.WindowManager;
@@ -79,30 +82,48 @@
 
     @Test
     public void testFullscreenScreenshot() {
-        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
+        mScreenshotHelper.takeScreenshot(
                 WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
     }
 
     @Test
+    public void testFullscreenScreenshotRequest() {
+        ScreenshotRequest request = new ScreenshotRequest.Builder(
+                TAKE_SCREENSHOT_FULLSCREEN, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
+                .build();
+        mScreenshotHelper.takeScreenshot(request, mHandler, null);
+    }
+
+    @Test
     public void testProvidedImageScreenshot() {
-        mScreenshotHelper.provideScreenshot(
-                new Bundle(), new Rect(), Insets.of(0, 0, 0, 0), 1, 1, new ComponentName("", ""),
-                WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
+        HardwareBuffer buffer = HardwareBuffer.create(
+                10, 10, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+        Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
+        ScreenshotRequest request = new ScreenshotRequest.Builder(
+                TAKE_SCREENSHOT_PROVIDED_IMAGE, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
+                .setTopComponent(new ComponentName("", ""))
+                .setTaskId(1)
+                .setUserId(1)
+                .setBitmap(bitmap)
+                .setBoundsOnScreen(new Rect())
+                .setInsets(Insets.NONE)
+                .build();
+        mScreenshotHelper.takeScreenshot(request, mHandler, null);
     }
 
     @Test
     public void testScreenshotTimesOut() {
         long timeoutMs = 10;
+        ScreenshotRequest request = new ScreenshotRequest.Builder(
+                TAKE_SCREENSHOT_FULLSCREEN, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
+                .build();
 
         CountDownLatch lock = new CountDownLatch(1);
-        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
-                WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
-                mHandler,
-                timeoutMs,
+        mScreenshotHelper.takeScreenshotInternal(request, mHandler,
                 uri -> {
                     assertNull(uri);
                     lock.countDown();
-                });
+                }, timeoutMs);
 
         try {
             // Add tolerance for delay to prevent flakes.
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
new file mode 100644
index 0000000..89acbc7
--- /dev/null
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.internal.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.UserHandle.USER_NULL;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class ScreenshotRequestTest {
+    private final ComponentName mComponentName =
+            new ComponentName("android.test", "android.test.Component");
+
+    @Test
+    public void testSimpleScreenshot() {
+        ScreenshotRequest in =
+                new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build();
+
+        Parcel parcel = Parcel.obtain();
+        in.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
+
+        assertEquals(TAKE_SCREENSHOT_FULLSCREEN, out.getType());
+        assertEquals(SCREENSHOT_OTHER, out.getSource());
+        assertNull("Top component was expected to be null", out.getTopComponent());
+        assertEquals(INVALID_TASK_ID, out.getTaskId());
+        assertEquals(USER_NULL, out.getUserId());
+        assertNull("Bitmap was expected to be null", out.getBitmap());
+        assertNull("Bounds were expected to be null", out.getBoundsInScreen());
+        assertEquals(Insets.NONE, out.getInsets());
+    }
+
+    @Test
+    public void testProvidedScreenshot() {
+        Bitmap bitmap = makeHardwareBitmap(50, 50);
+        ScreenshotRequest in =
+                new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+                        .setTopComponent(mComponentName)
+                        .setTaskId(2)
+                        .setUserId(3)
+                        .setBitmap(bitmap)
+                        .setBoundsOnScreen(new Rect(10, 10, 60, 60))
+                        .setInsets(Insets.of(2, 3, 4, 5))
+                        .build();
+
+        Parcel parcel = Parcel.obtain();
+        in.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
+
+        assertEquals(TAKE_SCREENSHOT_PROVIDED_IMAGE, out.getType());
+        assertEquals(SCREENSHOT_OTHER, out.getSource());
+        assertEquals(mComponentName, out.getTopComponent());
+        assertEquals(2, out.getTaskId());
+        assertEquals(3, out.getUserId());
+        assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
+        assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
+        assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
+    }
+
+    @Test
+    public void testProvidedScreenshot_nullBitmap() {
+        ScreenshotRequest.Builder inBuilder =
+                new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+                        .setTopComponent(mComponentName)
+                        .setTaskId(2)
+                        .setUserId(3)
+                        .setBoundsOnScreen(new Rect(10, 10, 60, 60))
+                        .setInsets(Insets.of(2, 3, 4, 5));
+
+        assertThrows(IllegalStateException.class, inBuilder::build);
+    }
+
+    @Test
+    public void testFullScreenshot_withBitmap() {
+        // A bitmap added to a FULLSCREEN request will be ignored, but it's technically valid
+        Bitmap bitmap = makeHardwareBitmap(50, 50);
+        ScreenshotRequest in =
+                new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
+                        .setBitmap(bitmap)
+                        .build();
+
+        Parcel parcel = Parcel.obtain();
+        in.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
+
+        assertEquals(TAKE_SCREENSHOT_FULLSCREEN, out.getType());
+        assertEquals(SCREENSHOT_OTHER, out.getSource());
+        assertNull(out.getTopComponent());
+        assertEquals(INVALID_TASK_ID, out.getTaskId());
+        assertEquals(USER_NULL, out.getUserId());
+        assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
+        assertNull("Bounds expected to be null", out.getBoundsInScreen());
+        assertEquals(Insets.NONE, out.getInsets());
+    }
+
+    @Test
+    public void testInvalidType() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ScreenshotRequest.Builder(5, 2).build());
+    }
+
+    private Bitmap makeHardwareBitmap(int width, int height) {
+        HardwareBuffer buffer = HardwareBuffer.create(
+                width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+        return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
+    }
+}
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/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index ff42fb5..b12c5a7 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -306,6 +306,7 @@
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
         <!-- Permission required for UiModeManager CTS test -->
         <permission name="android.permission.READ_PROJECTION_STATE"/>
+        <permission name="android.permission.READ_WALLPAPER_INTERNAL"/>
         <permission name="android.permission.READ_WIFI_CREDENTIAL"/>
         <permission name="android.permission.REAL_GET_TASKS"/>
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f47d9c6..1cf819a 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3331,6 +3331,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
+    "1015746067": {
+      "message": "Display id=%d is ignoring orientation request for %d, return %d following a per-app override for %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "1022095595": {
       "message": "TaskFragment info changed name=%s",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/Color.java b/graphics/java/android/graphics/Color.java
index 87a8053..0f2f879 100644
--- a/graphics/java/android/graphics/Color.java
+++ b/graphics/java/android/graphics/Color.java
@@ -767,7 +767,7 @@
      * Returns the alpha component encoded in the specified color long.
      * The returned value is always in the range \([0..1]\).
      *
-     * @param color The color long whose blue channel to extract
+     * @param color The color long whose alpha channel to extract
      * @return A float value in the range \([0..1]\)
      *
      * @see #colorSpace(long)
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index 14dc6a2..6b1cf8b 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -23,7 +23,6 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.animation.ValueAnimator;
-import android.annotation.ColorInt;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -983,9 +982,9 @@
         RippleShader shader = new RippleShader();
         // Grab the color for the current state and cut the alpha channel in
         // half so that the ripple and background together yield full alpha.
-        final int color = clampAlpha(mMaskColorFilter == null
+        final int color = mMaskColorFilter == null
                 ? mState.mColor.getColorForState(getState(), Color.BLACK)
-                : mMaskColorFilter.getColor());
+                : mMaskColorFilter.getColor();
         final int effectColor = mState.mEffectColor.getColorForState(getState(), Color.MAGENTA);
         final float noisePhase = AnimationUtils.currentAnimationTimeMillis();
         shader.setColor(color, effectColor);
@@ -1008,13 +1007,6 @@
         return properties;
     }
 
-    private int clampAlpha(@ColorInt int color) {
-        if (Color.alpha(color) < 128) {
-            return  (color & 0x00FFFFFF) | 0x80000000;
-        }
-        return color;
-    }
-
     @Override
     public void invalidateSelf() {
         invalidateSelf(true);
@@ -1229,7 +1221,7 @@
 
         // Grab the color for the current state and cut the alpha channel in
         // half so that the ripple and background together yield full alpha.
-        final int color = clampAlpha(mState.mColor.getColorForState(getState(), Color.BLACK));
+        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
         final Paint p = mRipplePaint;
 
         if (mMaskColorFilter != null) {
diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp
index dc4b563..a5b192c 100644
--- a/libs/WindowManager/Jetpack/Android.bp
+++ b/libs/WindowManager/Jetpack/Android.bp
@@ -63,6 +63,12 @@
     sdk_version: "current",
 }
 
+android_library_import {
+    name: "window-extensions-core",
+    aars: ["window-extensions-core-release.aar"],
+    sdk_version: "current",
+}
+
 java_library {
     name: "androidx.window.extensions",
     srcs: [
@@ -70,7 +76,10 @@
         "src/androidx/window/util/**/*.java",
         "src/androidx/window/common/**/*.java",
     ],
-    static_libs: ["window-extensions"],
+    static_libs: [
+        "window-extensions",
+        "window-extensions-core",
+    ],
     installable: true,
     sdk_version: "core_platform",
     system_ext_specific: true,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 54edd9e..666b472 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -48,7 +48,7 @@
     // TODO(b/241126279) Introduce constants to better version functionality
     @Override
     public int getVendorApiLevel() {
-        return 1;
+        return 2;
     }
 
     @NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 3adae70..ff5f256 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -25,12 +25,12 @@
 import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 
 import java.util.concurrent.Executor;
-import java.util.function.Consumer;
 
 /**
  * Reference implementation of androidx.window.extensions.area OEM interface for use with
@@ -252,4 +252,37 @@
             }
         }
     }
+
+    @Override
+    public void addRearDisplayPresentationStatusListener(
+            @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+        throw new UnsupportedOperationException(
+                "addRearDisplayPresentationStatusListener is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public void removeRearDisplayPresentationStatusListener(
+            @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+        throw new UnsupportedOperationException(
+                "removeRearDisplayPresentationStatusListener is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public void startRearDisplayPresentationSession(@NonNull Activity activity,
+            @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
+        throw new UnsupportedOperationException(
+                "startRearDisplayPresentationSession is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public void endRearDisplayPresentationSession() {
+        throw new UnsupportedOperationException(
+                "endRearDisplayPresentationSession is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
+        throw new UnsupportedOperationException(
+                "getRearDisplayPresentation is not supported in API_VERSION=2");
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 87fa63d..00e13c9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -191,10 +191,25 @@
      */
     void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
             @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
+        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
+                null /* pairedActivityToken */);
+    }
+
+    /**
+     * @param ownerToken The token of the activity that creates this task fragment. It does not
+     *                   have to be a child of this task fragment, but must belong to the same task.
+     * @param pairedActivityToken The token of the activity that will be reparented to this task
+     *                            fragment. When it is not {@code null}, the task fragment will be
+     *                            positioned right above it.
+     */
+    void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+            @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode,
+            @Nullable IBinder pairedActivityToken) {
         final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
                 getOrganizerToken(), fragmentToken, ownerToken)
                 .setInitialBounds(bounds)
                 .setWindowingMode(windowingMode)
+                .setPairedActivityToken(pairedActivityToken)
                 .build();
         createTaskFragment(wct, fragmentOptions);
     }
@@ -216,8 +231,10 @@
     private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
             @WindowingMode int windowingMode, @NonNull Activity activity) {
-        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
-        wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
+        final IBinder reparentActivityToken = activity.getActivityToken();
+        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
+                reparentActivityToken);
+        wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
     }
 
     void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 1cd3ea5..569eb80 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -77,6 +77,9 @@
 import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.extensions.WindowExtensionsImpl;
+import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.core.util.function.Function;
 import androidx.window.extensions.embedding.TransactionManager.TransactionRecord;
 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
 
@@ -87,7 +90,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
-import java.util.function.Consumer;
 
 /**
  * Main controller class that manages split states and presentation.
@@ -113,7 +115,7 @@
     /**
      * A developer-defined {@link SplitAttributes} calculator to compute the current
      * {@link SplitAttributes} with the current device and window states.
-     * It is registered via {@link #setSplitAttributesCalculator(SplitAttributesCalculator)}
+     * It is registered via {@link #setSplitAttributesCalculator(Function)}
      * and unregistered via {@link #clearSplitAttributesCalculator()}.
      * This is called when:
      * <ul>
@@ -126,7 +128,7 @@
      */
     @GuardedBy("mLock")
     @Nullable
-    private SplitAttributesCalculator mSplitAttributesCalculator;
+    private Function<SplitAttributesCalculatorParams, SplitAttributes> mSplitAttributesCalculator;
 
     /**
      * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info
@@ -139,6 +141,7 @@
     final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();
 
     /** Callback to Jetpack to notify about changes to split states. */
+    @GuardedBy("mLock")
     @Nullable
     private Consumer<List<SplitInfo>> mEmbeddingCallback;
     private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
@@ -164,7 +167,8 @@
         foldingFeatureProducer.addDataChangedCallback(new FoldingFeatureListener());
     }
 
-    private class FoldingFeatureListener implements Consumer<List<CommonFoldingFeature>> {
+    private class FoldingFeatureListener
+            implements java.util.function.Consumer<List<CommonFoldingFeature>> {
         @Override
         public void accept(List<CommonFoldingFeature> foldingFeatures) {
             synchronized (mLock) {
@@ -205,7 +209,8 @@
     }
 
     @Override
-    public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) {
+    public void setSplitAttributesCalculator(
+            @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator) {
         synchronized (mLock) {
             mSplitAttributesCalculator = calculator;
         }
@@ -220,7 +225,7 @@
 
     @GuardedBy("mLock")
     @Nullable
-    SplitAttributesCalculator getSplitAttributesCalculator() {
+    Function<SplitAttributesCalculatorParams, SplitAttributes> getSplitAttributesCalculator() {
         return mSplitAttributesCalculator;
     }
 
@@ -233,9 +238,22 @@
 
     /**
      * Registers the split organizer callback to notify about changes to active splits.
+     * @deprecated Use {@link #setSplitInfoCallback(Consumer)} starting with
+     * {@link WindowExtensionsImpl#getVendorApiLevel()} 2.
      */
+    @Deprecated
     @Override
-    public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) {
+    public void setSplitInfoCallback(
+            @NonNull java.util.function.Consumer<List<SplitInfo>> callback) {
+        Consumer<List<SplitInfo>> oemConsumer = callback::accept;
+        setSplitInfoCallback(oemConsumer);
+    }
+
+    /**
+     * Registers the split organizer callback to notify about changes to active splits.
+     * @since {@link WindowExtensionsImpl#getVendorApiLevel()} 2
+     */
+    public void setSplitInfoCallback(Consumer<List<SplitInfo>> callback) {
         synchronized (mLock) {
             mEmbeddingCallback = callback;
             updateCallbackIfNecessary();
@@ -1481,7 +1499,7 @@
      * Returns the active split that has the provided containers as primary and secondary or as
      * secondary and primary, if available.
      */
-    @VisibleForTesting
+    @GuardedBy("mLock")
     @Nullable
     SplitContainer getActiveSplitForContainers(
             @NonNull TaskFragmentContainer firstContainer,
@@ -2138,4 +2156,30 @@
         return configuration != null
                 && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED;
     }
+
+    @Override
+    public ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options,
+            @NonNull IBinder token) {
+        throw new UnsupportedOperationException(
+                "setLaunchingActivityStack is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) {
+        throw new UnsupportedOperationException(
+                "finishActivityStacks is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public void invalidateTopVisibleSplitAttributes() {
+        throw new UnsupportedOperationException(
+                "invalidateTopVisibleSplitAttributes is not supported in API_VERSION=2");
+    }
+
+    @Override
+    public void updateSplitAttributes(@NonNull IBinder splitInfoToken,
+            @NonNull SplitAttributes splitAttributes) {
+        throw new UnsupportedOperationException(
+                "updateSplitAttributes is not supported in API_VERSION=2");
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 14d244b..668a7d5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -43,11 +43,11 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Function;
 import androidx.window.extensions.embedding.SplitAttributes.SplitType;
 import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
 import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType;
 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType;
-import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams;
 import androidx.window.extensions.embedding.TaskContainer.TaskProperties;
 import androidx.window.extensions.layout.DisplayFeature;
 import androidx.window.extensions.layout.FoldingFeature;
@@ -268,10 +268,11 @@
             container = mController.newContainer(activity, taskId);
             final int windowingMode = mController.getTaskContainer(taskId)
                     .getWindowingModeForSplitTaskFragment(bounds);
-            createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(),
-                    bounds, windowingMode);
+            final IBinder reparentActivityToken = activity.getActivityToken();
+            createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken,
+                    bounds, windowingMode, reparentActivityToken);
             wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
-                    activity.getActivityToken());
+                    reparentActivityToken);
         } else {
             resizeTaskFragmentIfRegistered(wct, container, bounds);
             final int windowingMode = mController.getTaskContainer(taskId)
@@ -551,11 +552,12 @@
             @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
         final Configuration taskConfiguration = taskProperties.getConfiguration();
         final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
-        final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator();
+        final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
+                mController.getSplitAttributesCalculator();
         final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
-        final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics);
+        final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
         if (calculator == null) {
-            if (!isDefaultMinSizeSatisfied) {
+            if (!areDefaultConstraintsSatisfied) {
                 return EXPAND_CONTAINERS_ATTRIBUTES;
             }
             return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes,
@@ -565,9 +567,9 @@
                 .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
                         taskConfiguration.windowConfiguration);
         final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams(
-                taskWindowMetrics, taskConfiguration, defaultSplitAttributes,
-                isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag());
-        final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params);
+                taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes,
+                areDefaultConstraintsSatisfied, rule.getTag());
+        final SplitAttributes splitAttributes = calculator.apply(params);
         return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair);
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 076856c..17814c6 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -141,12 +141,26 @@
         mToken = new Binder("TaskFragmentContainer");
         mTaskContainer = taskContainer;
         if (pairedPrimaryContainer != null) {
+            // The TaskFragment will be positioned right above the paired container.
             if (pairedPrimaryContainer.getTaskContainer() != taskContainer) {
                 throw new IllegalArgumentException(
                         "pairedPrimaryContainer must be in the same Task");
             }
             final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer);
             taskContainer.mContainers.add(primaryIndex + 1, this);
+        } else if (pendingAppearedActivity != null) {
+            // The TaskFragment will be positioned right above the pending appeared Activity. If any
+            // existing TaskFragment is empty with pending Intent, it is likely that the Activity of
+            // the pending Intent hasn't been created yet, so the new Activity should be below the
+            // empty TaskFragment.
+            int i = taskContainer.mContainers.size() - 1;
+            for (; i >= 0; i--) {
+                final TaskFragmentContainer container = taskContainer.mContainers.get(i);
+                if (!container.isEmpty() || container.getPendingAppearedIntent() == null) {
+                    break;
+                }
+            }
+            taskContainer.mContainers.add(i + 1, this);
         } else {
             taskContainer.mContainers.add(this);
         }
@@ -500,6 +514,8 @@
         }
 
         if (!shouldFinishDependent) {
+            // Always finish the placeholder when the primary is finished.
+            finishPlaceholderIfAny(wct, presenter);
             return;
         }
 
@@ -526,6 +542,28 @@
         mActivitiesToFinishOnExit.clear();
     }
 
+    @GuardedBy("mController.mLock")
+    private void finishPlaceholderIfAny(@NonNull WindowContainerTransaction wct,
+            @NonNull SplitPresenter presenter) {
+        final List<TaskFragmentContainer> containersToRemove = new ArrayList<>();
+        for (TaskFragmentContainer container : mContainersToFinishOnExit) {
+            if (container.mIsFinished) {
+                continue;
+            }
+            final SplitContainer splitContainer = mController.getActiveSplitForContainers(
+                    this, container);
+            if (splitContainer != null && splitContainer.isPlaceholderContainer()
+                    && splitContainer.getSecondaryContainer() == container) {
+                // Remove the placeholder secondary TaskFragment.
+                containersToRemove.add(container);
+            }
+        }
+        mContainersToFinishOnExit.removeAll(containersToRemove);
+        for (TaskFragmentContainer container : containersToRemove) {
+            container.finish(false /* shouldFinishDependent */, presenter, wct, mController);
+        }
+    }
+
     boolean isFinished() {
         return mIsFinished;
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index c9f8700..8386131 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -45,6 +45,7 @@
 import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.extensions.core.util.function.Consumer;
 import androidx.window.util.DataProducer;
 
 import java.util.ArrayList;
@@ -53,7 +54,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Consumer;
 
 /**
  * Reference implementation of androidx.window.extensions.layout OEM interface for use with
@@ -82,6 +82,10 @@
     private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners =
             new ArrayMap<>();
 
+    @GuardedBy("mLock")
+    private final Map<java.util.function.Consumer<WindowLayoutInfo>, Consumer<WindowLayoutInfo>>
+            mJavaToExtConsumers = new ArrayMap<>();
+
     private final TaskFragmentOrganizer mTaskFragmentOrganizer;
 
     public WindowLayoutComponentImpl(@NonNull Context context,
@@ -95,7 +99,8 @@
     }
 
     /** Registers to listen to {@link CommonFoldingFeature} changes */
-    public void addFoldingStateChangedCallback(Consumer<List<CommonFoldingFeature>> consumer) {
+    public void addFoldingStateChangedCallback(
+            java.util.function.Consumer<List<CommonFoldingFeature>> consumer) {
         synchronized (mLock) {
             mFoldingFeatureProducer.addDataChangedCallback(consumer);
         }
@@ -109,13 +114,27 @@
      */
     @Override
     public void addWindowLayoutInfoListener(@NonNull Activity activity,
-            @NonNull Consumer<WindowLayoutInfo> consumer) {
-        addWindowLayoutInfoListener((Context) activity, consumer);
+            @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
+        final Consumer<WindowLayoutInfo> extConsumer = consumer::accept;
+        synchronized (mLock) {
+            mJavaToExtConsumers.put(consumer, extConsumer);
+        }
+        addWindowLayoutInfoListener(activity, extConsumer);
+    }
+
+    @Override
+    public void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
+            @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
+        final Consumer<WindowLayoutInfo> extConsumer = consumer::accept;
+        synchronized (mLock) {
+            mJavaToExtConsumers.put(consumer, extConsumer);
+        }
+        addWindowLayoutInfoListener(context, extConsumer);
     }
 
     /**
-     * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context
-     * as a parameter.
+     * Similar to {@link #addWindowLayoutInfoListener(Activity, java.util.function.Consumer)}, but
+     * takes a UI Context as a parameter.
      *
      * Jetpack {@link androidx.window.layout.ExtensionWindowLayoutInfoBackend} makes sure all
      * consumers related to the same {@link Context} gets updated {@link WindowLayoutInfo}
@@ -156,6 +175,18 @@
         }
     }
 
+    @Override
+    public void removeWindowLayoutInfoListener(
+            @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
+        final Consumer<WindowLayoutInfo> extConsumer;
+        synchronized (mLock) {
+            extConsumer = mJavaToExtConsumers.remove(consumer);
+        }
+        if (extConsumer != null) {
+            removeWindowLayoutInfoListener(extConsumer);
+        }
+    }
+
     /**
      * Removes a listener no longer interested in receiving updates.
      *
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index 2f92a57..459ec9f 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -34,9 +34,11 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Pair;
+import android.view.WindowMetrics;
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerToken;
 
+import androidx.window.extensions.core.util.function.Predicate;
 import androidx.window.extensions.embedding.SplitAttributes.SplitType;
 import androidx.window.extensions.layout.DisplayFeature;
 import androidx.window.extensions.layout.FoldingFeature;
@@ -107,7 +109,7 @@
     static SplitRule createSplitRule(@NonNull Activity primaryActivity,
             @NonNull Intent secondaryIntent, boolean clearTop) {
         final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
-        return new SplitPairRule.Builder(
+        return createSplitPairRuleBuilder(
                 activityPair -> false,
                 targetPair::equals,
                 w -> true)
@@ -144,7 +146,7 @@
             @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary,
             int finishSecondaryWithPrimary, boolean clearTop) {
         final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity);
-        return new SplitPairRule.Builder(
+        return createSplitPairRuleBuilder(
                 targetPair::equals,
                 activityIntentPair -> false,
                 w -> true)
@@ -223,4 +225,26 @@
         displayFeatures.add(foldingFeature);
         return new WindowLayoutInfo(displayFeatures);
     }
+
+    static ActivityRule.Builder createActivityBuilder(
+            @NonNull Predicate<Activity> activityPredicate,
+            @NonNull Predicate<Intent> intentPredicate) {
+        return new ActivityRule.Builder(activityPredicate, intentPredicate);
+    }
+
+    static SplitPairRule.Builder createSplitPairRuleBuilder(
+            @NonNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate,
+            @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate,
+            @NonNull Predicate<WindowMetrics> windowMetricsPredicate) {
+        return new SplitPairRule.Builder(activitiesPairPredicate, activityIntentPairPredicate,
+                windowMetricsPredicate);
+    }
+
+    static SplitPlaceholderRule.Builder createSplitPlaceholderRuleBuilder(
+            @NonNull Intent placeholderIntent, @NonNull Predicate<Activity> activityPredicate,
+            @NonNull Predicate<Intent> intentPredicate,
+            @NonNull Predicate<WindowMetrics> windowMetricsPredicate) {
+        return new SplitPlaceholderRule.Builder(placeholderIntent, activityPredicate,
+                intentPredicate, windowMetricsPredicate);
+    }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 81c3957..0bf0bc8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -34,8 +34,11 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityBuilder;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
@@ -432,7 +435,7 @@
     @Test
     public void testResolveStartActivityIntent_withoutLaunchingActivity() {
         final Intent intent = new Intent();
-        final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent)
+        final ActivityRule expandRule = createActivityBuilder(r -> false, i -> i == intent)
                 .setShouldAlwaysExpand(true)
                 .build();
         mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
@@ -1170,7 +1173,7 @@
 
     @Test
     public void testHasSamePresentation() {
-        SplitPairRule splitRule1 = new SplitPairRule.Builder(
+        SplitPairRule splitRule1 = createSplitPairRuleBuilder(
                 activityPair -> true,
                 activityIntentPair -> true,
                 windowMetrics -> true)
@@ -1178,7 +1181,7 @@
                 .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
                 .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
                 .build();
-        SplitPairRule splitRule2 = new SplitPairRule.Builder(
+        SplitPairRule splitRule2 = createSplitPairRuleBuilder(
                 activityPair -> true,
                 activityIntentPair -> true,
                 windowMetrics -> true)
@@ -1191,7 +1194,7 @@
                 SplitController.haveSamePresentation(splitRule1, splitRule2,
                         new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
 
-        splitRule2 = new SplitPairRule.Builder(
+        splitRule2 = createSplitPairRuleBuilder(
                 activityPair -> true,
                 activityIntentPair -> true,
                 windowMetrics -> true)
@@ -1355,7 +1358,7 @@
 
     /** Setups a rule to always expand the given intent. */
     private void setupExpandRule(@NonNull Intent expandIntent) {
-        final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals)
+        final ActivityRule expandRule = createActivityBuilder(r -> false, expandIntent::equals)
                 .setShouldAlwaysExpand(true)
                 .build();
         mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
@@ -1363,7 +1366,7 @@
 
     /** Setups a rule to always expand the given activity. */
     private void setupExpandRule(@NonNull Activity expandActivity) {
-        final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false)
+        final ActivityRule expandRule = createActivityBuilder(expandActivity::equals, i -> false)
                 .setShouldAlwaysExpand(true)
                 .build();
         mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
@@ -1371,7 +1374,7 @@
 
     /** Setups a rule to launch placeholder for the given activity. */
     private void setupPlaceholderRule(@NonNull Activity primaryActivity) {
-        final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT,
+        final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT,
                 primaryActivity::equals, i -> false, w -> true)
                 .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
                 .build();
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 121e813..a288fd6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -28,6 +28,7 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
@@ -76,6 +77,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
+import androidx.window.extensions.core.util.function.Function;
 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
 import androidx.window.extensions.layout.WindowLayoutInfo;
 
@@ -511,7 +513,7 @@
         final Activity secondaryActivity = createMockActivity();
         final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID);
         final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
-        final SplitPairRule rule = new SplitPairRule.Builder(pair ->
+        final SplitPairRule rule = createSplitPairRuleBuilder(pair ->
                 pair.first == mActivity && pair.second == secondaryActivity, pair -> false,
                 metrics -> true)
                 .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
@@ -529,7 +531,7 @@
 
     @Test
     public void testComputeSplitAttributes() {
-        final SplitPairRule splitPairRule = new SplitPairRule.Builder(
+        final SplitPairRule splitPairRule = createSplitPairRuleBuilder(
                 activityPair -> true,
                 activityIntentPair -> true,
                 windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS))
@@ -561,10 +563,10 @@
                                 SplitAttributes.SplitType.RatioSplitType.splitEqually()
                         )
                 ).build();
+        final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
+                params -> splitAttributes;
 
-        mController.setSplitAttributesCalculator(params -> {
-            return splitAttributes;
-        });
+        mController.setSplitAttributesCalculator(calculator);
 
         assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties,
                 splitPairRule, null /* minDimensionsPair */));
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 7d9d8b0..78b85e6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -154,17 +154,52 @@
                 null /* pendingAppearedIntent */, taskContainer, mController,
                 null /* pairedPrimaryContainer */);
         doReturn(container1).when(mController).getContainerWithActivity(mActivity);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
 
         // The activity is requested to be reparented, so don't finish it.
-        container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        container0.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
         verify(mTransaction, never()).finishActivity(any());
-        verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken());
+        verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken());
         verify(mController).removeContainer(container0);
     }
 
     @Test
+    public void testFinish_alwaysFinishPlaceholder() {
+        // Register container1 as a placeholder
+        final TaskContainer taskContainer = createTestTaskContainer();
+        final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
+                null /* pendingAppearedIntent */, taskContainer, mController,
+                null /* pairedPrimaryContainer */);
+        final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity);
+        container0.setInfo(mTransaction, info0);
+        final Activity placeholderActivity = createMockActivity();
+        final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity,
+                null /* pendingAppearedIntent */, taskContainer, mController,
+                null /* pairedPrimaryContainer */);
+        final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity);
+        container1.setInfo(mTransaction, info1);
+        final SplitAttributes splitAttributes = new SplitAttributes.Builder().build();
+        final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(new Intent(),
+                mActivity::equals, (java.util.function.Predicate) i -> false,
+                (java.util.function.Predicate) w -> true)
+                .setDefaultSplitAttributes(splitAttributes)
+                .build();
+        mController.registerSplit(mTransaction, container0, mActivity, container1, rule,
+                splitAttributes);
+
+        // The placeholder TaskFragment should be finished even if the primary is finished with
+        // shouldFinishDependent = false.
+        container0.finish(false /* shouldFinishDependent */, mPresenter, mTransaction, mController);
+
+        assertTrue(container0.isFinished());
+        assertTrue(container1.isFinished());
+        verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken());
+        verify(mPresenter).deleteTaskFragment(mTransaction, container1.getTaskFragmentToken());
+        verify(mController).removeContainer(container0);
+        verify(mController).removeContainer(container1);
+    }
+
+    @Test
     public void testSetInfo() {
         final TaskContainer taskContainer = createTestTaskContainer();
         // Pending activity should be cleared when it has appeared on server side.
@@ -493,8 +528,6 @@
         final TaskFragmentContainer tf1 = new TaskFragmentContainer(
                 null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
                 null /* pairedPrimaryTaskFragment */);
-        taskContainer.mContainers.add(tf0);
-        taskContainer.mContainers.add(tf1);
 
         // When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted
         // right above tf0.
@@ -506,6 +539,26 @@
     }
 
     @Test
+    public void testNewContainerWithPairedPendingAppearedActivity() {
+        final TaskContainer taskContainer = createTestTaskContainer();
+        final TaskFragmentContainer tf0 = new TaskFragmentContainer(
+                createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
+                null /* pairedPrimaryTaskFragment */);
+        final TaskFragmentContainer tf1 = new TaskFragmentContainer(
+                null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
+                null /* pairedPrimaryTaskFragment */);
+
+        // When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any
+        // TaskFragment without any Activity.
+        final TaskFragmentContainer tf2 = new TaskFragmentContainer(
+                createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
+                null /* pairedPrimaryTaskFragment */);
+        assertEquals(0, taskContainer.indexOf(tf0));
+        assertEquals(1, taskContainer.indexOf(tf2));
+        assertEquals(2, taskContainer.indexOf(tf1));
+    }
+
+    @Test
     public void testIsVisible() {
         final TaskContainer taskContainer = createTestTaskContainer();
         final TaskFragmentContainer container = new TaskFragmentContainer(
diff --git a/libs/WindowManager/Jetpack/window-extensions-core-release.aar b/libs/WindowManager/Jetpack/window-extensions-core-release.aar
new file mode 100644
index 0000000..96ff840
--- /dev/null
+++ b/libs/WindowManager/Jetpack/window-extensions-core-release.aar
Binary files differ
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 84ab448..c3b6916 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index f615ad6..c7c9424 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -47,7 +47,9 @@
         "src/com/android/wm/shell/sysui/ShellSharedConstants.java",
         "src/com/android/wm/shell/common/TransactionPool.java",
         "src/com/android/wm/shell/animation/Interpolators.java",
+        "src/com/android/wm/shell/pip/PipContentOverlay.java",
         "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java",
+        "src/com/android/wm/shell/draganddrop/DragAndDropConstants.java",
     ],
     path: "src",
 }
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml
similarity index 69%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml
index 0d88113..a3ca74f 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ 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.
@@ -14,8 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@android:color/system_neutral1_900" android:alpha="0.6" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml
similarity index 69%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml
index 0d88113..a3ca74f 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ 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.
@@ -14,8 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@android:color/system_neutral1_900" android:alpha="0.6" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/caption_close_button.xml b/libs/WindowManager/Shell/res/drawable/caption_close_button.xml
new file mode 100644
index 0000000..e258564
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_close_button.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateY="4.0">
+        <path
+            android:fillColor="#FFFF0000"
+            android:pathData="M12.45,38.35 L9.65,35.55 21.2,24 9.65,12.45 12.45,9.65 24,21.2 35.55,9.65 38.35,12.45 26.8,24 38.35,35.55 35.55,38.35 24,26.8Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_collapse_menu_button.xml b/libs/WindowManager/Shell/res/drawable/caption_collapse_menu_button.xml
new file mode 100644
index 0000000..166552d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_collapse_menu_button.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+>
+    <group android:scaleX="1.25"
+           android:scaleY="1.75"
+           android:translateY="6.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M10.3937 6.93935L11.3337 5.99935L6.00033 0.666016L0.666992 5.99935L1.60699 6.93935L6.00033 2.55268"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml
similarity index 68%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to libs/WindowManager/Shell/res/drawable/caption_decor_title.xml
index 0d88113..6114ad6 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ 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.
@@ -14,8 +14,9 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+<shape android:shape="rectangle"
+       android:tintMode="multiply"
+       android:tint="@color/decor_title_color"
+       xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/white" />
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_screenshot_button.xml b/libs/WindowManager/Shell/res/drawable/caption_screenshot_button.xml
new file mode 100644
index 0000000..7c86888
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_screenshot_button.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateY="4.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M10,38V28.35H13V35H19.65V38ZM10,19.65V10H19.65V13H13V19.65ZM28.35,38V35H35V28.35H38V38ZM35,19.65V13H28.35V10H38V19.65Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_select_button.xml b/libs/WindowManager/Shell/res/drawable/caption_select_button.xml
new file mode 100644
index 0000000..8c60c84
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_select_button.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group
+           android:translateX="4.0"
+           android:translateY="6.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M13.7021 12.5833L16.5676 15.5L15.426 16.7333L12.526 13.8333L10.4426 15.9167V10.5H15.9176L13.7021 12.5833ZM13.8343 3.83333H15.501V5.5H13.8343V3.83333ZM15.501 2.16667H13.8343V0.566667C14.751 0.566667 15.501 1.33333 15.501 2.16667ZM10.501 0.5H12.1676V2.16667H10.501V0.5ZM13.8343 7.16667H15.501V8.83333H13.8343V7.16667ZM5.50098 15.5H3.83431V13.8333H5.50098V15.5ZM2.16764 5.5H0.500977V3.83333H2.16764V5.5ZM2.16764 0.566667V2.16667H0.500977C0.500977 1.33333 1.33431 0.566667 2.16764 0.566667ZM2.16764 12.1667H0.500977V10.5H2.16764V12.1667ZM5.50098 2.16667H3.83431V0.5H5.50098V2.16667ZM8.83431 2.16667H7.16764V0.5H8.83431V2.16667ZM8.83431 15.5H7.16764V13.8333H8.83431V15.5ZM2.16764 8.83333H0.500977V7.16667H2.16764V8.83333ZM2.16764 15.5667C1.25098 15.5667 0.500977 14.6667 0.500977 13.8333H2.16764V15.5667Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
index c9f2623..27e0b18 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
@@ -17,9 +17,10 @@
         android:width="24dp"
         android:height="24dp"
         android:viewportWidth="24"
-        android:viewportHeight="24">
+        android:viewportHeight="24"
+        android:tint="@color/decor_button_dark_color">
     <group android:translateY="8.0">
         <path
-            android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/>
+            android:fillColor="@android:color/white" android:pathData="M3,5V3H21V5Z"/>
     </group>
 </vector>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
index 0bcaa53..91edbf1 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
@@ -14,11 +14,11 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24"
-        android:viewportHeight="24"
-        android:tint="?attr/colorControlNormal">
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+        android:tint="@color/decor_button_dark_color">
     <path
         android:fillColor="@android:color/white" android:pathData="M6,21V19H18V21Z"/>
 </vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
index 416287d..9167382 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
@@ -18,4 +18,5 @@
        xmlns:android="http://schemas.android.com/apk/res/android">
     <solid android:color="@android:color/white" />
     <corners android:radius="20dp" />
+    <stroke android:width="1dp" android:color="#b3b3b3"/>
 </shape>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
index 416287d..ef30060 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
@@ -15,7 +15,8 @@
   ~ limitations under the License.
   -->
 <shape android:shape="rectangle"
+       android:tintMode="multiply"
+       android:tint="@color/decor_title_color"
        xmlns:android="http://schemas.android.com/apk/res/android">
     <solid android:color="@android:color/white" />
-    <corners android:radius="20dp" />
 </shape>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml
index 42572d6..a269968 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml
@@ -14,7 +14,30 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/letterbox_education_dismiss_button_background_ripple">
-    <item android:drawable="@drawable/letterbox_education_dismiss_button_background"/>
-</ripple>
\ No newline at end of file
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       android:insetTop="@dimen/letterbox_education_dialog_vertical_inset"
+       android:insetBottom="@dimen/letterbox_education_dialog_vertical_inset">
+    <ripple android:color="@color/letterbox_education_dismiss_button_background_ripple">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <corners android:radius="@dimen/letterbox_education_dialog_button_radius"/>
+                <solid android:color="@android:color/white"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/transparent"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <solid android:color="@color/letterbox_education_accent_primary"/>
+                <corners android:radius="@dimen/letterbox_education_dialog_button_radius"/>
+                <padding android:left="@dimen/letterbox_education_dialog_horizontal_padding"
+                         android:top="@dimen/letterbox_education_dialog_vertical_padding"
+                         android:right="@dimen/letterbox_education_dialog_horizontal_padding"
+                         android:bottom="@dimen/letterbox_education_dialog_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml
new file mode 100644
index 0000000..1f12514
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml
@@ -0,0 +1,44 @@
+<?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.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:insetTop="@dimen/letterbox_restart_dialog_vertical_inset"
+       android:insetBottom="@dimen/letterbox_restart_dialog_vertical_inset">
+    <ripple android:color="@color/letterbox_restart_dismiss_button_background_ripple">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/>
+                <solid android:color="@android:color/white"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/transparent"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <solid android:color="?androidprv:attr/colorAccentPrimaryVariant"/>
+                <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/>
+                <padding android:left="@dimen/letterbox_restart_dialog_horizontal_padding"
+                         android:top="@dimen/letterbox_restart_dialog_vertical_padding"
+                         android:right="@dimen/letterbox_restart_dialog_horizontal_padding"
+                         android:bottom="@dimen/letterbox_restart_dialog_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml
new file mode 100644
index 0000000..c247c6e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:state_checked="true"
+          android:drawable="@drawable/letterbox_restart_checkbox_checked" />
+    <item android:state_pressed="true"
+          android:drawable="@drawable/letterbox_restart_checkbox_checked" />
+    <item android:state_pressed="false"
+          android:drawable="@drawable/letterbox_restart_checkbox_unchecked" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml
new file mode 100644
index 0000000..4f97e2c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="20dp"
+        android:height="20dp"
+        android:viewportWidth="20"
+        android:viewportHeight="20"
+        android:tint="?android:attr/textColorSecondary">
+    <group
+        android:scaleX="0.83333333333"
+        android:scaleY="0.83333333333"
+        android:translateX="0"
+        android:translateY="0">
+        <path
+            android:fillColor="?android:attr/textColorSecondary"
+            android:pathData="M10.6,16.2 L17.65,9.15 16.25,7.75 10.6,13.4 7.75,10.55 6.35,11.95ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21Z"/>
+    </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml
new file mode 100644
index 0000000..bb14d19
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="20dp"
+        android:height="20dp"
+        android:viewportWidth="20"
+        android:viewportHeight="20"
+        android:tint="?android:attr/textColorSecondary">
+    <group
+        android:scaleX="0.83333333333"
+        android:scaleY="0.83333333333"
+        android:translateX="0"
+        android:translateY="0">
+        <path
+            android:fillColor="?android:attr/textColorSecondary"
+            android:pathData="M5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Z"/>
+    </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml
similarity index 73%
rename from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
rename to libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml
index 0d88113..e3c18a2 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ 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.
@@ -15,7 +15,8 @@
   ~ limitations under the License.
   -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
        android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
+    <solid android:color="?androidprv:attr/colorSurface"/>
+    <corners android:radius="@dimen/letterbox_restart_dialog_corner_radius"/>
 </shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml
new file mode 100644
index 0000000..3aa0981
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml
@@ -0,0 +1,46 @@
+<?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.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:insetTop="@dimen/letterbox_restart_dialog_vertical_inset"
+       android:insetBottom="@dimen/letterbox_restart_dialog_vertical_inset">
+    <ripple android:color="@color/letterbox_restart_dismiss_button_background_ripple">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/>
+                <solid android:color="@android:color/white"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/transparent"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant"
+                        android:width="1dp"/>
+                <solid android:color="?androidprv:attr/colorSurface"/>
+                <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/>
+                <padding android:left="@dimen/letterbox_restart_dialog_horizontal_padding"
+                         android:top="@dimen/letterbox_restart_dialog_vertical_padding"
+                         android:right="@dimen/letterbox_restart_dialog_horizontal_padding"
+                         android:bottom="@dimen/letterbox_restart_dialog_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml
new file mode 100644
index 0000000..5053971
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        android:width="@dimen/letterbox_restart_dialog_title_icon_width"
+        android:height="@dimen/letterbox_restart_dialog_title_icon_height"
+        android:viewportWidth="45"
+        android:viewportHeight="44">
+    <group
+        android:scaleX="0.8"
+        android:scaleY="0.8"
+        android:translateX="8"
+        android:translateY="8">
+        <path
+            android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
+            android:fillColor="?androidprv:attr/colorAccentPrimaryVariant"/>
+    </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml
new file mode 100644
index 0000000..b6e0172
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="@dimen/letterbox_restart_dialog_title_icon_width"
+        android:height="@dimen/letterbox_restart_dialog_title_icon_height"
+        android:viewportWidth="45"
+        android:viewportHeight="44">
+    <group
+        android:scaleX="0.8"
+        android:scaleY="0.8"
+        android:translateX="8"
+        android:translateY="8">
+        <path
+            android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
+            android:fillColor="@color/compat_controls_text"/>
+    </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
index 2994593..b3f8e801 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
@@ -25,12 +25,10 @@
         android:fillAlpha="0.8"
         android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/>
     <group
-        android:scaleX="0.8"
-        android:scaleY="0.8"
-        android:translateX="10"
-        android:translateY="10">
+        android:translateX="12"
+        android:translateY="12">
         <path
-            android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
-            android:fillColor="@color/compat_controls_text"/>
+            android:fillColor="@color/compat_controls_text"
+            android:pathData="M3,21V15H5V17.6L8.1,14.5L9.5,15.9L6.4,19H9V21ZM15,21V19H17.6L14.5,15.9L15.9,14.5L19,17.6V15H21V21ZM8.1,9.5 L5,6.4V9H3V3H9V5H6.4L9.5,8.1ZM15.9,9.5 L14.5,8.1 17.6,5H15V3H21V9H19V6.4Z"/>
     </group>
 </vector>
diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decor.xml b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml
new file mode 100644
index 0000000..f3d2198
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml
@@ -0,0 +1,56 @@
+<?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.
+  -->
+<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/caption"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="end"
+    android:background="@drawable/caption_decor_title">
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/back_button"
+        android:layout_gravity="center_vertical|end"
+        android:contentDescription="@string/back_button_text"
+        android:background="@drawable/decor_back_button_dark"
+        android:duplicateParentState="true"/>
+    <Space
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:elevation="2dp"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/minimize_window"
+        android:layout_gravity="center_vertical|end"
+        android:contentDescription="@string/minimize_button_text"
+        android:background="@drawable/decor_minimize_button_dark"
+        android:duplicateParentState="true"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/maximize_window"
+        android:layout_gravity="center_vertical|end"
+        android:contentDescription="@string/maximize_button_text"
+        android:background="@drawable/decor_maximize_button_dark"
+        android:duplicateParentState="true"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/close_window"
+        android:contentDescription="@string/close_button_text"
+        android:background="@drawable/decor_close_button_dark"
+        android:duplicateParentState="true"/>
+</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml
index 44b2f45..3d3c003 100644
--- a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml
+++ b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml
@@ -29,11 +29,15 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:lineSpacingExtra="4sp"
+        android:letterSpacing="0.02"
         android:background="@drawable/compat_hint_bubble"
         android:padding="16dp"
         android:textAlignment="viewStart"
         android:textColor="@color/compat_controls_text"
-        android:textSize="14sp"/>
+        android:textSize="14sp"
+        android:fontFamily="@*android:string/config_bodyFontFamily"
+        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Subhead"
+    />
 
     <ImageView
         android:layout_width="wrap_content"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
index 8b4792a..f6e3f2e 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
@@ -1,49 +1,129 @@
 <?xml version="1.0" encoding="utf-8"?>
-    <!--
-      ~ Copyright (C) 2022 The Android Open Source Project
-      ~
-      ~ Licensed under the Apache License, Version 2.0 (the "License");
-      ~ you may not use this file except in compliance with the License.
-      ~ You may obtain a copy of the License at
-      ~
-      ~      http://www.apache.org/licenses/LICENSE-2.0
-      ~
-      ~ Unless required by applicable law or agreed to in writing, software
-      ~ distributed under the License is distributed on an "AS IS" BASIS,
-      ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-      ~ See the License for the specific language governing permissions and
-      ~ limitations under the License.
-      -->
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 <com.android.wm.shell.windowdecor.WindowDecorLinearLayout
-xmlns:android="http://schemas.android.com/apk/res/android"
-android:id="@+id/handle_menu"
-android:layout_width="wrap_content"
-android:layout_height="wrap_content"
-android:gravity="center_horizontal"
-android:background="@drawable/desktop_mode_decor_menu_background">
-    <Button
-        style="@style/CaptionButtonStyle"
-        android:id="@+id/fullscreen_button"
-        android:contentDescription="@string/fullscreen_text"
-        android:background="@drawable/caption_fullscreen_button"/>
-    <Button
-        style="@style/CaptionButtonStyle"
-        android:id="@+id/split_screen_button"
-        android:contentDescription="@string/split_screen_text"
-        android:background="@drawable/caption_split_screen_button"/>
-    <Button
-        style="@style/CaptionButtonStyle"
-        android:id="@+id/floating_button"
-        android:contentDescription="@string/float_button_text"
-        android:background="@drawable/caption_floating_button"/>
-    <Button
-        style="@style/CaptionButtonStyle"
-        android:id="@+id/desktop_button"
-        android:contentDescription="@string/desktop_text"
-        android:background="@drawable/caption_desktop_button"/>
-    <Button
-        style="@style/CaptionButtonStyle"
-        android:id="@+id/more_button"
-        android:contentDescription="@string/more_button_text"
-        android:background="@drawable/caption_more_button"/>
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/handle_menu"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@drawable/desktop_mode_decor_menu_background"
+    android:elevation="@dimen/caption_menu_elevation"
+    android:divider="?android:attr/dividerHorizontal"
+    android:showDividers="middle"
+    android:dividerPadding="18dip">
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <ImageView
+            android:id="@+id/application_icon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_margin="12dp"
+            android:contentDescription="@string/app_icon_text"
+            android:layout_alignParentStart="true"
+            android:layout_centerVertical="true"/>
+        <TextView
+            android:id="@+id/application_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_toEndOf="@+id/application_icon"
+            android:layout_toStartOf="@+id/collapse_menu_button"
+            android:textColor="#FF000000"
+            android:layout_centerVertical="true"/>
+        <Button
+            android:id="@+id/collapse_menu_button"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginEnd="10dp"
+            android:contentDescription="@string/collapse_menu_text"
+            android:layout_alignParentEnd="true"
+            android:background="@drawable/caption_collapse_menu_button"
+            android:layout_centerVertical="true"/>
+    </RelativeLayout>
+    <LinearLayout
+        android:id="@+id/windowing_mode_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal">
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="0.5" />
+        <Button
+            style="@style/CaptionWindowingButtonStyle"
+            android:id="@+id/fullscreen_button"
+            android:contentDescription="@string/fullscreen_text"
+            android:background="@drawable/caption_fullscreen_button"/>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+        <Button
+            style="@style/CaptionWindowingButtonStyle"
+            android:id="@+id/split_screen_button"
+            android:contentDescription="@string/split_screen_text"
+            android:background="@drawable/caption_split_screen_button"/>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+        <Button
+            style="@style/CaptionWindowingButtonStyle"
+            android:id="@+id/floating_button"
+            android:contentDescription="@string/float_button_text"
+            android:background="@drawable/caption_floating_button"/>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+        <Button
+            style="@style/CaptionWindowingButtonStyle"
+            android:id="@+id/desktop_button"
+            android:contentDescription="@string/desktop_text"
+            android:background="@drawable/caption_desktop_button"/>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="0.5" />
+
+    </LinearLayout>
+    <LinearLayout
+        android:id="@+id/menu_buttons_misc"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <Button
+            style="@style/CaptionMenuButtonStyle"
+            android:id="@+id/screenshot_button"
+            android:contentDescription="@string/screenshot_text"
+            android:text="@string/screenshot_text"
+            android:drawableStart="@drawable/caption_screenshot_button"/>
+        <Button
+            style="@style/CaptionMenuButtonStyle"
+            android:id="@+id/select_button"
+            android:contentDescription="@string/select_text"
+            android:text="@string/select_text"
+            android:drawableStart="@drawable/caption_select_button"/>
+        <Button
+            style="@style/CaptionMenuButtonStyle"
+            android:id="@+id/close_button"
+            android:contentDescription="@string/close_text"
+            android:text="@string/close_text"
+            android:drawableStart="@drawable/caption_close_button"
+            android:textColor="#FFFF0000"/>
+    </LinearLayout>
 </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
index 2a4cc02..29cf151 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
@@ -17,21 +17,20 @@
 <com.android.wm.shell.windowdecor.WindowDecorLinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/desktop_mode_caption"
-    android:layout_width="wrap_content"
+    android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:gravity="center_horizontal"
     android:background="@drawable/desktop_mode_decor_title">
     <Button
         style="@style/CaptionButtonStyle"
         android:id="@+id/back_button"
         android:contentDescription="@string/back_button_text"
-        android:background="@drawable/decor_back_button_dark"
-    />
+        android:background="@drawable/decor_back_button_dark"/>
     <Button
         android:id="@+id/caption_handle"
         android:layout_width="128dp"
         android:layout_height="32dp"
         android:layout_margin="5dp"
-        android:padding="4dp"
         android:contentDescription="@string/handle_text"
         android:background="@drawable/decor_handle_dark"/>
     <Button
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
index c65f24d..095576b 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
@@ -29,6 +29,8 @@
         android:layout_marginBottom="20dp"/>
 
     <TextView
+        android:fontFamily="@*android:string/config_bodyFontFamily"
+        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"
         android:id="@+id/letterbox_education_dialog_action_text"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
index 3a44eb9..e8edad1 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
@@ -69,6 +69,8 @@
                     android:text="@string/letterbox_education_dialog_title"
                     android:textAlignment="center"
                     android:textColor="@color/compat_controls_text"
+                    android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
+                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline"
                     android:textSize="24sp"/>
 
                 <LinearLayout
@@ -95,10 +97,16 @@
                 </LinearLayout>
 
                 <Button
+                    android:fontFamily="@*android:string/config_bodyFontFamily"
+                    android:fontWeight="500"
+                    android:lineHeight="20dp"
+                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Small"
                     android:id="@+id/letterbox_education_dialog_dismiss_button"
+                    android:textStyle="bold"
                     android:layout_width="match_parent"
                     android:layout_height="56dp"
                     android:layout_marginTop="40dp"
+                    android:textSize="14sp"
                     android:background=
                         "@drawable/letterbox_education_dismiss_button_background_ripple"
                     android:text="@string/letterbox_education_got_it"
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
new file mode 100644
index 0000000..ba9852c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
@@ -0,0 +1,142 @@
+<!--
+  ~ 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.
+  -->
+<com.android.wm.shell.compatui.RestartDialogLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@android:color/system_neutral1_900">
+
+    <!-- The background of the top-level layout acts as the background dim. -->
+
+    <!--TODO (b/266288912): Resolve overdraw warning -->
+
+    <!-- Vertical margin will be set dynamically since it depends on task bounds.
+         Setting the alpha of the dialog container to 0, since it shouldn't be visible until the
+         enter animation starts. -->
+    <FrameLayout
+        android:id="@+id/letterbox_restart_dialog_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/letterbox_restart_dialog_margin"
+        android:background="@drawable/letterbox_restart_dialog_background"
+        android:alpha="0"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintWidth_max="@dimen/letterbox_restart_dialog_width">
+
+        <!-- The ScrollView should only wrap the content of the dialog, otherwise the background
+             corner radius will be cut off when scrolling to the top/bottom. -->
+
+        <ScrollView android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+            <LinearLayout
+                android:padding="24dp"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center_horizontal"
+                android:orientation="vertical">
+
+                <ImageView
+                    android:importantForAccessibility="no"
+                    android:layout_width="@dimen/letterbox_restart_dialog_title_icon_width"
+                    android:layout_height="@dimen/letterbox_restart_dialog_title_icon_height"
+                    android:src="@drawable/letterbox_restart_header_ic_arrows"/>
+
+                <TextView
+                    android:layout_marginVertical="16dp"
+                    android:id="@+id/letterbox_restart_dialog_title"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/letterbox_restart_dialog_title"
+                    android:textAlignment="center"
+                    android:textAppearance="@style/RestartDialogTitleText"/>
+
+                <TextView
+                    android:textAppearance="@style/RestartDialogBodyText"
+                    android:id="@+id/letterbox_restart_dialog_description"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/letterbox_restart_dialog_description"
+                    android:textAlignment="center"/>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginVertical="32dp">
+
+                    <CheckBox
+                        android:id="@+id/letterbox_restart_dialog_checkbox"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:button="@drawable/letterbox_restart_checkbox_button"/>
+
+                    <TextView
+                        android:textAppearance="@style/RestartDialogCheckboxText"
+                        android:layout_marginStart="12dp"
+                        android:id="@+id/letterbox_restart_dialog_checkbox_description"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/letterbox_restart_dialog_checkbox_title"
+                        android:textAlignment="textStart"/>
+
+                </LinearLayout>
+
+                <FrameLayout
+                    android:minHeight="@dimen/letterbox_restart_dialog_button_height"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+                    <Button
+                        android:textAppearance="@style/RestartDialogDismissButton"
+                        android:id="@+id/letterbox_restart_dialog_dismiss_button"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:minWidth="@dimen/letterbox_restart_dialog_button_width"
+                        android:minHeight="@dimen/letterbox_restart_dialog_button_height"
+                        android:layout_gravity="start"
+                        android:background=
+                            "@drawable/letterbox_restart_dismiss_button_background_ripple"
+                        android:text="@string/letterbox_restart_cancel"
+                        android:contentDescription="@string/letterbox_restart_cancel"/>
+
+                    <Button
+                        android:textAppearance="@style/RestartDialogConfirmButton"
+                        android:id="@+id/letterbox_restart_dialog_restart_button"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:minWidth="@dimen/letterbox_restart_dialog_button_width"
+                        android:minHeight="@dimen/letterbox_restart_dialog_button_height"
+                        android:layout_gravity="end"
+                        android:background=
+                            "@drawable/letterbox_restart_button_background_ripple"
+                        android:text="@string/letterbox_restart_restart"
+                        android:contentDescription="@string/letterbox_restart_restart"/>
+
+                </FrameLayout>
+
+            </LinearLayout>
+
+        </ScrollView>
+
+    </FrameLayout>
+
+</com.android.wm.shell.compatui.RestartDialogLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 774f6c6..76eb094 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -105,6 +105,10 @@
         1.777778
     </item>
 
+    <!-- The aspect ratio that by which optimizations to large screen sizes are made.
+         Needs to be less that or equal to 1. -->
+    <item name="config_pipLargeScreenOptimizedAspectRatio" format="float" type="dimen">0.5625</item>
+
     <!-- The default gravity for the picture-in-picture window.
          Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
     <integer name="config_defaultPictureInPictureGravity">0x55</integer>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 3ee20ea..336c156 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -228,7 +228,7 @@
     <dimen name="bubble_user_education_stack_padding">16dp</dimen>
 
     <!-- Bottom and end margin for compat buttons. -->
-    <dimen name="compat_button_margin">16dp</dimen>
+    <dimen name="compat_button_margin">24dp</dimen>
 
     <!-- The radius of the corners of the compat hint bubble. -->
     <dimen name="compat_hint_corner_radius">28dp</dimen>
@@ -273,6 +273,60 @@
     <!-- The space between two actions in the letterbox education dialog -->
     <dimen name="letterbox_education_dialog_space_between_actions">24dp</dimen>
 
+    <!-- The corner radius of the buttons in the letterbox education dialog -->
+    <dimen name="letterbox_education_dialog_button_radius">12dp</dimen>
+
+    <!-- The horizontal padding for the buttons in the letterbox education dialog -->
+    <dimen name="letterbox_education_dialog_horizontal_padding">16dp</dimen>
+
+    <!-- The vertical padding for the buttons in the letterbox education dialog -->
+    <dimen name="letterbox_education_dialog_vertical_padding">8dp</dimen>
+
+    <!-- The insets for the buttons in the letterbox education dialog -->
+    <dimen name="letterbox_education_dialog_vertical_inset">6dp</dimen>
+
+    <!-- The margin between the dialog container and its parent. -->
+    <dimen name="letterbox_restart_dialog_margin">24dp</dimen>
+
+    <!-- The corner radius of the restart confirmation dialog. -->
+    <dimen name="letterbox_restart_dialog_corner_radius">28dp</dimen>
+
+    <!-- The fixed width of the dialog if there is enough space in the parent. -->
+    <dimen name="letterbox_restart_dialog_width">348dp</dimen>
+
+    <!-- The width of the top icon in the restart confirmation dialog. -->
+    <dimen name="letterbox_restart_dialog_title_icon_width">32dp</dimen>
+
+    <!-- The height of the top icon in the restart confirmation dialog. -->
+    <dimen name="letterbox_restart_dialog_title_icon_height">32dp</dimen>
+
+    <!-- The width of an icon in the restart confirmation dialog. -->
+    <dimen name="letterbox_restart_dialog_icon_width">40dp</dimen>
+
+    <!-- The height of an icon in the restart confirmation dialog. -->
+    <dimen name="letterbox_restart_dialog_icon_height">32dp</dimen>
+
+    <!-- The space between two actions in the restart confirmation dialog -->
+    <dimen name="letterbox_restart_dialog_space_between_actions">24dp</dimen>
+
+    <!-- The width of the buttons in the restart dialog -->
+    <dimen name="letterbox_restart_dialog_button_width">82dp</dimen>
+
+    <!-- The width of the buttons in the restart dialog -->
+    <dimen name="letterbox_restart_dialog_button_height">36dp</dimen>
+
+    <!-- The corner radius of the buttons in the restart dialog -->
+    <dimen name="letterbox_restart_dialog_button_radius">18dp</dimen>
+
+    <!-- The insets for the buttons in the letterbox restart dialog -->
+    <dimen name="letterbox_restart_dialog_vertical_inset">6dp</dimen>
+
+    <!-- The horizontal padding for the buttons in the letterbox restart dialog -->
+    <dimen name="letterbox_restart_dialog_horizontal_padding">16dp</dimen>
+
+    <!-- The vertical padding for the buttons in the letterbox restart dialog -->
+    <dimen name="letterbox_restart_dialog_vertical_padding">8dp</dimen>
+
     <!-- The width of the brand image on staring surface. -->
     <dimen name="starting_surface_brand_image_width">200dp</dimen>
 
@@ -298,30 +352,6 @@
     -->
     <dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen>
 
-    <!-- The size of the drag handle / menu shown along with a floating task. -->
-    <dimen name="floating_task_menu_size">32dp</dimen>
-
-    <!-- The size of menu items in the floating task menu. -->
-    <dimen name="floating_task_menu_item_size">24dp</dimen>
-
-    <!-- The horizontal margin of menu items in the floating task menu. -->
-    <dimen name="floating_task_menu_item_padding">5dp</dimen>
-
-    <!-- The width of visible floating view region when stashed. -->
-    <dimen name="floating_task_stash_offset">32dp</dimen>
-
-    <!-- The amount of elevation for a floating task. -->
-    <dimen name="floating_task_elevation">8dp</dimen>
-
-    <!-- The amount of padding around the bottom and top of the task. -->
-    <dimen name="floating_task_vertical_padding">8dp</dimen>
-
-    <!-- The normal size of the dismiss target. -->
-    <dimen name="floating_task_dismiss_circle_size">150dp</dimen>
-
-    <!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
-    <dimen name="floating_dismiss_circle_small">120dp</dimen>
-
     <!-- The thickness of shadows of a window that has focus in DIP. -->
     <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen>
 
@@ -331,11 +361,15 @@
     <!-- Height of button (32dp)  + 2 * margin (5dp each). -->
     <dimen name="freeform_decor_caption_height">42dp</dimen>
 
-    <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). -->
-    <dimen name="freeform_decor_caption_width">216dp</dimen>
+    <!-- Width of buttons (32dp each) + padding (128dp total). -->
+    <dimen name="freeform_decor_caption_menu_width">256dp</dimen>
+
+    <dimen name="freeform_decor_caption_menu_height">250dp</dimen>
 
     <dimen name="freeform_resize_handle">30dp</dimen>
 
     <dimen name="freeform_resize_corner">44dp</dimen>
 
+    <dimen name="caption_menu_elevation">4dp</dimen>
+
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 25eddf8..63992329 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -100,6 +100,15 @@
     <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] -->
     <string name="accessibility_action_divider_bottom_full">Bottom full screen</string>
 
+    <!-- Accessibility label for splitting to the left drop zone [CHAR LIMIT=NONE] -->
+    <string name="accessibility_split_left">Split left</string>
+    <!-- Accessibility label for splitting to the right drop zone [CHAR LIMIT=NONE] -->
+    <string name="accessibility_split_right">Split right</string>
+    <!-- Accessibility label for splitting to the top drop zone [CHAR LIMIT=NONE] -->
+    <string name="accessibility_split_top">Split top</string>
+    <!-- Accessibility label for splitting to the bottom drop zone [CHAR LIMIT=NONE] -->
+    <string name="accessibility_split_bottom">Split bottom</string>
+
     <!-- One-Handed Tutorial title [CHAR LIMIT=60] -->
     <string name="one_handed_tutorial_title">Using one-handed mode</string>
     <!-- One-Handed Tutorial description [CHAR LIMIT=NONE] -->
@@ -188,6 +197,23 @@
     <!-- Accessibility description of the letterbox education toast expand to dialog button. [CHAR LIMIT=NONE] -->
     <string name="letterbox_education_expand_button_description">Expand for more information.</string>
 
+    <!-- The title of the restart confirmation dialog. [CHAR LIMIT=NONE] -->
+    <string name="letterbox_restart_dialog_title">Restart for a better view?</string>
+
+    <!-- The description of the restart confirmation dialog. [CHAR LIMIT=NONE] -->
+    <string name="letterbox_restart_dialog_description">You can restart the app so it looks better on
+        your screen, but you may lose your progress or any unsaved changes
+    </string>
+
+    <!-- Button text for dismissing the restart confirmation dialog. [CHAR LIMIT=20] -->
+    <string name="letterbox_restart_cancel">Cancel</string>
+
+    <!-- Button text for dismissing the restart confirmation dialog. [CHAR LIMIT=20] -->
+    <string name="letterbox_restart_restart">Restart</string>
+
+    <!-- Checkbox text for asking to not show the restart confirmation dialog again. [CHAR LIMIT=NONE] -->
+    <string name="letterbox_restart_dialog_checkbox_title">Don\u2019t show again</string>
+
     <!-- Freeform window caption strings -->
     <!-- Accessibility text for the maximize window button [CHAR LIMIT=NONE] -->
     <string name="maximize_button_text">Maximize</string>
@@ -199,6 +225,8 @@
     <string name="back_button_text">Back</string>
     <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] -->
     <string name="handle_text">Handle</string>
+    <!-- Accessibility text for the handle menu app icon [CHAR LIMIT=NONE] -->
+    <string name="app_icon_text">App Icon</string>
     <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] -->
     <string name="fullscreen_text">Fullscreen</string>
     <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] -->
@@ -209,4 +237,12 @@
     <string name="more_button_text">More</string>
     <!-- Accessibility text for the handle floating window button [CHAR LIMIT=NONE] -->
     <string name="float_button_text">Float</string>
+    <!-- Accessibility text for the handle menu select button [CHAR LIMIT=NONE] -->
+    <string name="select_text">Select</string>
+    <!-- Accessibility text for the handle menu screenshot button [CHAR LIMIT=NONE] -->
+    <string name="screenshot_text">Screenshot</string>
+    <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] -->
+    <string name="close_text">Close</string>
+    <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
+    <string name="collapse_menu_text">Close Menu</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index a859721..0a0c49f 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -37,6 +37,22 @@
         <item name="android:padding">4dp</item>
     </style>
 
+    <style name="CaptionWindowingButtonStyle">
+        <item name="android:layout_width">32dp</item>
+        <item name="android:layout_height">32dp</item>
+        <item name="android:padding">4dp</item>
+        <item name="android:layout_marginTop">5dp</item>
+        <item name="android:layout_marginBottom">5dp</item>
+    </style>
+
+    <style name="CaptionMenuButtonStyle" parent="@style/Widget.AppCompat.Button.Borderless">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">52dp</item>
+        <item name="android:layout_marginStart">10dp</item>
+        <item name="android:padding">4dp</item>
+        <item name="android:gravity">start|center_vertical</item>
+    </style>
+
     <style name="DockedDividerBackground">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">@dimen/split_divider_bar_width</item>
@@ -63,4 +79,65 @@
         <item name="android:lineHeight">16sp</item>
         <item name="android:textColor">@color/tv_pip_edu_text</item>
     </style>
+
+    <style name="RestartDialogTitleText">
+        <item name="android:textSize">24sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:lineSpacingExtra">2sp</item>
+        <item name="android:textAppearance">
+            @*android:style/TextAppearance.DeviceDefault.Headline
+        </item>
+        <item name="android:fontFamily">
+            @*android:string/config_bodyFontFamilyMedium
+        </item>
+    </style>
+
+    <style name="RestartDialogBodyText">
+        <item name="android:textSize">14sp</item>
+        <item name="android:letterSpacing">0.02</item>
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:lineSpacingExtra">2sp</item>
+        <item name="android:textAppearance">
+            @*android:style/TextAppearance.DeviceDefault.Body2
+        </item>
+        <item name="android:fontFamily">
+            @*android:string/config_bodyFontFamily
+        </item>
+    </style>
+
+    <style name="RestartDialogCheckboxText">
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:lineSpacingExtra">4sp</item>
+        <item name="android:textAppearance">
+            @*android:style/TextAppearance.DeviceDefault.Headline
+        </item>
+        <item name="android:fontFamily">
+            @*android:string/config_bodyFontFamilyMedium
+        </item>
+    </style>
+
+    <style name="RestartDialogDismissButton">
+        <item name="android:lineSpacingExtra">2sp</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textAppearance">
+            @*android:style/TextAppearance.DeviceDefault.Body2
+        </item>
+        <item name="android:fontFamily">
+            @*android:string/config_bodyFontFamily
+        </item>
+    </style>
+
+    <style name="RestartDialogConfirmButton">
+        <item name="android:lineSpacingExtra">2sp</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+        <item name="android:textAppearance">
+            @*android:style/TextAppearance.DeviceDefault.Body2
+        </item>
+        <item name="android:fontFamily">
+            @*android:string/config_bodyFontFamily
+        </item>
+    </style>
 </resources>
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/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index e58e785..97a9fed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -256,12 +256,30 @@
         }
     }
 
+    /**
+     * Creates a persistent root task in WM for a particular windowing-mode.
+     * @param displayId The display to create the root task on.
+     * @param windowingMode Windowing mode to put the root task in.
+     * @param listener The listener to get the created task callback.
+     */
     public void createRootTask(int displayId, int windowingMode, TaskListener listener) {
-        ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s",
+        createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */);
+    }
+
+    /**
+     * Creates a persistent root task in WM for a particular windowing-mode.
+     * @param displayId The display to create the root task on.
+     * @param windowingMode Windowing mode to put the root task in.
+     * @param listener The listener to get the created task callback.
+     * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed.
+     */
+    public void createRootTask(int displayId, int windowingMode, TaskListener listener,
+            boolean removeWithTaskOrganizer) {
+        ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" ,
                 displayId, windowingMode, listener.toString());
         final IBinder cookie = new Binder();
         setPendingLaunchCookieListener(cookie, listener);
-        super.createRootTask(displayId, windowingMode, cookie);
+        super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index cbcd949..aaeef19 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -51,6 +51,7 @@
 import android.view.SurfaceControl;
 import android.window.BackAnimationAdaptor;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackNavigationInfo;
 import android.window.IBackAnimationRunner;
 import android.window.IBackNaviAnimationController;
@@ -81,7 +82,7 @@
     /** Flag for U animation features */
     public static boolean IS_U_ANIMATION_ENABLED =
             SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
-                    SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
     /** Predictive back animation developer option */
     private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
     // TODO (b/241808055) Find a appropriate time to remove during refactor
@@ -173,11 +174,11 @@
             boolean consumed = false;
             if (mWaitingAnimation && mOnBackCallback != null) {
                 if (mTriggerBack) {
-                    final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
+                    final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(1);
                     dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
                     dispatchOnBackInvoked(mOnBackCallback);
                 } else {
-                    final BackEvent backFinish = mTouchTracker.createProgressEvent(0);
+                    final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(0);
                     dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
                     dispatchOnBackCancelled(mOnBackCallback);
                 }
@@ -480,7 +481,7 @@
         if (!mBackGestureStarted || mBackNavigationInfo == null) {
             return;
         }
-        final BackEvent backEvent = mTouchTracker.createProgressEvent();
+        final BackMotionEvent backEvent = mTouchTracker.createProgressEvent();
         if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) {
                 dispatchOnBackProgressed(mBackToLauncherCallback, backEvent);
         } else if (mEnableAnimations.get()) {
@@ -573,7 +574,7 @@
     }
 
     private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
-            BackEvent backEvent) {
+            BackMotionEvent backEvent) {
         if (callback == null) {
             return;
         }
@@ -611,7 +612,7 @@
     }
 
     private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
-            BackEvent backEvent) {
+            BackMotionEvent backEvent) {
         if (callback == null) {
             return;
         }
@@ -730,7 +731,7 @@
                     }
                     dispatchOnBackStarted(mBackToLauncherCallback,
                             mTouchTracker.createStartEvent(mAnimationTarget));
-                    final BackEvent backInit = mTouchTracker.createProgressEvent();
+                    final BackMotionEvent backInit = mTouchTracker.createProgressEvent();
                     if (!mCachingBackDispatcher.consume()) {
                         dispatchOnBackProgressed(mBackToLauncherCallback, backInit);
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
index ccfac65..695ef4e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -19,6 +19,7 @@
 import android.os.SystemProperties;
 import android.view.RemoteAnimationTarget;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 
 /**
  * Helper class to record the touch location for gesture and generate back events.
@@ -82,11 +83,11 @@
         mSwipeEdge = BackEvent.EDGE_LEFT;
     }
 
-    BackEvent createStartEvent(RemoteAnimationTarget target) {
-        return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+    BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
+        return new BackMotionEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
     }
 
-    BackEvent createProgressEvent() {
+    BackMotionEvent createProgressEvent() {
         float progressThreshold = PROGRESS_THRESHOLD >= 0
                 ? PROGRESS_THRESHOLD : mProgressThreshold;
         progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
@@ -109,8 +110,8 @@
         return createProgressEvent(progress);
     }
 
-    BackEvent createProgressEvent(float progress) {
-        return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+    BackMotionEvent createProgressEvent(float progress) {
+        return new BackMotionEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
     }
 
     public void setProgressThreshold(float progressThreshold) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 09dc68a..e24c228 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -21,6 +21,7 @@
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
 import android.annotation.DimenRes;
+import android.annotation.Hide;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Notification;
@@ -125,7 +126,7 @@
     private Icon mIcon;
     private boolean mIsBubble;
     private boolean mIsTextChanged;
-    private boolean mIsClearable;
+    private boolean mIsDismissable;
     private boolean mShouldSuppressNotificationDot;
     private boolean mShouldSuppressNotificationList;
     private boolean mShouldSuppressPeek;
@@ -180,7 +181,7 @@
     @VisibleForTesting(visibility = PRIVATE)
     public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
             final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
-            int taskId, @Nullable final String locus, Executor mainExecutor,
+            int taskId, @Nullable final String locus, boolean isDismissable, Executor mainExecutor,
             final Bubbles.BubbleMetadataFlagListener listener) {
         Objects.requireNonNull(key);
         Objects.requireNonNull(shortcutInfo);
@@ -189,6 +190,7 @@
         mKey = key;
         mGroupKey = null;
         mLocusId = locus != null ? new LocusId(locus) : null;
+        mIsDismissable = isDismissable;
         mFlags = 0;
         mUser = shortcutInfo.getUserHandle();
         mPackageName = shortcutInfo.getPackage();
@@ -245,6 +247,11 @@
         return mKey;
     }
 
+    @Hide
+    public boolean isDismissable() {
+        return mIsDismissable;
+    }
+
     /**
      * @see StatusBarNotification#getGroupKey()
      * @return the group key for this bubble, if one exists.
@@ -526,7 +533,7 @@
             mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
         }
 
-        mIsClearable = entry.isClearable();
+        mIsDismissable = entry.isDismissable();
         mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot();
         mShouldSuppressNotificationList = entry.shouldSuppressNotificationList();
         mShouldSuppressPeek = entry.shouldSuppressPeek();
@@ -605,7 +612,7 @@
      * Whether this notification should be shown in the shade.
      */
     boolean showInShade() {
-        return !shouldSuppressNotification() || !mIsClearable;
+        return !shouldSuppressNotification() || !mIsDismissable;
     }
 
     /**
@@ -870,7 +877,7 @@
         pw.print("  desiredHeight: "); pw.println(getDesiredHeightString());
         pw.print("  suppressNotif: "); pw.println(shouldSuppressNotification());
         pw.print("  autoExpand:    "); pw.println(shouldAutoExpand());
-        pw.print("  isClearable:   "); pw.println(mIsClearable);
+        pw.print("  isDismissable: "); pw.println(mIsDismissable);
         pw.println("  bubbleMetadataFlagListener null: " + (mBubbleMetadataFlagListener == null));
         if (mExpandedView != null) {
             mExpandedView.dump(pw);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index dd8afff..71e15c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -973,21 +973,59 @@
     }
 
     /**
-     * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n
-     * otification and remain until the user dismisses the bubble or bubble stack. Only one intent
-     * bubble is supported at a time.
+     * This method has different behavior depending on:
+     *    - if an app bubble exists
+     *    - if an app bubble is expanded
+     *
+     * If no app bubble exists, this will add and expand a bubble with the provided intent. The
+     * intent must be explicit (i.e. include a package name or fully qualified component class name)
+     * and the activity for it should be resizable.
+     *
+     * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
+     * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
+     * this method will expand it.
+     *
+     * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
+     * the bubble or bubble stack.
+     *
+     * Some notes:
+     *    - Only one app bubble is supported at a time
+     *    - Calling this method with a different intent than the existing app bubble will do nothing
      *
      * @param intent the intent to display in the bubble expanded view.
      */
-    public void showAppBubble(Intent intent) {
-        if (intent == null || intent.getPackage() == null) return;
+    public void showOrHideAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) {
+            Log.w(TAG, "App bubble failed to show, invalid intent: " + intent
+                    + ((intent != null) ? " with package: " + intent.getPackage() : " "));
+            return;
+        }
 
         PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId);
         if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return;
 
-        Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
-        b.setShouldAutoExpand(true);
-        inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+        Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE);
+        if (existingAppBubble != null) {
+            BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
+            if (isStackExpanded()) {
+                if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) {
+                    // App bubble is expanded, lets collapse
+                    collapseStack();
+                } else {
+                    // App bubble is not selected, select it
+                    mBubbleData.setSelectedBubble(existingAppBubble);
+                }
+            } else {
+                // App bubble is not selected, select it & expand
+                mBubbleData.setSelectedBubble(existingAppBubble);
+                mBubbleData.setExpanded(true);
+            }
+        } else {
+            // App bubble does not exist, lets add and expand it
+            Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
+            b.setShouldAutoExpand(true);
+            inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+        }
     }
 
     /**
@@ -1697,9 +1735,9 @@
         }
 
         @Override
-        public void showAppBubble(Intent intent) {
+        public void showOrHideAppBubble(Intent intent) {
             mMainExecutor.execute(() -> {
-                BubbleController.this.showAppBubble(intent);
+                BubbleController.this.showOrHideAppBubble(intent);
             });
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index af31391..6230d22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -17,6 +17,7 @@
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -684,7 +685,8 @@
         if (bubble.getPendingIntentCanceled()
                 || !(reason == Bubbles.DISMISS_AGED
                 || reason == Bubbles.DISMISS_USER_GESTURE
-                || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
+                || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)
+                || KEY_APP_BUBBLE.equals(bubble.getKey())) {
             return;
         }
         if (DEBUG_BUBBLE_DATA) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index 3a59614..e37c785 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -109,7 +109,8 @@
                     b.rawDesiredHeightResId,
                     b.title,
                     b.taskId,
-                    b.locusId?.id
+                    b.locusId?.id,
+                    b.isDismissable
             )
         }
     }
@@ -205,6 +206,7 @@
                                 entity.title,
                                 entity.taskId,
                                 entity.locus,
+                                entity.isDismissable,
                                 mainExecutor,
                                 bubbleMetadataFlagListener
                         )
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
index 5f42826..afe19c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
@@ -38,18 +38,18 @@
     private StatusBarNotification mSbn;
     private Ranking mRanking;
 
-    private boolean mIsClearable;
+    private boolean mIsDismissable;
     private boolean mShouldSuppressNotificationDot;
     private boolean mShouldSuppressNotificationList;
     private boolean mShouldSuppressPeek;
 
     public BubbleEntry(@NonNull StatusBarNotification sbn,
-            Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot,
+            Ranking ranking, boolean isDismissable, boolean shouldSuppressNotificationDot,
             boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) {
         mSbn = sbn;
         mRanking = ranking;
 
-        mIsClearable = isClearable;
+        mIsDismissable = isDismissable;
         mShouldSuppressNotificationDot = shouldSuppressNotificationDot;
         mShouldSuppressNotificationList = shouldSuppressNotificationList;
         mShouldSuppressPeek = shouldSuppressPeek;
@@ -115,9 +115,9 @@
         return mRanking.canBubble();
     }
 
-    /** @return true if this notification is clearable. */
-    public boolean isClearable() {
-        return mIsClearable;
+    /** @return true if this notification can be dismissed. */
+    public boolean isDismissable() {
+        return mIsDismissable;
     }
 
     /** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 465d1ab..df43257 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -109,13 +109,28 @@
     void expandStackAndSelectBubble(Bubble bubble);
 
     /**
-     * Adds and expands bubble that is not notification based, but instead based on an intent from
-     * the app. The intent must be explicit (i.e. include a package name or fully qualified
-     * component class name) and the activity for it should be resizable.
+     * This method has different behavior depending on:
+     *    - if an app bubble exists
+     *    - if an app bubble is expanded
      *
-     * @param intent the intent to populate the bubble.
+     * If no app bubble exists, this will add and expand a bubble with the provided intent. The
+     * intent must be explicit (i.e. include a package name or fully qualified component class name)
+     * and the activity for it should be resizable.
+     *
+     * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
+     * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
+     * this method will expand it.
+     *
+     * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
+     * the bubble or bubble stack.
+     *
+     * Some notes:
+     *    - Only one app bubble is supported at a time
+     *    - Calling this method with a different intent than the existing app bubble will do nothing
+     *
+     * @param intent the intent to display in the bubble expanded view.
      */
-    void showAppBubble(Intent intent);
+    void showOrHideAppBubble(Intent intent);
 
     /**
      * @return a bubble that matches the provided shortcutId, if one exists.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
index 186b9b1..9b2e263 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
@@ -27,5 +27,6 @@
     @DimenRes val desiredHeightResId: Int,
     val title: String? = null,
     val taskId: Int,
-    val locus: String? = null
+    val locus: String? = null,
+    val isDismissable: Boolean = false
 )
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
index f4fa183..48d8ccf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
@@ -43,6 +43,7 @@
 private const val ATTR_TITLE = "t"
 private const val ATTR_TASK_ID = "tid"
 private const val ATTR_LOCUS = "l"
+private const val ATTR_DISMISSABLE = "d"
 
 /**
  * Writes the bubbles in xml format into given output stream.
@@ -84,6 +85,7 @@
         bubble.title?.let { serializer.attribute(null, ATTR_TITLE, it) }
         serializer.attribute(null, ATTR_TASK_ID, bubble.taskId.toString())
         bubble.locus?.let { serializer.attribute(null, ATTR_LOCUS, it) }
+        serializer.attribute(null, ATTR_DISMISSABLE, bubble.isDismissable.toString())
         serializer.endTag(null, TAG_BUBBLE)
     } catch (e: IOException) {
         throw RuntimeException(e)
@@ -142,7 +144,8 @@
             parser.getAttributeWithName(ATTR_DESIRED_HEIGHT_RES_ID)?.toInt() ?: return null,
             parser.getAttributeWithName(ATTR_TITLE),
             parser.getAttributeWithName(ATTR_TASK_ID)?.toInt() ?: INVALID_TASK_ID,
-            parser.getAttributeWithName(ATTR_LOCUS)
+            parser.getAttributeWithName(ATTR_LOCUS),
+            parser.getAttributeWithName(ATTR_DISMISSABLE)?.toBoolean() ?: false
     )
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java
new file mode 100644
index 0000000..22587f4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java
@@ -0,0 +1,147 @@
+/*
+ * 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.wm.shell.common;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.util.SparseIntArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Wrapper class to track the device posture change on Fold-ables.
+ * See also <a
+ * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables
+ * #foldable_postures">Foldable states and postures</a> for reference.
+ *
+ * Note that most of the implementation here inherits from
+ * {@link com.android.systemui.statusbar.policy.DevicePostureController}.
+ */
+public class DevicePostureController {
+    @IntDef(prefix = {"DEVICE_POSTURE_"}, value = {
+            DEVICE_POSTURE_UNKNOWN,
+            DEVICE_POSTURE_CLOSED,
+            DEVICE_POSTURE_HALF_OPENED,
+            DEVICE_POSTURE_OPENED,
+            DEVICE_POSTURE_FLIPPED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DevicePostureInt {}
+
+    // NOTE: These constants **must** match those defined for Jetpack Sidecar. This is because we
+    // use the Device State -> Jetpack Posture map to translate between the two.
+    public static final int DEVICE_POSTURE_UNKNOWN = 0;
+    public static final int DEVICE_POSTURE_CLOSED = 1;
+    public static final int DEVICE_POSTURE_HALF_OPENED = 2;
+    public static final int DEVICE_POSTURE_OPENED = 3;
+    public static final int DEVICE_POSTURE_FLIPPED = 4;
+
+    private final Context mContext;
+    private final ShellExecutor mMainExecutor;
+    private final List<OnDevicePostureChangedListener> mListeners = new ArrayList<>();
+    private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+    private int mDevicePosture = DEVICE_POSTURE_UNKNOWN;
+
+    public DevicePostureController(
+            Context context, ShellInit shellInit, ShellExecutor mainExecutor) {
+        mContext = context;
+        mMainExecutor = mainExecutor;
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        // Most of this is borrowed from WindowManager/Jetpack/DeviceStateManagerPostureProducer.
+        // Using the sidecar/extension libraries directly brings in a new dependency that it'd be
+        // good to avoid (along with the fact that sidecar is deprecated, and extensions isn't fully
+        // ready yet), and we'd have to make our own layer over the sidecar library anyway to easily
+        // allow the implementation to change, so it was easier to just interface with
+        // DeviceStateManager directly.
+        String[] deviceStatePosturePairs = mContext.getResources()
+                .getStringArray(R.array.config_device_state_postures);
+        for (String deviceStatePosturePair : deviceStatePosturePairs) {
+            String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+            if (deviceStatePostureMapping.length != 2) {
+                continue;
+            }
+
+            int deviceState;
+            int posture;
+            try {
+                deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+                posture = Integer.parseInt(deviceStatePostureMapping[1]);
+            } catch (NumberFormatException e) {
+                continue;
+            }
+
+            mDeviceStateToPostureMap.put(deviceState, posture);
+        }
+
+        final DeviceStateManager deviceStateManager = mContext.getSystemService(
+                DeviceStateManager.class);
+        if (deviceStateManager != null) {
+            deviceStateManager.registerCallback(mMainExecutor, state -> onDevicePostureChanged(
+                    mDeviceStateToPostureMap.get(state, DEVICE_POSTURE_UNKNOWN)));
+        }
+    }
+
+    @VisibleForTesting
+    void onDevicePostureChanged(int devicePosture) {
+        if (devicePosture == mDevicePosture) return;
+        mDevicePosture = devicePosture;
+        mListeners.forEach(l -> l.onDevicePostureChanged(mDevicePosture));
+    }
+
+    /**
+     * Register {@link OnDevicePostureChangedListener} for device posture changes.
+     * The listener will receive callback with current device posture upon registration.
+     */
+    public void registerOnDevicePostureChangedListener(
+            @NonNull OnDevicePostureChangedListener listener) {
+        if (mListeners.contains(listener)) return;
+        mListeners.add(listener);
+        listener.onDevicePostureChanged(mDevicePosture);
+    }
+
+    /**
+     * Unregister {@link OnDevicePostureChangedListener} for device posture changes.
+     */
+    public void unregisterOnDevicePostureChangedListener(
+            @NonNull OnDevicePostureChangedListener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Listener interface for device posture change.
+     */
+    public interface OnDevicePostureChangedListener {
+        /**
+         * Callback when device posture changes.
+         * See {@link DevicePostureInt} for callback values.
+         */
+        void onDevicePostureChanged(@DevicePostureInt int posture);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index 96efeeb..8484013 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -96,8 +96,7 @@
 
     /**
      * Different from {@link #equals(Object)}, this method compares the basic geometry properties
-     * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout and
-     * insets.
+     * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout.
      * @return {@code true} if the given {@link DisplayLayout} is identical geometry wise.
      */
     public boolean isSameGeometry(@NonNull DisplayLayout other) {
@@ -105,8 +104,7 @@
                 && mHeight == other.mHeight
                 && mRotation == other.mRotation
                 && mDensityDpi == other.mDensityDpi
-                && Objects.equals(mCutout, other.mCutout)
-                && Objects.equals(mStableInsets, other.mStableInsets);
+                && Objects.equals(mCutout, other.mCutout);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
index 9e0a48b..e2106e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
@@ -216,7 +216,6 @@
         args.argi1 = homeTaskVisible ? 1 : 0;
         args.argi2 = clearedTask ? 1 : 0;
         args.argi3 = wasVisible ? 1 : 0;
-        mMainHandler.removeMessages(ON_ACTIVITY_RESTART_ATTEMPT);
         mMainHandler.obtainMessage(ON_ACTIVITY_RESTART_ATTEMPT, args).sendToTarget();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index a9d3c9f..abb357c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -78,6 +78,7 @@
     private final Rect mResizingBounds = new Rect();
     private final Rect mTempRect = new Rect();
     private ValueAnimator mFadeAnimator;
+    private ValueAnimator mScreenshotAnimator;
 
     private int mIconSize;
     private int mOffsetX;
@@ -135,8 +136,17 @@
 
     /** Releases the surfaces for split decor. */
     public void release(SurfaceControl.Transaction t) {
-        if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
-            mFadeAnimator.cancel();
+        if (mFadeAnimator != null) {
+            if (mFadeAnimator.isRunning()) {
+                mFadeAnimator.cancel();
+            }
+            mFadeAnimator = null;
+        }
+        if (mScreenshotAnimator != null) {
+            if (mScreenshotAnimator.isRunning()) {
+                mScreenshotAnimator.cancel();
+            }
+            mScreenshotAnimator = null;
         }
         if (mViewHost != null) {
             mViewHost.release();
@@ -238,16 +248,20 @@
     /** Stops showing resizing hint. */
     public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) {
         if (mScreenshot != null) {
+            if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
+                mScreenshotAnimator.cancel();
+            }
+
             t.setPosition(mScreenshot, mOffsetX, mOffsetY);
 
             final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
-            final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
-            va.addUpdateListener(valueAnimator -> {
+            mScreenshotAnimator = ValueAnimator.ofFloat(1, 0);
+            mScreenshotAnimator.addUpdateListener(valueAnimator -> {
                 final float progress = (float) valueAnimator.getAnimatedValue();
                 animT.setAlpha(mScreenshot, progress);
                 animT.apply();
             });
-            va.addListener(new AnimatorListenerAdapter() {
+            mScreenshotAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
                     mRunningAnimationCount++;
@@ -266,7 +280,7 @@
                     }
                 }
             });
-            va.start();
+            mScreenshotAnimator.start();
         }
 
         if (mResizingIconView == null) {
@@ -292,9 +306,6 @@
                 });
                 return;
             }
-
-            // If fade-in animation is running, cancel it and re-run fade-out one.
-            mFadeAnimator.cancel();
         }
         if (mShown) {
             fadeOutDecor(animFinishedCallback);
@@ -332,6 +343,11 @@
      * directly. */
     public void fadeOutDecor(Runnable finishedCallback) {
         if (mShown) {
+            // If previous animation is running, just cancel it.
+            if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
+                mFadeAnimator.cancel();
+            }
+
             startFadeAnimation(false /* show */, true, finishedCallback);
             mShown = false;
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 45b234a..f616e6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -699,19 +699,6 @@
         return bounds.width() > bounds.height();
     }
 
-    /** Reverse the split position. */
-    @SplitPosition
-    public static int reversePosition(@SplitPosition int position) {
-        switch (position) {
-            case SPLIT_POSITION_TOP_OR_LEFT:
-                return SPLIT_POSITION_BOTTOM_OR_RIGHT;
-            case SPLIT_POSITION_BOTTOM_OR_RIGHT:
-                return SPLIT_POSITION_TOP_OR_LEFT;
-            default:
-                return SPLIT_POSITION_UNDEFINED;
-        }
-    }
-
     /**
      * Return if this layout is landscape.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
new file mode 100644
index 0000000..042721c9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
@@ -0,0 +1,85 @@
+/*
+ * 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.wm.shell.common.split;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.wm.shell.ShellTaskOrganizer;
+
+/** Helper utility class for split screen components to use. */
+public class SplitScreenUtils {
+    /** Reverse the split position. */
+    @SplitScreenConstants.SplitPosition
+    public static int reverseSplitPosition(@SplitScreenConstants.SplitPosition int position) {
+        switch (position) {
+            case SPLIT_POSITION_TOP_OR_LEFT:
+                return SPLIT_POSITION_BOTTOM_OR_RIGHT;
+            case SPLIT_POSITION_BOTTOM_OR_RIGHT:
+                return SPLIT_POSITION_TOP_OR_LEFT;
+            case SPLIT_POSITION_UNDEFINED:
+            default:
+                return SPLIT_POSITION_UNDEFINED;
+        }
+    }
+
+    /** Returns true if the task is valid for split screen. */
+    public static boolean isValidToSplit(ActivityManager.RunningTaskInfo taskInfo) {
+        return taskInfo != null && taskInfo.supportsMultiWindow
+                && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
+                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode());
+    }
+
+    /** Retrieve package name from an intent */
+    @Nullable
+    public static String getPackageName(Intent intent) {
+        if (intent == null || intent.getComponent() == null) {
+            return null;
+        }
+        return intent.getComponent().getPackageName();
+    }
+
+    /** Retrieve package name from a PendingIntent */
+    @Nullable
+    public static String getPackageName(PendingIntent pendingIntent) {
+        if (pendingIntent == null || pendingIntent.getIntent() == null) {
+            return null;
+        }
+        return getPackageName(pendingIntent.getIntent());
+    }
+
+    /** Retrieve package name from a taskId */
+    @Nullable
+    public static String getPackageName(int taskId, ShellTaskOrganizer taskOrganizer) {
+        final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId);
+        return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null;
+    }
+
+    /** Returns true if they are the same package. */
+    public static boolean samePackage(String packageName1, String packageName2) {
+        return packageName1 != null && packageName1.equals(packageName2);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
index 4f33a71..06f0a70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -16,11 +16,12 @@
 
 package com.android.wm.shell.compatui;
 
+import android.annotation.NonNull;
+import android.app.TaskInfo;
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.provider.DeviceConfig;
 
-import androidx.annotation.NonNull;
-
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ShellMainThread;
@@ -34,11 +35,27 @@
 @WMSingleton
 public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener {
 
-    static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog";
+    private static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG =
+            "enable_letterbox_restart_confirmation_dialog";
 
-    static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
+    private static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
             "enable_letterbox_reachability_education";
 
+    private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG = true;
+
+    private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION = false;
+
+    /**
+     * The name of the {@link SharedPreferences} that holds which user has seen the Restart
+     * confirmation dialog.
+     */
+    private static final String DONT_SHOW_RESTART_DIALOG_PREF_NAME = "dont_show_restart_dialog";
+
+    /**
+     * The {@link SharedPreferences} instance for {@link #DONT_SHOW_RESTART_DIALOG_PREF_NAME}.
+     */
+    private final SharedPreferences mSharedPreferences;
+
     // Whether the extended restart dialog is enabled
     private boolean mIsRestartDialogEnabled;
 
@@ -64,12 +81,15 @@
         mIsReachabilityEducationEnabled = context.getResources().getBoolean(
                 R.bool.config_letterboxIsReachabilityEducationEnabled);
         mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false);
+                DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
+                DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG);
         mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
                 DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
-                false);
+                DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION);
         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor,
                 this);
+        mSharedPreferences = context.getSharedPreferences(DONT_SHOW_RESTART_DIALOG_PREF_NAME,
+                Context.MODE_PRIVATE);
     }
 
     /**
@@ -102,18 +122,37 @@
         mIsReachabilityEducationOverrideEnabled = enabled;
     }
 
+    boolean getDontShowRestartDialogAgain(TaskInfo taskInfo) {
+        final int userId = taskInfo.userId;
+        final String packageName = taskInfo.topActivity.getPackageName();
+        return mSharedPreferences.getBoolean(
+                getDontShowAgainRestartKey(userId, packageName), /* default= */ false);
+    }
+
+    void setDontShowRestartDialogAgain(TaskInfo taskInfo) {
+        final int userId = taskInfo.userId;
+        final String packageName = taskInfo.topActivity.getPackageName();
+        mSharedPreferences.edit().putBoolean(getDontShowAgainRestartKey(userId, packageName),
+                true).apply();
+    }
+
     @Override
     public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
-        // TODO(b/263349751): Update flag and default value to true
         if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) {
             mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
                     DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
-                    false);
+                    DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG);
         }
+        // TODO(b/263349751): Update flag and default value to true
         if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) {
             mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
                     DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                    KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false);
+                    KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
+                    DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION);
         }
     }
-}
+
+    private String getDontShowAgainRestartKey(int userId, String packageName) {
+        return packageName + "@" + userId;
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 6627de5..76d9152 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -24,6 +24,7 @@
 import android.hardware.display.DisplayManager;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.InsetsSourceControl;
@@ -49,6 +50,7 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -91,6 +93,18 @@
     private final SparseArray<CompatUIWindowManager> mActiveCompatLayouts = new SparseArray<>(0);
 
     /**
+     * {@link SparseArray} that maps task ids to {@link RestartDialogWindowManager} that are
+     * currently visible
+     */
+    private final SparseArray<RestartDialogWindowManager> mTaskIdToRestartDialogWindowManagerMap =
+            new SparseArray<>(0);
+
+    /**
+     * {@link Set} of task ids for which we need to display a restart confirmation dialog
+     */
+    private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>();
+
+    /**
      * The active Letterbox Education layout if there is one (there can be at most one active).
      *
      * <p>An active layout is a layout that is eligible to be shown for the associated task but
@@ -111,11 +125,12 @@
     private final ShellExecutor mMainExecutor;
     private final Lazy<Transitions> mTransitionsLazy;
     private final DockStateReader mDockStateReader;
-
-    private CompatUICallback mCallback;
-
+    private final CompatUIConfiguration mCompatUIConfiguration;
     // Only show each hint once automatically in the process life.
     private final CompatUIHintsState mCompatUIHintsState;
+    private final CompatUIShellCommandHandler mCompatUIShellCommandHandler;
+
+    private CompatUICallback mCallback;
 
     // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't
     // be shown.
@@ -130,7 +145,9 @@
             SyncTransactionQueue syncQueue,
             ShellExecutor mainExecutor,
             Lazy<Transitions> transitionsLazy,
-            DockStateReader dockStateReader) {
+            DockStateReader dockStateReader,
+            CompatUIConfiguration compatUIConfiguration,
+            CompatUIShellCommandHandler compatUIShellCommandHandler) {
         mContext = context;
         mShellController = shellController;
         mDisplayController = displayController;
@@ -140,14 +157,17 @@
         mMainExecutor = mainExecutor;
         mTransitionsLazy = transitionsLazy;
         mCompatUIHintsState = new CompatUIHintsState();
-        shellInit.addInitCallback(this::onInit, this);
         mDockStateReader = dockStateReader;
+        mCompatUIConfiguration = compatUIConfiguration;
+        mCompatUIShellCommandHandler = compatUIShellCommandHandler;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     private void onInit() {
         mShellController.addKeyguardChangeListener(this);
         mDisplayController.addDisplayWindowListener(this);
         mImeController.addPositionProcessor(this);
+        mCompatUIShellCommandHandler.onInit();
     }
 
     /** Sets the callback for UI interactions. */
@@ -164,6 +184,9 @@
      */
     public void onCompatInfoChanged(TaskInfo taskInfo,
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
+        if (taskInfo != null && !taskInfo.topActivityInSizeCompat) {
+            mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
+        }
         if (taskInfo.configuration == null || taskListener == null) {
             // Null token means the current foreground activity is not in compatibility mode.
             removeLayouts(taskInfo.taskId);
@@ -172,6 +195,7 @@
 
         createOrUpdateCompatLayout(taskInfo, taskListener);
         createOrUpdateLetterboxEduLayout(taskInfo, taskListener);
+        createOrUpdateRestartDialogLayout(taskInfo, taskListener);
     }
 
     @Override
@@ -278,7 +302,21 @@
             ShellTaskOrganizer.TaskListener taskListener) {
         return new CompatUIWindowManager(context,
                 taskInfo, mSyncQueue, mCallback, taskListener,
-                mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState);
+                mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState,
+                mCompatUIConfiguration, this::onRestartButtonClicked);
+    }
+
+    private void onRestartButtonClicked(
+            Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState) {
+        if (mCompatUIConfiguration.isRestartDialogEnabled()
+                && !mCompatUIConfiguration.getDontShowRestartDialogAgain(
+                taskInfoState.first)) {
+            // We need to show the dialog
+            mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId);
+            onCompatInfoChanged(taskInfoState.first, taskInfoState.second);
+        } else {
+            mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
+        }
     }
 
     private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo,
@@ -327,6 +365,61 @@
         mActiveLetterboxEduLayout = null;
     }
 
+    private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo,
+            ShellTaskOrganizer.TaskListener taskListener) {
+        RestartDialogWindowManager layout =
+                mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
+        if (layout != null) {
+            if (layout.needsToBeRecreated(taskInfo, taskListener)) {
+                mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
+                layout.release();
+            } else {
+                layout.setRequestRestartDialog(
+                        mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId));
+                // UI already exists, update the UI layout.
+                if (!layout.updateCompatInfo(taskInfo, taskListener,
+                        showOnDisplay(layout.getDisplayId()))) {
+                    // The layout is no longer eligible to be shown, remove from active layouts.
+                    mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
+                }
+                return;
+            }
+        }
+        // Create a new UI layout.
+        final Context context = getOrCreateDisplayContext(taskInfo.displayId);
+        if (context == null) {
+            return;
+        }
+        layout = createRestartDialogWindowManager(context, taskInfo, taskListener);
+        layout.setRequestRestartDialog(
+                mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId));
+        if (layout.createLayout(showOnDisplay(taskInfo.displayId))) {
+            // The new layout is eligible to be shown, add it the active layouts.
+            mTaskIdToRestartDialogWindowManagerMap.put(taskInfo.taskId, layout);
+        }
+    }
+
+    @VisibleForTesting
+    RestartDialogWindowManager createRestartDialogWindowManager(Context context, TaskInfo taskInfo,
+            ShellTaskOrganizer.TaskListener taskListener) {
+        return new RestartDialogWindowManager(context, taskInfo, mSyncQueue, taskListener,
+                mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(),
+                this::onRestartDialogCallback, this::onRestartDialogDismissCallback,
+                mCompatUIConfiguration);
+    }
+
+    private void onRestartDialogCallback(
+            Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
+        mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId);
+        mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
+    }
+
+    private void onRestartDialogDismissCallback(
+            Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
+        mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId);
+        onCompatInfoChanged(stateInfo.first, stateInfo.second);
+    }
+
     private void removeLayouts(int taskId) {
         final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId);
         if (layout != null) {
@@ -338,6 +431,14 @@
             mActiveLetterboxEduLayout.release();
             mActiveLetterboxEduLayout = null;
         }
+
+        final RestartDialogWindowManager restartLayout =
+                mTaskIdToRestartDialogWindowManagerMap.get(taskId);
+        if (restartLayout != null) {
+            restartLayout.release();
+            mTaskIdToRestartDialogWindowManagerMap.remove(taskId);
+            mSetOfTaskIdsShowingRestartDialog.remove(taskId);
+        }
     }
 
     private Context getOrCreateDisplayContext(int displayId) {
@@ -382,6 +483,14 @@
         if (mActiveLetterboxEduLayout != null && condition.test(mActiveLetterboxEduLayout)) {
             callback.accept(mActiveLetterboxEduLayout);
         }
+        for (int i = 0; i < mTaskIdToRestartDialogWindowManagerMap.size(); i++) {
+            final int taskId = mTaskIdToRestartDialogWindowManagerMap.keyAt(i);
+            final RestartDialogWindowManager layout =
+                    mTaskIdToRestartDialogWindowManagerMap.get(taskId);
+            if (layout != null && condition.test(layout)) {
+                callback.accept(layout);
+            }
+        }
     }
 
     /** An implementation of {@link OnInsetsChangedListener} for a given display id. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index bce3ec4..fe95d04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -21,12 +21,14 @@
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.TaskInfo;
 import android.app.TaskInfo.CameraCompatControlState;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.Log;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 
@@ -38,6 +40,8 @@
 import com.android.wm.shell.compatui.CompatUIController.CompatUICallback;
 import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
 
+import java.util.function.Consumer;
+
 /**
  * Window manager for the Size Compat restart button and Camera Compat control.
  */
@@ -50,6 +54,13 @@
 
     private final CompatUICallback mCallback;
 
+    private final CompatUIConfiguration mCompatUIConfiguration;
+
+    private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
+
+    @NonNull
+    private TaskInfo mTaskInfo;
+
     // Remember the last reported states in case visibility changes due to keyguard or IME updates.
     @VisibleForTesting
     boolean mHasSizeCompat;
@@ -68,12 +79,16 @@
     CompatUIWindowManager(Context context, TaskInfo taskInfo,
             SyncTransactionQueue syncQueue, CompatUICallback callback,
             ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
-            CompatUIHintsState compatUIHintsState) {
+            CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration,
+            Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) {
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
+        mTaskInfo = taskInfo;
         mCallback = callback;
         mHasSizeCompat = taskInfo.topActivityInSizeCompat;
         mCameraCompatControlState = taskInfo.cameraCompatControlState;
         mCompatUIHintsState = compatUIHintsState;
+        mCompatUIConfiguration = compatUIConfiguration;
+        mOnRestartButtonClicked = onRestartButtonClicked;
     }
 
     @Override
@@ -119,6 +134,7 @@
     @Override
     public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
             boolean canShow) {
+        mTaskInfo = taskInfo;
         final boolean prevHasSizeCompat = mHasSizeCompat;
         final int prevCameraCompatControlState = mCameraCompatControlState;
         mHasSizeCompat = taskInfo.topActivityInSizeCompat;
@@ -138,7 +154,7 @@
 
     /** Called when the restart button is clicked. */
     void onRestartButtonClicked() {
-        mCallback.onSizeCompatRestartButtonClicked(mTaskId);
+        mOnRestartButtonClicked.accept(Pair.create(mTaskInfo, getTaskListener()));
     }
 
     /** Called when the camera treatment button is clicked. */
@@ -199,8 +215,14 @@
                 : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
         final int positionY = taskStableBounds.bottom - taskBounds.top
                 - mLayout.getMeasuredHeight();
-
+        // To secure a proper visualisation, we hide the layout while updating the position of
+        // the {@link SurfaceControl} it belongs.
+        final int oldVisibility = mLayout.getVisibility();
+        if (oldVisibility == View.VISIBLE) {
+            mLayout.setVisibility(View.GONE);
+        }
         updateSurfacePosition(positionX, positionY);
+        mLayout.setVisibility(oldVisibility);
     }
 
     private void updateVisibilityOfViews() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index face243..cfb2acc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -169,6 +169,10 @@
         initSurface(mLeash);
     }
 
+    protected ShellTaskOrganizer.TaskListener getTaskListener() {
+        return mTaskListener;
+    }
+
     /** Inits the z-order of the surface. */
     private void initSurface(SurfaceControl leash) {
         final int z = getZOrder();
@@ -206,7 +210,8 @@
         }
 
         View layout = getLayout();
-        if (layout == null || prevTaskListener != taskListener) {
+        if (layout == null || prevTaskListener != taskListener
+                || mTaskConfig.uiMode != prevTaskConfig.uiMode) {
             // Layout wasn't created yet or TaskListener changed, recreate the layout for new
             // surface parent.
             release();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
new file mode 100644
index 0000000..c53e638
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.wm.shell.R;
+
+import java.util.function.Consumer;
+
+/**
+ * Container for a SCM restart confirmation dialog and background dim.
+ */
+public class RestartDialogLayout extends ConstraintLayout implements DialogContainerSupplier {
+
+    private View mDialogContainer;
+    private TextView mDialogTitle;
+    private Drawable mBackgroundDim;
+
+    public RestartDialogLayout(Context context) {
+        this(context, null);
+    }
+
+    public RestartDialogLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RestartDialogLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RestartDialogLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public View getDialogContainerView() {
+        return mDialogContainer;
+    }
+
+    TextView getDialogTitle() {
+        return mDialogTitle;
+    }
+
+    @Override
+    public Drawable getBackgroundDimDrawable() {
+        return mBackgroundDim;
+    }
+
+    /**
+     * Register a callback for the dismiss button and background dim.
+     *
+     * @param callback The callback to register or null if all on click listeners should be removed.
+     */
+    void setDismissOnClickListener(@Nullable Runnable callback) {
+        final OnClickListener listener = callback == null ? null : view -> callback.run();
+        findViewById(R.id.letterbox_restart_dialog_dismiss_button).setOnClickListener(listener);
+    }
+
+    /**
+     * Register a callback for the restart button
+     *
+     * @param callback The callback to register or null if all on click listeners should be removed.
+     */
+    void setRestartOnClickListener(@Nullable Consumer<Boolean> callback) {
+        final CheckBox dontShowAgainCheckbox = findViewById(R.id.letterbox_restart_dialog_checkbox);
+        final OnClickListener listener = callback == null ? null : view -> callback.accept(
+                dontShowAgainCheckbox.isChecked());
+        findViewById(R.id.letterbox_restart_dialog_restart_button).setOnClickListener(listener);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mDialogContainer = findViewById(R.id.letterbox_restart_dialog_container);
+        mDialogTitle = findViewById(R.id.letterbox_restart_dialog_title);
+        mBackgroundDim = getBackground().mutate();
+        // Set the alpha of the background dim to 0 for enter animation.
+        mBackgroundDim.setAlpha(0);
+        // We add a no-op on-click listener to the dialog container so that clicks on it won't
+        // propagate to the listener of the layout (which represents the background dim).
+        mDialogContainer.setOnClickListener(view -> {});
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
new file mode 100644
index 0000000..2440838
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui;
+
+import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.graphics.Rect;
+import android.provider.Settings;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.function.Consumer;
+
+/**
+ * Window manager for the Restart Dialog.
+ *
+ * TODO(b/263484314): Create abstraction of RestartDialogWindowManager and LetterboxEduWindowManager
+ */
+class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
+
+    /**
+     * The restart dialog should be the topmost child of the Task in case there can be more
+     * than one child.
+     */
+    private static final int Z_ORDER = Integer.MAX_VALUE;
+
+    private final DialogAnimationController<RestartDialogLayout> mAnimationController;
+
+    private final Transitions mTransitions;
+
+    // Remember the last reported state in case visibility changes due to keyguard or IME updates.
+    private boolean mRequestRestartDialog;
+
+    private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback;
+
+    private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartCallback;
+
+    private final CompatUIConfiguration mCompatUIConfiguration;
+
+    /**
+     * The vertical margin between the dialog container and the task stable bounds (excluding
+     * insets).
+     */
+    private final int mDialogVerticalMargin;
+
+    @NonNull
+    private TaskInfo mTaskInfo;
+
+    @Nullable
+    @VisibleForTesting
+    RestartDialogLayout mLayout;
+
+    RestartDialogWindowManager(Context context, TaskInfo taskInfo,
+            SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
+            DisplayLayout displayLayout, Transitions transitions,
+            Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback,
+            Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback,
+            CompatUIConfiguration compatUIConfiguration) {
+        this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
+                onRestartCallback, onDismissCallback,
+                new DialogAnimationController<>(context, "RestartDialogWindowManager"),
+                compatUIConfiguration);
+    }
+
+    @VisibleForTesting
+    RestartDialogWindowManager(Context context, TaskInfo taskInfo,
+            SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
+            DisplayLayout displayLayout, Transitions transitions,
+            Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback,
+            Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback,
+            DialogAnimationController<RestartDialogLayout> animationController,
+            CompatUIConfiguration compatUIConfiguration) {
+        super(context, taskInfo, syncQueue, taskListener, displayLayout);
+        mTaskInfo = taskInfo;
+        mTransitions = transitions;
+        mOnDismissCallback = onDismissCallback;
+        mOnRestartCallback = onRestartCallback;
+        mAnimationController = animationController;
+        mDialogVerticalMargin = (int) mContext.getResources().getDimension(
+                R.dimen.letterbox_restart_dialog_margin);
+        mCompatUIConfiguration = compatUIConfiguration;
+    }
+
+    @Override
+    protected int getZOrder() {
+        return Z_ORDER;
+    }
+
+    @Override
+    @Nullable
+    protected  View getLayout() {
+        return mLayout;
+    }
+
+    @Override
+    protected void removeLayout() {
+        mLayout = null;
+    }
+
+    @Override
+    protected boolean eligibleToShowLayout() {
+        // We don't show this dialog if the user has explicitly selected so clicking on a checkbox.
+        return mRequestRestartDialog && !isTaskbarEduShowing() && (mLayout != null
+                || !mCompatUIConfiguration.getDontShowRestartDialogAgain(mTaskInfo));
+    }
+
+    @Override
+    protected View createLayout() {
+        mLayout = inflateLayout();
+        updateDialogMargins();
+
+        // startEnterAnimation will be called immediately if shell-transitions are disabled.
+        mTransitions.runOnIdle(this::startEnterAnimation);
+
+        return mLayout;
+    }
+
+    void setRequestRestartDialog(boolean enabled) {
+        mRequestRestartDialog = enabled;
+    }
+
+    @Override
+    public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
+            boolean canShow) {
+        mTaskInfo = taskInfo;
+        return super.updateCompatInfo(taskInfo, taskListener, canShow);
+    }
+
+    boolean needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
+        return taskInfo.configuration.uiMode != mTaskInfo.configuration.uiMode
+                || !getTaskListener().equals(taskListener);
+    }
+
+    private void updateDialogMargins() {
+        if (mLayout == null) {
+            return;
+        }
+        final View dialogContainer = mLayout.getDialogContainerView();
+        ViewGroup.MarginLayoutParams marginParams =
+                (ViewGroup.MarginLayoutParams) dialogContainer.getLayoutParams();
+
+        final Rect taskBounds = getTaskBounds();
+        final Rect taskStableBounds = getTaskStableBounds();
+
+        marginParams.topMargin = taskStableBounds.top - taskBounds.top + mDialogVerticalMargin;
+        marginParams.bottomMargin =
+                taskBounds.bottom - taskStableBounds.bottom + mDialogVerticalMargin;
+        dialogContainer.setLayoutParams(marginParams);
+    }
+
+    private RestartDialogLayout inflateLayout() {
+        return (RestartDialogLayout) LayoutInflater.from(mContext).inflate(
+                R.layout.letterbox_restart_dialog_layout, null);
+    }
+
+    private void startEnterAnimation() {
+        if (mLayout == null) {
+            // Dialog has already been released.
+            return;
+        }
+        mAnimationController.startEnterAnimation(mLayout, /* endCallback= */
+                this::onDialogEnterAnimationEnded);
+    }
+
+    private void onDialogEnterAnimationEnded() {
+        if (mLayout == null) {
+            // Dialog has already been released.
+            return;
+        }
+        mLayout.setDismissOnClickListener(this::onDismiss);
+        mLayout.setRestartOnClickListener(dontShowAgain -> {
+            if (mLayout != null) {
+                mLayout.setDismissOnClickListener(null);
+                mAnimationController.startExitAnimation(mLayout, () -> {
+                    release();
+                });
+            }
+            if (dontShowAgain) {
+                mCompatUIConfiguration.setDontShowRestartDialogAgain(mTaskInfo);
+            }
+            mOnRestartCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+        });
+        // Focus on the dialog title for accessibility.
+        mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+    }
+
+    private void onDismiss() {
+        if (mLayout == null) {
+            return;
+        }
+
+        mLayout.setDismissOnClickListener(null);
+        mAnimationController.startExitAnimation(mLayout, () -> {
+            release();
+            mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+        });
+    }
+
+    @Override
+    public void release() {
+        mAnimationController.cancelAnimation();
+        super.release();
+    }
+
+    @Override
+    protected void onParentBoundsChanged() {
+        if (mLayout == null) {
+            return;
+        }
+        // Both the layout dimensions and dialog margins depend on the parent bounds.
+        WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams();
+        mLayout.setLayoutParams(windowLayoutParams);
+        updateDialogMargins();
+        relayout(windowLayoutParams);
+    }
+
+    @Override
+    protected void updateSurfacePosition() {
+        // Nothing to do, since the position of the surface is fixed to the top left corner (0,0)
+        // of the task (parent surface), which is the default position of a surface.
+    }
+
+    @Override
+    protected WindowManager.LayoutParams getWindowLayoutParams() {
+        final Rect taskBounds = getTaskBounds();
+        return getWindowLayoutParams(/* width= */ taskBounds.width(), /* height= */
+                taskBounds.height());
+    }
+
+    @VisibleForTesting
+    boolean isTaskbarEduShowing() {
+        return Settings.Secure.getInt(mContext.getContentResolver(),
+                LAUNCHER_TASKBAR_EDUCATION_SHOWING, /* def= */ 0) == 1;
+    }
+}
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 8022e9b..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
@@ -39,6 +39,7 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUiEventLogger;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
 import com.android.wm.shell.pip.tv.TvPipBoundsController;
 import com.android.wm.shell.pip.tv.TvPipBoundsState;
@@ -69,6 +70,7 @@
             ShellInit shellInit,
             ShellController shellController,
             TvPipBoundsState tvPipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             TvPipBoundsController tvPipBoundsController,
             PipAppOpsListener pipAppOpsListener,
@@ -88,6 +90,7 @@
                         shellInit,
                         shellController,
                         tvPipBoundsState,
+                        pipSizeSpecHandler,
                         tvPipBoundsAlgorithm,
                         tvPipBoundsController,
                         pipAppOpsListener,
@@ -127,14 +130,23 @@
     @WMSingleton
     @Provides
     static TvPipBoundsAlgorithm provideTvPipBoundsAlgorithm(Context context,
-            TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) {
-        return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm);
+            TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm,
+            PipSizeSpecHandler pipSizeSpecHandler) {
+        return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm,
+                pipSizeSpecHandler);
     }
 
     @WMSingleton
     @Provides
-    static TvPipBoundsState provideTvPipBoundsState(Context context) {
-        return new TvPipBoundsState(context);
+    static TvPipBoundsState provideTvPipBoundsState(Context context,
+            PipSizeSpecHandler pipSizeSpecHandler) {
+        return new TvPipBoundsState(context, pipSizeSpecHandler);
+    }
+
+    @WMSingleton
+    @Provides
+    static PipSizeSpecHandler providePipSizeSpecHelper(Context context) {
+        return new PipSizeSpecHandler(context);
     }
 
     // Handler needed for loadDrawableAsync() in PipControlsViewController
@@ -194,6 +206,7 @@
             TvPipMenuController tvPipMenuController,
             SyncTransactionQueue syncTransactionQueue,
             TvPipBoundsState tvPipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipTransitionState pipTransitionState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             PipAnimationController pipAnimationController,
@@ -205,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/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 09f5cf1..72dc771 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -41,6 +41,7 @@
 import com.android.wm.shell.back.BackAnimationController;
 import com.android.wm.shell.bubbles.BubbleController;
 import com.android.wm.shell.bubbles.Bubbles;
+import com.android.wm.shell.common.DevicePostureController;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
@@ -56,7 +57,9 @@
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.compatui.CompatUIConfiguration;
 import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.compatui.CompatUIShellCommandHandler;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopModeController;
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
@@ -158,6 +161,16 @@
 
     @WMSingleton
     @Provides
+    static DevicePostureController provideDevicePostureController(
+            Context context,
+            ShellInit shellInit,
+            @ShellMainThread ShellExecutor mainExecutor
+    ) {
+        return new DevicePostureController(context, shellInit, mainExecutor);
+    }
+
+    @WMSingleton
+    @Provides
     static DragAndDropController provideDragAndDropController(Context context,
             ShellInit shellInit,
             ShellController shellController,
@@ -196,10 +209,11 @@
             DisplayController displayController, DisplayInsetsController displayInsetsController,
             DisplayImeController imeController, SyncTransactionQueue syncQueue,
             @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy,
-            DockStateReader dockStateReader) {
+            DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration,
+            CompatUIShellCommandHandler compatUIShellCommandHandler) {
         return new CompatUIController(context, shellInit, shellController, displayController,
                 displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy,
-                dockStateReader);
+                dockStateReader, compatUIConfiguration, compatUIShellCommandHandler);
     }
 
     @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 d3b9fa5..4578523 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
@@ -49,6 +49,7 @@
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.draganddrop.DragAndDropController;
@@ -76,6 +77,7 @@
 import com.android.wm.shell.pip.phone.PhonePipMenuController;
 import com.android.wm.shell.pip.phone.PipController;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -93,6 +95,7 @@
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
 import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition;
 import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
+import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
@@ -191,8 +194,10 @@
             DisplayController displayController,
             SyncTransactionQueue syncQueue,
             Optional<DesktopModeController> desktopModeController,
-            Optional<DesktopTasksController> desktopTasksController) {
-        return new DesktopModeWindowDecorViewModel(
+            Optional<DesktopTasksController> desktopTasksController,
+            Optional<SplitScreenController> splitScreenController) {
+        if (DesktopModeStatus.isAnyEnabled()) {
+            return new DesktopModeWindowDecorViewModel(
                     context,
                     mainHandler,
                     mainChoreographer,
@@ -200,7 +205,16 @@
                     displayController,
                     syncQueue,
                     desktopModeController,
-                    desktopTasksController);
+                    desktopTasksController,
+                    splitScreenController);
+        }
+        return new CaptionWindowDecorViewModel(
+                    context,
+                    mainHandler,
+                    mainChoreographer,
+                    taskOrganizer,
+                    displayController,
+                    syncQueue);
     }
 
     //
@@ -327,6 +341,7 @@
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PhonePipKeepClearAlgorithm pipKeepClearAlgorithm,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipMotionHelper pipMotionHelper,
             PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController,
@@ -343,17 +358,18 @@
         return Optional.ofNullable(PipController.create(
                 context, shellInit, shellCommandHandler, shellController,
                 displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm,
-                pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController,
-                phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler,
-                pipTransitionController, windowManagerShellWrapper, taskStackListener,
-                pipParamsChangedForwarder, displayInsetsController, oneHandedController,
-                mainExecutor));
+                pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipMotionHelper,
+                pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
+                pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
+                taskStackListener, pipParamsChangedForwarder, displayInsetsController,
+                oneHandedController, mainExecutor));
     }
 
     @WMSingleton
     @Provides
-    static PipBoundsState providePipBoundsState(Context context) {
-        return new PipBoundsState(context);
+    static PipBoundsState providePipBoundsState(Context context,
+            PipSizeSpecHandler pipSizeSpecHandler) {
+        return new PipBoundsState(context, pipSizeSpecHandler);
     }
 
     @WMSingleton
@@ -370,11 +386,18 @@
 
     @WMSingleton
     @Provides
+    static PipSizeSpecHandler providePipSizeSpecHelper(Context context) {
+        return new PipSizeSpecHandler(context);
+    }
+
+    @WMSingleton
+    @Provides
     static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context,
             PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm,
-            PhonePipKeepClearAlgorithm pipKeepClearAlgorithm) {
+            PhonePipKeepClearAlgorithm pipKeepClearAlgorithm,
+            PipSizeSpecHandler pipSizeSpecHandler) {
         return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm,
-                pipKeepClearAlgorithm);
+                pipKeepClearAlgorithm, pipSizeSpecHandler);
     }
 
     // Handler is used by Icon.loadDrawableAsync
@@ -398,13 +421,14 @@
             PhonePipMenuController menuPhoneController,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipTaskOrganizer pipTaskOrganizer,
             PipMotionHelper pipMotionHelper,
             FloatingContentCoordinator floatingContentCoordinator,
             PipUiEventLogger pipUiEventLogger,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm,
-                pipBoundsState, pipTaskOrganizer, pipMotionHelper,
+                pipBoundsState, pipSizeSpecHandler, pipTaskOrganizer, pipMotionHelper,
                 floatingContentCoordinator, pipUiEventLogger, mainExecutor);
     }
 
@@ -420,6 +444,7 @@
             SyncTransactionQueue syncTransactionQueue,
             PipTransitionState pipTransitionState,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PhonePipMenuController menuPhoneController,
             PipAnimationController pipAnimationController,
@@ -431,10 +456,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
@@ -449,13 +475,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
@@ -533,16 +560,18 @@
     static FullscreenUnfoldTaskAnimator provideFullscreenUnfoldTaskAnimator(
             Context context,
             UnfoldBackgroundController unfoldBackgroundController,
+            ShellController shellController,
             DisplayInsetsController displayInsetsController
     ) {
         return new FullscreenUnfoldTaskAnimator(context, unfoldBackgroundController,
-                displayInsetsController);
+                shellController, displayInsetsController);
     }
 
     @Provides
     static SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimatorBase(
             Context context,
             UnfoldBackgroundController backgroundController,
+            ShellController shellController,
             @ShellMainThread ShellExecutor executor,
             Lazy<Optional<SplitScreenController>> splitScreenOptional,
             DisplayInsetsController displayInsetsController
@@ -552,7 +581,7 @@
         // controller directly once we refactor ShellTaskOrganizer to not depend on the unfold
         // animation controller directly.
         return new SplitTaskUnfoldAnimator(context, executor, splitScreenOptional,
-                backgroundController, displayInsetsController);
+                shellController, backgroundController, displayInsetsController);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index f5f3573..bc81710 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
@@ -36,6 +37,7 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.ArraySet;
@@ -251,18 +253,26 @@
      * Show apps on desktop
      */
     void showDesktopApps() {
-        WindowContainerTransaction wct = bringDesktopAppsToFront();
+        // Bring apps to front, ignoring their visibility status to always ensure they are on top.
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        bringDesktopAppsToFront(wct);
 
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */);
-        } else {
-            mShellTaskOrganizer.applyTransaction(wct);
+        if (!wct.isEmpty()) {
+            if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+                // TODO(b/268662477): add animation for the transition
+                mTransitions.startTransition(TRANSIT_NONE, wct, null /* handler */);
+            } else {
+                mShellTaskOrganizer.applyTransaction(wct);
+            }
         }
     }
 
-    @NonNull
-    private WindowContainerTransaction bringDesktopAppsToFront() {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    /** Get number of tasks that are marked as visible */
+    int getVisibleTaskCount() {
+        return mDesktopModeTaskRepository.getVisibleTaskCount();
+    }
+
+    private void bringDesktopAppsToFront(WindowContainerTransaction wct) {
         final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
 
@@ -275,16 +285,11 @@
         }
 
         if (taskInfos.isEmpty()) {
-            return wct;
+            return;
         }
 
-        final boolean allActiveTasksAreVisible = taskInfos.stream()
-                .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId));
-        if (allActiveTasksAreVisible) {
-            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
-                    "bringDesktopAppsToFront: active tasks are already in front, skipping.");
-            return wct;
-        }
+        moveHomeTaskToFront(wct);
+
         ProtoLog.d(WM_SHELL_DESKTOP_MODE,
                 "bringDesktopAppsToFront: reordering all active tasks to the front");
         final List<Integer> allTasksInZOrder =
@@ -295,7 +300,15 @@
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
-        return wct;
+    }
+
+    private void moveHomeTaskToFront(WindowContainerTransaction wct) {
+        for (RunningTaskInfo task : mShellTaskOrganizer.getRunningTasks(mContext.getDisplayId())) {
+            if (task.getActivityType() == ACTIVITY_TYPE_HOME) {
+                wct.reorder(task.token, true /* onTop */);
+                return;
+            }
+        }
     }
 
     /**
@@ -354,7 +367,7 @@
         if (wct == null) {
             wct = new WindowContainerTransaction();
         }
-        wct.merge(bringDesktopAppsToFront(), true /* transfer */);
+        bringDesktopAppsToFront(wct);
         wct.reorder(request.getTriggerTask().token, true /* onTop */);
 
         return wct;
@@ -435,5 +448,15 @@
             executeRemoteCallWithTaskPermission(mController, "showDesktopApps",
                     DesktopModeController::showDesktopApps);
         }
+
+        @Override
+        public int getVisibleTaskCount() throws RemoteException {
+            int[] result = new int[1];
+            executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount",
+                    controller -> result[0] = controller.getVisibleTaskCount(),
+                    true /* blocking */
+            );
+            return result[0];
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 600ccc1..47342c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -143,6 +143,13 @@
     }
 
     /**
+     * Get number of tasks that are marked as visible
+     */
+    fun getVisibleTaskCount(): Int {
+        return visibleTasks.size
+    }
+
+    /**
      * Add (or move if it already exists) the task to the top of the ordered list.
      */
     fun addOrMoveFreeformTaskToTop(taskId: Int) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 3341470..73a7403 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -27,6 +27,7 @@
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionInfo
@@ -84,19 +85,24 @@
     fun showDesktopApps() {
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps")
         val wct = WindowContainerTransaction()
-
         bringDesktopAppsToFront(wct)
 
         // Execute transaction if there are pending operations
         if (!wct.isEmpty) {
             if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-                transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
+                // TODO(b/268662477): add animation for the transition
+                transitions.startTransition(TRANSIT_NONE, wct, null /* handler */)
             } else {
                 shellTaskOrganizer.applyTransaction(wct)
             }
         }
     }
 
+    /** Get number of tasks that are marked as visible */
+    fun getVisibleTaskCount(): Int {
+        return desktopModeTaskRepository.getVisibleTaskCount()
+    }
+
     /** Move a task with given `taskId` to desktop */
     fun moveToDesktop(taskId: Int) {
         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) }
@@ -151,18 +157,8 @@
     }
 
     private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) {
-        val activeTasks = desktopModeTaskRepository.getActiveTasks()
-
-        // Skip if all tasks are already visible
-        if (activeTasks.isNotEmpty() && activeTasks.all(desktopModeTaskRepository::isVisibleTask)) {
-            ProtoLog.d(
-                WM_SHELL_DESKTOP_MODE,
-                "bringDesktopAppsToFront: active tasks are already in front, skipping."
-            )
-            return
-        }
-
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront")
+        val activeTasks = desktopModeTaskRepository.getActiveTasks()
 
         // First move home to front and then other tasks on top of it
         moveHomeTaskToFront(wct)
@@ -310,5 +306,16 @@
                 Consumer(DesktopTasksController::showDesktopApps)
             )
         }
+
+        override fun getVisibleTaskCount(): Int {
+            val result = IntArray(1)
+            ExecutorUtils.executeRemoteCallWithTaskPermission(
+                controller,
+                "getVisibleTaskCount",
+                { controller -> result[0] = controller.getVisibleTaskCount() },
+                true /* blocking */
+            )
+            return result[0]
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index 5042bd6..d0739e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -23,4 +23,7 @@
 
     /** Show apps on the desktop */
     void showDesktopApps();
+
+    /** Get count of visible desktop tasks */
+    int getVisibleTaskCount();
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java
new file mode 100644
index 0000000..20da54e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.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.wm.shell.draganddrop;
+
+/** Constants that can be used by both Shell and other users of the library, e.g. Launcher */
+public class DragAndDropConstants {
+
+    /**
+     * An Intent extra that Launcher can use to specify a region of the screen where Shell should
+     * ignore drag events.
+     */
+    public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION";
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index b59fe18..4cfaae6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -36,6 +36,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
 import android.content.ClipDescription;
+import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.PixelFormat;
@@ -58,9 +59,9 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ExternalMainThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -70,7 +71,7 @@
  * Handles the global drag and drop handling for the Shell.
  */
 public class DragAndDropController implements DisplayController.OnDisplaysChangedListener,
-        View.OnDragListener, ConfigurationChangeListener {
+        View.OnDragListener, ComponentCallbacks2 {
 
     private static final String TAG = DragAndDropController.class.getSimpleName();
 
@@ -119,7 +120,6 @@
         mMainExecutor.executeDelayed(() -> {
             mDisplayController.addDisplayWindowListener(this);
         }, 0);
-        mShellController.addConfigurationChangeListener(this);
     }
 
     /**
@@ -180,6 +180,7 @@
         try {
             wm.addView(rootView, layoutParams);
             addDisplayDropTarget(displayId, context, wm, rootView, dragLayout);
+            context.registerComponentCallbacks(this);
         } catch (WindowManager.InvalidDisplayException e) {
             Slog.w(TAG, "Unable to add view for display id: " + displayId);
         }
@@ -209,6 +210,7 @@
         if (pd == null) {
             return;
         }
+        pd.context.unregisterComponentCallbacks(this);
         pd.wm.removeViewImmediate(pd.rootView);
         mDisplayDropTargets.remove(displayId);
     }
@@ -328,18 +330,29 @@
         return mimeTypes;
     }
 
-    @Override
-    public void onThemeChanged() {
-        for (int i = 0; i < mDisplayDropTargets.size(); i++) {
-            mDisplayDropTargets.get(i).dragLayout.onThemeChange();
-        }
-    }
-
+    // Note: Component callbacks are always called on the main thread of the process
+    @ExternalMainThread
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
-        for (int i = 0; i < mDisplayDropTargets.size(); i++) {
-            mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig);
-        }
+        mMainExecutor.execute(() -> {
+            for (int i = 0; i < mDisplayDropTargets.size(); i++) {
+                mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig);
+            }
+        });
+    }
+
+    // Note: Component callbacks are always called on the main thread of the process
+    @ExternalMainThread
+    @Override
+    public void onTrimMemory(int level) {
+        // Do nothing
+    }
+
+    // Note: Component callbacks are always called on the main thread of the process
+    @ExternalMainThread
+    @Override
+    public void onLowMemory() {
+        // Do nothing
     }
 
     private static class PerDisplay {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index d93a901..df94b41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -34,6 +34,7 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION;
 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN;
 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT;
@@ -53,6 +54,7 @@
 import android.content.pm.LauncherApps;
 import android.graphics.Insets;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -86,6 +88,7 @@
     private final Starter mStarter;
     private final SplitScreenController mSplitScreen;
     private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>();
+    private final RectF mDisallowHitRegion = new RectF();
 
     private InstanceId mLoggerSessionId;
     private DragSession mSession;
@@ -111,6 +114,12 @@
         mSession = new DragSession(mActivityTaskManager, displayLayout, data);
         // TODO(b/169894807): Also update the session data with task stack changes
         mSession.update();
+        RectF disallowHitRegion = (RectF) mSession.dragData.getExtra(EXTRA_DISALLOW_HIT_REGION);
+        if (disallowHitRegion == null) {
+            mDisallowHitRegion.setEmpty();
+        } else {
+            mDisallowHitRegion.set(disallowHitRegion);
+        }
     }
 
     /**
@@ -218,6 +227,9 @@
      */
     @Nullable
     Target getTargetAtLocation(int x, int y) {
+        if (mDisallowHitRegion.contains(x, y)) {
+            return null;
+        }
         for (int i = mTargets.size() - 1; i >= 0; i--) {
             DragAndDropPolicy.Target t = mTargets.get(i);
             if (t.hitRegion.contains(x, y)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 55378a8..fe42822a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -18,10 +18,16 @@
 
 import static android.app.StatusBarManager.DISABLE_NONE;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS;
+import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
+import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT;
+import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
+import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -68,6 +74,7 @@
     private final SplitScreenController mSplitScreenController;
     private final IconProvider mIconProvider;
     private final StatusBarManager mStatusBarManager;
+    private final Configuration mLastConfiguration = new Configuration();
 
     private DragAndDropPolicy.Target mCurrentTarget = null;
     private DropZoneView mDropZoneView1;
@@ -88,6 +95,7 @@
         mIconProvider = iconProvider;
         mPolicy = new DragAndDropPolicy(context, splitScreenController);
         mStatusBarManager = context.getSystemService(StatusBarManager.class);
+        mLastConfiguration.setTo(context.getResources().getConfiguration());
 
         mDisplayMargin = context.getResources().getDimensionPixelSize(
                 R.dimen.drop_layout_display_margin);
@@ -114,7 +122,7 @@
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mInsets = insets.getInsets(Type.systemBars() | Type.displayCutout());
+        mInsets = insets.getInsets(Type.tappableElement() | Type.displayCutout());
         recomputeDropTargets();
 
         final int orientation = getResources().getConfiguration().orientation;
@@ -128,11 +136,6 @@
         return super.onApplyWindowInsets(insets);
     }
 
-    public void onThemeChange() {
-        mDropZoneView1.onThemeChange();
-        mDropZoneView2.onThemeChange();
-    }
-
     public void onConfigChanged(Configuration newConfig) {
         if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
                 && getOrientation() != HORIZONTAL) {
@@ -143,6 +146,15 @@
             setOrientation(LinearLayout.VERTICAL);
             updateContainerMargins(newConfig.orientation);
         }
+
+        final int diff = newConfig.diff(mLastConfiguration);
+        final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0
+                || (diff & CONFIG_UI_MODE) != 0;
+        if (themeChanged) {
+            mDropZoneView1.onThemeChange();
+            mDropZoneView2.onThemeChange();
+        }
+        mLastConfiguration.setTo(newConfig);
     }
 
     private void updateContainerMarginsForSingleTask() {
@@ -315,6 +327,25 @@
                 // Switching between targets
                 mDropZoneView1.animateSwitch();
                 mDropZoneView2.animateSwitch();
+                // Announce for accessibility.
+                switch (target.type) {
+                    case TYPE_SPLIT_LEFT:
+                        mDropZoneView1.announceForAccessibility(
+                                mContext.getString(R.string.accessibility_split_left));
+                        break;
+                    case TYPE_SPLIT_RIGHT:
+                        mDropZoneView2.announceForAccessibility(
+                                mContext.getString(R.string.accessibility_split_right));
+                        break;
+                    case TYPE_SPLIT_TOP:
+                        mDropZoneView1.announceForAccessibility(
+                                mContext.getString(R.string.accessibility_split_top));
+                        break;
+                    case TYPE_SPLIT_BOTTOM:
+                        mDropZoneView2.announceForAccessibility(
+                                mContext.getString(R.string.accessibility_split_bottom));
+                        break;
+                }
             }
             mCurrentTarget = target;
         }
@@ -346,7 +377,9 @@
 
         // Start animating the drop UI out with the drag surface
         hide(event, dropCompleteCallback);
-        hideDragSurface(dragSurface);
+        if (handledDrop) {
+            hideDragSurface(dragSurface);
+        }
         return handledDrop;
     }
 
@@ -424,12 +457,10 @@
     }
 
     private void animateHighlight(DragAndDropPolicy.Target target) {
-        if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_LEFT
-                || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_TOP) {
+        if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) {
             mDropZoneView1.setShowingHighlight(true);
             mDropZoneView2.setShowingHighlight(false);
-        } else if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT
-                || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM) {
+        } else if (target.type == TYPE_SPLIT_RIGHT || target.type == TYPE_SPLIT_BOTTOM) {
             mDropZoneView1.setShowingHighlight(false);
             mDropZoneView2.setShowingHighlight(true);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index b9caf62..d094c22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -108,13 +108,19 @@
         }
         if (!createdWindowDecor) {
             mSyncQueue.runInSync(t -> {
+                if (!leash.isValid()) {
+                    // Task vanished before sync completion
+                    return;
+                }
                 // Reset several properties back to fullscreen (PiP, for example, leaves all these
                 // properties in a bad state).
                 t.setWindowCrop(leash, null);
                 t.setPosition(leash, positionInParent.x, positionInParent.y);
                 t.setAlpha(leash, 1f);
                 t.setMatrix(leash, 1, 0, 0, 1);
-                t.show(leash);
+                if (taskInfo.isVisible) {
+                    t.show(leash);
+                }
             });
         }
     }
@@ -134,6 +140,10 @@
         final Point positionInParent = state.mTaskInfo.positionInParent;
         if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) {
             mSyncQueue.runInSync(t -> {
+                if (!state.mLeash.isValid()) {
+                    // Task vanished before sync completion
+                    return;
+                }
                 t.setPosition(state.mLeash, positionInParent.x, positionInParent.y);
             });
         }
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/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 6728c00..23f73f6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -28,6 +28,7 @@
 import android.annotation.NonNull;
 import android.app.TaskInfo;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -208,7 +209,7 @@
     /**
      * Quietly cancel the animator by removing the listeners first.
      */
-    static void quietCancel(@NonNull ValueAnimator animator) {
+    public static void quietCancel(@NonNull ValueAnimator animator) {
         animator.removeAllUpdateListeners();
         animator.removeAllListeners();
         animator.cancel();
@@ -361,22 +362,26 @@
         }
 
         void setColorContentOverlay(Context context) {
-            final SurfaceControl.Transaction tx =
-                    mSurfaceControlTransactionFactory.getTransaction();
-            if (mContentOverlay != null) {
-                mContentOverlay.detach(tx);
-            }
-            mContentOverlay = new PipContentOverlay.PipColorOverlay(context);
-            mContentOverlay.attach(tx, mLeash);
+            reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context));
         }
 
         void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
+            reattachContentOverlay(
+                    new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint));
+        }
+
+        void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo) {
+            reattachContentOverlay(
+                    new PipContentOverlay.PipAppIconOverlay(context, bounds, activityInfo));
+        }
+
+        private void reattachContentOverlay(PipContentOverlay overlay) {
             final SurfaceControl.Transaction tx =
                     mSurfaceControlTransactionFactory.getTransaction();
             if (mContentOverlay != null) {
                 mContentOverlay.detach(tx);
             }
-            mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint);
+            mContentOverlay = overlay;
             mContentOverlay.attach(tx, mLeash);
         }
 
@@ -570,8 +575,9 @@
                     final Rect base = getBaseValue();
                     final Rect start = getStartValue();
                     final Rect end = getEndValue();
+                    Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
                     if (mContentOverlay != null) {
-                        mContentOverlay.onAnimationUpdate(tx, fraction);
+                        mContentOverlay.onAnimationUpdate(tx, bounds, fraction);
                     }
                     if (rotatedEndRect != null) {
                         // Animate the bounds in a different orientation. It only happens when
@@ -579,7 +585,6 @@
                         applyRotation(tx, leash, fraction, start, end);
                         return;
                     }
-                    Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
                     float angle = (1.0f - fraction) * startingAngle;
                     setCurrentValue(bounds);
                     if (inScaleTransition() || sourceHintRect == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index cd61dbb..867162b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -16,24 +16,19 @@
 
 package com.android.wm.shell.pip;
 
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.PictureInPictureParams;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.DisplayMetrics;
 import android.util.Size;
-import android.util.TypedValue;
 import android.view.Gravity;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 
 import java.io.PrintWriter;
 
@@ -45,33 +40,29 @@
     private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
     private static final float INVALID_SNAP_FRACTION = -1f;
 
-    private final @NonNull PipBoundsState mPipBoundsState;
+    @NonNull private final PipBoundsState mPipBoundsState;
+    @NonNull protected final PipSizeSpecHandler mPipSizeSpecHandler;
     private final PipSnapAlgorithm mSnapAlgorithm;
-    private final PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+    private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
 
-    private float mDefaultSizePercent;
-    private float mMinAspectRatioForMinSize;
-    private float mMaxAspectRatioForMinSize;
     private float mDefaultAspectRatio;
     private float mMinAspectRatio;
     private float mMaxAspectRatio;
     private int mDefaultStackGravity;
-    private int mDefaultMinSize;
-    private int mOverridableMinSize;
-    protected Point mScreenEdgeInsets;
 
     public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
             @NonNull PipSnapAlgorithm pipSnapAlgorithm,
-            @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) {
+            @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
         mPipBoundsState = pipBoundsState;
         mSnapAlgorithm = pipSnapAlgorithm;
         mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         reloadResources(context);
         // Initialize the aspect ratio to the default aspect ratio.  Don't do this in reload
         // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
         // triggers a configuration change and the resources to be reloaded.
         mPipBoundsState.setAspectRatio(mDefaultAspectRatio);
-        mPipBoundsState.setMinEdgeSize(mDefaultMinSize);
     }
 
     /**
@@ -83,27 +74,15 @@
                 R.dimen.config_pictureInPictureDefaultAspectRatio);
         mDefaultStackGravity = res.getInteger(
                 R.integer.config_defaultPictureInPictureGravity);
-        mDefaultMinSize = res.getDimensionPixelSize(
-                R.dimen.default_minimal_size_pip_resizable_task);
-        mOverridableMinSize = res.getDimensionPixelSize(
-                R.dimen.overridable_minimal_size_pip_resizable_task);
         final String screenEdgeInsetsDpString = res.getString(
                 R.string.config_defaultPictureInPictureScreenEdgeInsets);
         final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
                 ? Size.parseSize(screenEdgeInsetsDpString)
                 : null;
-        mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
-                : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()),
-                        dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics()));
         mMinAspectRatio = res.getFloat(
                 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
         mMaxAspectRatio = res.getFloat(
                 com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
-        mDefaultSizePercent = res.getFloat(
-                R.dimen.config_pictureInPictureDefaultSizePercent);
-        mMaxAspectRatioForMinSize = res.getFloat(
-                R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
-        mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
     }
 
     /**
@@ -180,8 +159,9 @@
         if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) {
             // If either dimension is smaller than the allowed minimum, adjust them
             // according to mOverridableMinSize
-            return new Size(Math.max(windowLayout.minWidth, mOverridableMinSize),
-                    Math.max(windowLayout.minHeight, mOverridableMinSize));
+            return new Size(
+                    Math.max(windowLayout.minWidth, mPipSizeSpecHandler.getOverrideMinEdgeSize()),
+                    Math.max(windowLayout.minHeight, mPipSizeSpecHandler.getOverrideMinEdgeSize()));
         }
         return null;
     }
@@ -243,28 +223,13 @@
         final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
                 getMovementBounds(stackBounds), mPipBoundsState.getStashedState());
 
-        final Size overrideMinSize = mPipBoundsState.getOverrideMinSize();
         final Size size;
         if (useCurrentMinEdgeSize || useCurrentSize) {
-            // The default minimum edge size, or the override min edge size if set.
-            final int defaultMinEdgeSize = overrideMinSize == null ? mDefaultMinSize
-                    : mPipBoundsState.getOverrideMinEdgeSize();
-            final int minEdgeSize = useCurrentMinEdgeSize ? mPipBoundsState.getMinEdgeSize()
-                    : defaultMinEdgeSize;
-            // Use the existing size but adjusted to the aspect ratio and min edge size.
-            size = getSizeForAspectRatio(
-                    new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize);
+            // Use the existing size but adjusted to the new aspect ratio.
+            size = mPipSizeSpecHandler.getSizeForAspectRatio(
+                    new Size(stackBounds.width(), stackBounds.height()), aspectRatio);
         } else {
-            if (overrideMinSize != null) {
-                // The override minimal size is set, use that as the default size making sure it's
-                // adjusted to the aspect ratio.
-                size = adjustSizeToAspectRatio(overrideMinSize, aspectRatio);
-            } else {
-                // Calculate the default size using the display size and default min edge size.
-                final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout();
-                size = getSizeForAspectRatio(aspectRatio, mDefaultMinSize,
-                        displayLayout.width(), displayLayout.height());
-            }
+            size = mPipSizeSpecHandler.getDefaultSize(aspectRatio);
         }
 
         final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
@@ -273,18 +238,6 @@
         mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
     }
 
-    /** Adjusts the given size to conform to the given aspect ratio. */
-    private Size adjustSizeToAspectRatio(@NonNull Size size, float aspectRatio) {
-        final float sizeAspectRatio = size.getWidth() / (float) size.getHeight();
-        if (sizeAspectRatio > aspectRatio) {
-            // Size is wider, fix the width and increase the height
-            return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio));
-        } else {
-            // Size is taller, fix the height and adjust the width.
-            return new Size((int) (size.getHeight() * aspectRatio), size.getHeight());
-        }
-    }
-
     /**
      * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are
      * provided, then it will apply the default bounds to the provided snap fraction and size.
@@ -303,17 +256,9 @@
         final Size defaultSize;
         final Rect insetBounds = new Rect();
         getInsetBounds(insetBounds);
-        final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout();
-        final Size overrideMinSize = mPipBoundsState.getOverrideMinSize();
-        if (overrideMinSize != null) {
-            // The override minimal size is set, use that as the default size making sure it's
-            // adjusted to the aspect ratio.
-            defaultSize = adjustSizeToAspectRatio(overrideMinSize, mDefaultAspectRatio);
-        } else {
-            // Calculate the default size using the display size and default min edge size.
-            defaultSize = getSizeForAspectRatio(mDefaultAspectRatio,
-                    mDefaultMinSize, displayLayout.width(), displayLayout.height());
-        }
+
+        // Calculate the default size
+        defaultSize = mPipSizeSpecHandler.getDefaultSize(mDefaultAspectRatio);
 
         // Now that we have the default size, apply the snap fraction if valid or position the
         // bounds using the default gravity.
@@ -335,12 +280,7 @@
      * Populates the bounds on the screen that the PIP can be visible in.
      */
     public void getInsetBounds(Rect outRect) {
-        final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout();
-        Rect insets = mPipBoundsState.getDisplayLayout().stableInsets();
-        outRect.set(insets.left + mScreenEdgeInsets.x,
-                insets.top + mScreenEdgeInsets.y,
-                displayLayout.width() - insets.right - mScreenEdgeInsets.x,
-                displayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
+        outRect.set(mPipSizeSpecHandler.getInsetBounds());
     }
 
     /**
@@ -405,71 +345,11 @@
         mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction);
     }
 
-    public int getDefaultMinSize() {
-        return mDefaultMinSize;
-    }
-
     /**
      * @return the pixels for a given dp value.
      */
     private int dpToPx(float dpValue, DisplayMetrics dm) {
-        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
-    }
-
-    /**
-     * @return the size of the PiP at the given aspectRatio, ensuring that the minimum edge
-     * is at least minEdgeSize.
-     */
-    public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
-            int displayHeight) {
-        final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
-        final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
-
-        final int width;
-        final int height;
-        if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
-            // Beyond these points, we can just use the min size as the shorter edge
-            if (aspectRatio <= 1) {
-                // Portrait, width is the minimum size
-                width = minSize;
-                height = Math.round(width / aspectRatio);
-            } else {
-                // Landscape, height is the minimum size
-                height = minSize;
-                width = Math.round(height * aspectRatio);
-            }
-        } else {
-            // Within these points, we ensure that the bounds fit within the radius of the limits
-            // at the points
-            final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
-            final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
-            height = (int) Math.round(Math.sqrt((radius * radius)
-                    / (aspectRatio * aspectRatio + 1)));
-            width = Math.round(height * aspectRatio);
-        }
-        return new Size(width, height);
-    }
-
-    /**
-     * @return the adjusted size so that it conforms to the given aspectRatio, ensuring that the
-     * minimum edge is at least minEdgeSize.
-     */
-    public Size getSizeForAspectRatio(Size size, float aspectRatio, float minEdgeSize) {
-        final int smallestSize = Math.min(size.getWidth(), size.getHeight());
-        final int minSize = (int) Math.max(minEdgeSize, smallestSize);
-
-        final int width;
-        final int height;
-        if (aspectRatio <= 1) {
-            // Portrait, width is the minimum size.
-            width = minSize;
-            height = Math.round(width / aspectRatio);
-        } else {
-            // Landscape, height is the minimum size
-            height = minSize;
-            width = Math.round(height * aspectRatio);
-        }
-        return new Size(width, height);
+        return PipUtils.dpToPx(dpValue, dm);
     }
 
     /**
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 5376ae3..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
@@ -37,6 +37,7 @@
 import com.android.internal.util.function.TriConsumer;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.io.PrintWriter;
@@ -83,13 +84,10 @@
     private int mStashedState = STASH_TYPE_NONE;
     private int mStashOffset;
     private @Nullable PipReentryState mPipReentryState;
+    private final @Nullable PipSizeSpecHandler mPipSizeSpecHandler;
     private @Nullable ComponentName mLastPipComponentName;
     private int mDisplayId = Display.DEFAULT_DISPLAY;
     private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout();
-    /** The current minimum edge size of PIP. */
-    private int mMinEdgeSize;
-    /** The preferred minimum (and default) size specified by apps. */
-    private @Nullable Size mOverrideMinSize;
     private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState();
     private boolean mIsImeShowing;
     private int mImeHeight;
@@ -122,9 +120,10 @@
     private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
     private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>();
 
-    public PipBoundsState(@NonNull Context context) {
+    public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler) {
         mContext = context;
         reloadResources();
+        mPipSizeSpecHandler = pipSizeSpecHandler;
     }
 
     /** Reloads the resources. */
@@ -312,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
@@ -323,20 +322,10 @@
         mPipReentryState = null;
     }
 
-    /** Set the PIP minimum edge size. */
-    public void setMinEdgeSize(int minEdgeSize) {
-        mMinEdgeSize = minEdgeSize;
-    }
-
-    /** Returns the PIP's current minimum edge size. */
-    public int getMinEdgeSize() {
-        return mMinEdgeSize;
-    }
-
     /** Sets the preferred size of PIP as specified by the activity in PIP mode. */
     public void setOverrideMinSize(@Nullable Size overrideMinSize) {
-        final boolean changed = !Objects.equals(overrideMinSize, mOverrideMinSize);
-        mOverrideMinSize = overrideMinSize;
+        final boolean changed = !Objects.equals(overrideMinSize, getOverrideMinSize());
+        mPipSizeSpecHandler.setOverrideMinSize(overrideMinSize);
         if (changed && mOnMinimalSizeChangeCallback != null) {
             mOnMinimalSizeChangeCallback.run();
         }
@@ -345,13 +334,12 @@
     /** Returns the preferred minimal size specified by the activity in PIP. */
     @Nullable
     public Size getOverrideMinSize() {
-        return mOverrideMinSize;
+        return mPipSizeSpecHandler.getOverrideMinSize();
     }
 
     /** Returns the minimum edge size of the override minimum size, or 0 if not set. */
     public int getOverrideMinEdgeSize() {
-        if (mOverrideMinSize == null) return 0;
-        return Math.min(mOverrideMinSize.getWidth(), mOverrideMinSize.getHeight());
+        return mPipSizeSpecHandler.getOverrideMinEdgeSize();
     }
 
     /** Get the state of the bounds in motion. */
@@ -581,11 +569,8 @@
         pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName);
         pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio);
         pw.println(innerPrefix + "mDisplayId=" + mDisplayId);
-        pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout);
         pw.println(innerPrefix + "mStashedState=" + mStashedState);
         pw.println(innerPrefix + "mStashOffset=" + mStashOffset);
-        pw.println(innerPrefix + "mMinEdgeSize=" + mMinEdgeSize);
-        pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize);
         pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
         pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
         pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 7096a64..480bf93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -16,10 +16,21 @@
 
 package com.android.wm.shell.pip;
 
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
+import android.annotation.Nullable;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.window.TaskSnapshot;
@@ -41,13 +52,20 @@
         }
     }
 
+    @Nullable
+    public SurfaceControl getLeash() {
+        return mLeash;
+    }
+
     /**
      * Animates the internal {@link #mLeash} by a given fraction.
      * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
      *                 call apply on this transaction, it should be applied on the caller side.
+     * @param currentBounds {@link Rect} of the current animation bounds.
      * @param fraction progress of the animation ranged from 0f to 1f.
      */
-    public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction);
+    public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+            Rect currentBounds, float fraction);
 
     /**
      * Callback when reaches the end of animation on the internal {@link #mLeash}.
@@ -60,13 +78,15 @@
 
     /** A {@link PipContentOverlay} uses solid color. */
     public static final class PipColorOverlay extends PipContentOverlay {
+        private static final String TAG = PipColorOverlay.class.getSimpleName();
+
         private final Context mContext;
 
         public PipColorOverlay(Context context) {
             mContext = context;
             mLeash = new SurfaceControl.Builder(new SurfaceSession())
-                    .setCallsite("PipAnimation")
-                    .setName(PipColorOverlay.class.getSimpleName())
+                    .setCallsite(TAG)
+                    .setName(TAG)
                     .setColorLayer()
                     .build();
         }
@@ -82,7 +102,8 @@
         }
 
         @Override
-        public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+        public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+                Rect currentBounds, float fraction) {
             atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
         }
 
@@ -108,6 +129,8 @@
 
     /** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
     public static final class PipSnapshotOverlay extends PipContentOverlay {
+        private static final String TAG = PipSnapshotOverlay.class.getSimpleName();
+
         private final TaskSnapshot mSnapshot;
         private final Rect mSourceRectHint;
 
@@ -115,8 +138,8 @@
             mSnapshot = snapshot;
             mSourceRectHint = new Rect(sourceRectHint);
             mLeash = new SurfaceControl.Builder(new SurfaceSession())
-                    .setCallsite("PipAnimation")
-                    .setName(PipSnapshotOverlay.class.getSimpleName())
+                    .setCallsite(TAG)
+                    .setName(TAG)
                     .build();
         }
 
@@ -137,7 +160,8 @@
         }
 
         @Override
-        public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+        public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+                Rect currentBounds, float fraction) {
             // Do nothing. Keep the snapshot till animation ends.
         }
 
@@ -146,4 +170,113 @@
             atomicTx.remove(mLeash);
         }
     }
+
+    /** A {@link PipContentOverlay} shows app icon on solid color background. */
+    public static final class PipAppIconOverlay extends PipContentOverlay {
+        private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+        private static final int APP_ICON_SIZE_DP = 48;
+
+        private final Context mContext;
+        private final int mAppIconSizePx;
+        private final Rect mAppBounds;
+        private final Matrix mTmpTransform = new Matrix();
+        private final float[] mTmpFloat9 = new float[9];
+
+        private Bitmap mBitmap;
+
+        public PipAppIconOverlay(Context context, Rect appBounds, ActivityInfo activityInfo) {
+            mContext = context;
+            mAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, APP_ICON_SIZE_DP,
+                    context.getResources().getDisplayMetrics());
+            mAppBounds = new Rect(appBounds);
+            mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(),
+                    Bitmap.Config.ARGB_8888);
+            prepareAppIconOverlay(activityInfo);
+            mLeash = new SurfaceControl.Builder(new SurfaceSession())
+                    .setCallsite(TAG)
+                    .setName(TAG)
+                    .build();
+        }
+
+        @Override
+        public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+            tx.show(mLeash);
+            tx.setLayer(mLeash, Integer.MAX_VALUE);
+            tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+            tx.reparent(mLeash, parentLeash);
+            tx.apply();
+        }
+
+        @Override
+        public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+                Rect currentBounds, float fraction) {
+            mTmpTransform.reset();
+            // Scale back the bitmap with the pivot point at center.
+            mTmpTransform.postScale(
+                    (float) mAppBounds.width() / currentBounds.width(),
+                    (float) mAppBounds.height() / currentBounds.height(),
+                    mAppBounds.centerX(),
+                    mAppBounds.centerY());
+            atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+                    .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+        }
+
+        @Override
+        public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
+            atomicTx.remove(mLeash);
+        }
+
+        @Override
+        public void detach(SurfaceControl.Transaction tx) {
+            super.detach(tx);
+            if (mBitmap != null && !mBitmap.isRecycled()) {
+                mBitmap.recycle();
+            }
+        }
+
+        private void prepareAppIconOverlay(ActivityInfo activityInfo) {
+            final Canvas canvas = new Canvas();
+            canvas.setBitmap(mBitmap);
+            final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+                    android.R.attr.colorBackground });
+            try {
+                int colorAccent = ta.getColor(0, 0);
+                canvas.drawRGB(
+                        Color.red(colorAccent),
+                        Color.green(colorAccent),
+                        Color.blue(colorAccent));
+            } finally {
+                ta.recycle();
+            }
+            final Drawable appIcon = loadActivityInfoIcon(activityInfo,
+                    mContext.getResources().getConfiguration().densityDpi);
+            final Rect appIconBounds = new Rect(
+                    mAppBounds.centerX() - mAppIconSizePx / 2,
+                    mAppBounds.centerY() - mAppIconSizePx / 2,
+                    mAppBounds.centerX() + mAppIconSizePx / 2,
+                    mAppBounds.centerY() + mAppIconSizePx / 2);
+            appIcon.setBounds(appIconBounds);
+            appIcon.draw(canvas);
+            mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+        }
+
+        // Copied from com.android.launcher3.icons.IconProvider#loadActivityInfoIcon
+        private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
+            final int iconRes = ai.getIconResource();
+            Drawable icon = null;
+            // Get the preferred density icon from the app's resources
+            if (density != 0 && iconRes != 0) {
+                try {
+                    final Resources resources = mContext.getPackageManager()
+                            .getResourcesForApplication(ai.applicationInfo);
+                    icon = resources.getDrawableForDensity(iconRes, density);
+                } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { }
+            }
+            // Get the default density icon
+            if (icon == null) {
+                icon = ai.loadIcon(mContext.getPackageManager());
+            }
+            return icon;
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
similarity index 97%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
index e3495e1..5045cf9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
@@ -24,7 +24,7 @@
  * Interface for interacting with keep clear algorithm used to move PiP window out of the way of
  * keep clear areas.
  */
-public interface PipKeepClearAlgorithm {
+public interface PipKeepClearAlgorithmInterface {
 
     /**
      * Adjust the position of picture in picture window based on the registered keep clear areas.
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 8ba2583..f11836e 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
@@ -62,6 +62,7 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.Display;
@@ -78,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;
@@ -125,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;
@@ -312,6 +316,7 @@
             @NonNull SyncTransactionQueue syncTransactionQueue,
             @NonNull PipTransitionState pipTransitionState,
             @NonNull PipBoundsState pipBoundsState,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler,
             @NonNull PipBoundsAlgorithm boundsHandler,
             @NonNull PipMenuController pipMenuController,
             @NonNull PipAnimationController pipAnimationController,
@@ -327,6 +332,7 @@
         mSyncTransactionQueue = syncTransactionQueue;
         mPipTransitionState = pipTransitionState;
         mPipBoundsState = pipBoundsState;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         mPipBoundsAlgorithm = boundsHandler;
         mPipMenuController = pipMenuController;
         mPipTransitionController = pipTransitionController;
@@ -644,7 +650,6 @@
         }
 
         mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo);
-        mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER);
 
         // If the displayId of the task is different than what PipBoundsHandler has, then update
         // it. This is possible if we entered PiP on an external display.
@@ -653,6 +658,17 @@
             mOnDisplayIdChangeCallback.accept(info.displayId);
         }
 
+        // UiEvent logging.
+        final PipUiEventLogger.PipUiEventEnum uiEventEnum;
+        if (isLaunchIntoPipTask()) {
+            uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER_CONTENT_PIP;
+        } else if (mPipTransitionState.getInSwipePipToHomeTransition()) {
+            uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_AUTO_ENTER;
+        } else {
+            uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER;
+        }
+        mPipUiEventLoggerLogger.log(uiEventEnum);
+
         if (mPipTransitionState.getInSwipePipToHomeTransition()) {
             if (!mWaitForFixedRotation) {
                 onEndOfSwipePipToHomeTransition();
@@ -1403,7 +1419,6 @@
             @PipAnimationController.TransitionDirection int direction,
             @PipAnimationController.AnimationType int type) {
         final Rect preResizeBounds = new Rect(mPipBoundsState.getBounds());
-        final boolean isPipTopLeft = isPipTopLeft();
         mPipBoundsState.setBounds(destinationBounds);
         if (direction == TRANSITION_DIRECTION_REMOVE_STACK) {
             removePipImmediately();
@@ -1449,9 +1464,11 @@
                             null /* callback */, false /* withStartDelay */);
                 });
             } else {
-                applyFinishBoundsResize(wct, direction, isPipTopLeft);
+                applyFinishBoundsResize(wct, direction, false);
             }
         } else {
+            final boolean isPipTopLeft =
+                    direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN && isPipToTopLeft();
             applyFinishBoundsResize(wct, direction, isPipTopLeft);
         }
 
@@ -1520,6 +1537,14 @@
         return topLeft.contains(mPipBoundsState.getBounds());
     }
 
+    private boolean isPipToTopLeft() {
+        if (!mSplitScreenOptional.isPresent()) {
+            return false;
+        }
+        return mSplitScreenOptional.get().getActivateSplitPosition(mTaskInfo)
+                == SPLIT_POSITION_TOP_OR_LEFT;
+    }
+
     /**
      * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined
      * and can be overridden to restore to an alternate windowing mode.
@@ -1568,7 +1593,13 @@
             // Similar to auto-enter-pip transition, we use content overlay when there is no
             // source rect hint to enter PiP use bounds animation.
             if (sourceHintRect == null) {
-                animator.setColorContentOverlay(mContext);
+                if (SystemProperties.getBoolean(
+                        "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+                    animator.setAppIconContentOverlay(
+                            mContext, currentBounds, mTaskInfo.topActivityInfo);
+                } else {
+                    animator.setColorContentOverlay(mContext);
+                }
             } else {
                 final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
                         mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);
@@ -1594,7 +1625,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.
@@ -1645,8 +1681,7 @@
         final Rect topLeft = new Rect();
         final Rect bottomRight = new Rect();
         mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight);
-        final boolean isPipTopLeft = isPipTopLeft();
-        destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight);
+        destinationBoundsOut.set(isPipToTopLeft()  ? topLeft : bottomRight);
         return true;
     }
 
@@ -1730,6 +1765,11 @@
         mSurfaceControlTransactionFactory = factory;
     }
 
+    public boolean isLaunchToSplit(TaskInfo taskInfo) {
+        return mSplitScreenOptional.isPresent()
+                && mSplitScreenOptional.get().isLaunchToSplit(taskInfo);
+    }
+
     /**
      * Dumps internal states.
      */
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 e6c7e10..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
@@ -50,6 +50,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.IBinder;
+import android.os.SystemProperties;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
@@ -64,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;
@@ -82,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;
@@ -112,6 +116,7 @@
             @NonNull ShellTaskOrganizer shellTaskOrganizer,
             @NonNull Transitions transitions,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipTransitionState pipTransitionState,
             PipMenuController pipMenuController,
             PipBoundsAlgorithm pipBoundsAlgorithm,
@@ -122,6 +127,7 @@
                 pipBoundsAlgorithm, pipAnimationController);
         mContext = context;
         mPipTransitionState = pipTransitionState;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         mEnterExitAnimationDuration = context.getResources()
                 .getInteger(R.integer.config_pipResizeAnimationDuration);
         mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
@@ -307,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;
@@ -662,8 +673,8 @@
             }
 
             // Please file a bug to handle the unexpected transition type.
-            throw new IllegalStateException("Entering PIP with unexpected transition type="
-                    + transitTypeToString(transitType));
+            android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type="
+                    + transitTypeToString(transitType), new Throwable());
         }
         return false;
     }
@@ -792,7 +803,13 @@
             if (sourceHintRect == null) {
                 // We use content overlay when there is no source rect hint to enter PiP use bounds
                 // animation.
-                animator.setColorContentOverlay(mContext);
+                if (SystemProperties.getBoolean(
+                        "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+                    animator.setAppIconContentOverlay(
+                            mContext, currentBounds, taskInfo.topActivityInfo);
+                } else {
+                    animator.setColorContentOverlay(mContext);
+                }
             }
         } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
             animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
@@ -817,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/PipUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java
index 513ebba..3e5a19b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java
@@ -78,6 +78,12 @@
         @UiEvent(doc = "Activity enters picture-in-picture mode")
         PICTURE_IN_PICTURE_ENTER(603),
 
+        @UiEvent(doc = "Activity enters picture-in-picture mode with auto-enter-pip API")
+        PICTURE_IN_PICTURE_AUTO_ENTER(1313),
+
+        @UiEvent(doc = "Activity enters picture-in-picture mode from content-pip API")
+        PICTURE_IN_PICTURE_ENTER_CONTENT_PIP(1314),
+
         @UiEvent(doc = "Expands from picture-in-picture to fullscreen")
         PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN(604),
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
index fa00619..8b98790 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
 
 import android.annotation.Nullable;
 import android.app.ActivityTaskManager;
@@ -26,8 +27,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.RemoteException;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.Pair;
+import android.util.TypedValue;
 import android.window.TaskSnapshot;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -70,6 +73,13 @@
     }
 
     /**
+     * @return the pixels for a given dp value.
+     */
+    public static int dpToPx(float dpValue, DisplayMetrics dm) {
+        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
+    }
+
+    /**
      * @return true if the aspect ratios differ
      */
     public static boolean aspectRatioChanged(float aspectRatio1, float aspectRatio2) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index 690505e..ed8dc7de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -26,14 +26,14 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 
 import java.util.Set;
 
 /**
  * Calculates the adjusted position that does not occlude keep clear areas.
  */
-public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm {
+public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterface {
 
     private boolean mKeepClearAreaGravityEnabled =
             SystemProperties.getBoolean(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 281ea53..431bd7b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -333,6 +333,9 @@
         mTmpDestinationRectF.set(destinationBounds);
         mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
         final SurfaceControl surfaceControl = getSurfaceControl();
+        if (surfaceControl == null) {
+            return;
+        }
         final SurfaceControl.Transaction menuTx =
                 mSurfaceControlTransactionFactory.getTransaction();
         menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
@@ -359,6 +362,9 @@
         }
 
         final SurfaceControl surfaceControl = getSurfaceControl();
+        if (surfaceControl == null) {
+            return;
+        }
         final SurfaceControl.Transaction menuTx =
                 mSurfaceControlTransactionFactory.getTransaction();
         menuTx.setCrop(surfaceControl, destinationBounds);
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 01d81ff..fa3efeb 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
@@ -46,8 +46,6 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.os.UserManager;
 import android.util.Pair;
 import android.util.Size;
 import android.view.DisplayInfo;
@@ -85,7 +83,7 @@
 import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -137,8 +135,9 @@
     private PipAppOpsListener mAppOpsListener;
     private PipMediaController mMediaController;
     private PipBoundsAlgorithm mPipBoundsAlgorithm;
-    private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+    private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
     private PipBoundsState mPipBoundsState;
+    private PipSizeSpecHandler mPipSizeSpecHandler;
     private PipMotionHelper mPipMotionHelper;
     private PipTouchHandler mTouchHandler;
     private PipTransitionController mPipTransitionController;
@@ -380,8 +379,9 @@
             PipAnimationController pipAnimationController,
             PipAppOpsListener pipAppOpsListener,
             PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm,
+            PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipMotionHelper pipMotionHelper,
             PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController,
@@ -403,11 +403,11 @@
 
         return new PipController(context, shellInit, shellCommandHandler, shellController,
                 displayController, pipAnimationController, pipAppOpsListener,
-                pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper,
-                pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
-                pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
-                taskStackListener, pipParamsChangedForwarder, displayInsetsController,
-                oneHandedController, mainExecutor)
+                pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler,
+                pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
+                pipTransitionState, pipTouchHandler, pipTransitionController,
+                windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
+                displayInsetsController, oneHandedController, mainExecutor)
                 .mImpl;
     }
 
@@ -419,8 +419,9 @@
             PipAnimationController pipAnimationController,
             PipAppOpsListener pipAppOpsListener,
             PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm,
+            PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
             @NonNull PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipMotionHelper pipMotionHelper,
             PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController,
@@ -446,6 +447,7 @@
         mPipBoundsAlgorithm = pipBoundsAlgorithm;
         mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
         mPipBoundsState = pipBoundsState;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         mPipMotionHelper = pipMotionHelper;
         mPipTaskOrganizer = pipTaskOrganizer;
         mPipTransitionState = pipTransitionState;
@@ -514,7 +516,10 @@
         // Ensure that we have the display info in case we get calls to update the bounds before the
         // listener calls back
         mPipBoundsState.setDisplayId(mContext.getDisplayId());
-        mPipBoundsState.setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay()));
+
+        DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay());
+        mPipSizeSpecHandler.setDisplayLayout(layout);
+        mPipBoundsState.setDisplayLayout(layout);
 
         try {
             mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener);
@@ -565,8 +570,12 @@
                         if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
                             return;
                         }
-                        mTouchHandler.getMotionHelper().expandLeavePip(
-                                clearedTask /* skipAnimation */);
+                        if (mPipTaskOrganizer.isLaunchToSplit(task)) {
+                            mTouchHandler.getMotionHelper().expandIntoSplit();
+                        } else {
+                            mTouchHandler.getMotionHelper().expandLeavePip(
+                                    clearedTask /* skipAnimation */);
+                        }
                     }
                 });
 
@@ -618,7 +627,7 @@
                             return;
                         }
                         int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
-                        onDisplayChanged(
+                        onDisplayChangedUncheck(
                                 mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()),
                                 false /* saveRestoreSnapFraction */);
                         int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
@@ -688,6 +697,7 @@
         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
         mTouchHandler.onConfigurationChanged();
         mPipBoundsState.onConfigurationChanged();
+        mPipSizeSpecHandler.onConfigurationChanged();
     }
 
     @Override
@@ -704,13 +714,26 @@
     }
 
     private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) {
-        if (mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) {
-            return;
+        if (!mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) {
+            PipAnimationController.PipTransitionAnimator animator =
+                    mPipAnimationController.getCurrentAnimator();
+            if (animator != null && animator.isRunning()) {
+                // cancel any running animator, as it is using stale display layout information
+                PipAnimationController.quietCancel(animator);
+            }
+            onDisplayChangedUncheck(layout, saveRestoreSnapFraction);
         }
+    }
+
+    private void onDisplayChangedUncheck(DisplayLayout layout, boolean saveRestoreSnapFraction) {
         Runnable updateDisplayLayout = () -> {
             final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
                     && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
+
+            // update the internal state of objects subscribed to display changes
+            mPipSizeSpecHandler.setDisplayLayout(layout);
             mPipBoundsState.setDisplayLayout(layout);
+
             final WindowContainerTransaction wct =
                     fromRotation ? new WindowContainerTransaction() : null;
             updateMovementBounds(null /* toBounds */,
@@ -814,7 +837,7 @@
     @Override
     public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
             boolean animatingDismiss) {
-        if (!mPipTaskOrganizer.isInPip()) {
+        if (!mPipTransitionState.hasEnteredPip()) {
             return;
         }
         if (visible) {
@@ -1019,7 +1042,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);
     }
 
     /**
@@ -1056,7 +1083,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.
@@ -1082,6 +1113,7 @@
         mPipTaskOrganizer.dump(pw, innerPrefix);
         mPipBoundsState.dump(pw, innerPrefix);
         mPipInputConsumer.dump(pw, innerPrefix);
+        mPipSizeSpecHandler.dump(pw, innerPrefix);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
index 7619646..9729a40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
@@ -235,21 +235,14 @@
 
     /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */
     public void createOrUpdateDismissTarget() {
-        if (!mTargetViewContainer.isAttachedToWindow()) {
+        if (mTargetViewContainer.getParent() == null) {
             mTargetViewContainer.cancelAnimators();
 
             mTargetViewContainer.setVisibility(View.INVISIBLE);
             mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this);
             mHasDismissTargetSurface = false;
 
-            try {
-                mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams());
-            } catch (IllegalStateException e) {
-                // This shouldn't happen, but if the target is already added, just update its layout
-                // params.
-                mWindowManager.updateViewLayout(
-                        mTargetViewContainer, getDismissTargetLayoutParams());
-            }
+            mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams());
         } else {
             mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams());
         }
@@ -306,7 +299,7 @@
      * Removes the dismiss target and cancels any pending callbacks to show it.
      */
     public void cleanUpDismissTarget() {
-        if (mTargetViewContainer.isAttachedToWindow()) {
+        if (mTargetViewContainer.getParent() != null) {
             mWindowManager.removeViewImmediate(mTargetViewContainer);
         }
     }
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
new file mode 100644
index 0000000..d03d075
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.phone;
+
+import static com.android.wm.shell.pip.PipUtils.dpToPx;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.Size;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayLayout;
+
+import java.io.PrintWriter;
+
+/**
+ * Acts as a source of truth for appropriate size spec for PIP.
+ */
+public class PipSizeSpecHandler {
+    private static final String TAG = PipSizeSpecHandler.class.getSimpleName();
+
+    @NonNull private final DisplayLayout mDisplayLayout = new DisplayLayout();
+
+    @VisibleForTesting
+    final SizeSpecSource mSizeSpecSourceImpl;
+
+    /** The preferred minimum (and default minimum) size specified by apps. */
+    @Nullable private Size mOverrideMinSize;
+    private int mOverridableMinSize;
+
+    /** Used to store values obtained from resource files. */
+    private Point mScreenEdgeInsets;
+    private float mMinAspectRatioForMinSize;
+    private float mMaxAspectRatioForMinSize;
+    private int mDefaultMinSize;
+
+    @NonNull private final Context mContext;
+
+    private interface SizeSpecSource {
+        /** Returns max size allowed for the PIP window */
+        Size getMaxSize(float aspectRatio);
+
+        /** Returns default size for the PIP window */
+        Size getDefaultSize(float aspectRatio);
+
+        /** Returns min size allowed for the PIP window */
+        Size getMinSize(float aspectRatio);
+
+        /** Returns the adjusted size based on current size and target aspect ratio */
+        Size getSizeForAspectRatio(Size size, float aspectRatio);
+
+        /** Updates internal resources on configuration changes */
+        default void reloadResources() {}
+    }
+
+    /**
+     * Determines PIP window size optimized for large screens and aspect ratios close to 1:1
+     */
+    private class SizeSpecLargeScreenOptimizedImpl implements SizeSpecSource {
+        private static final float DEFAULT_OPTIMIZED_ASPECT_RATIO = 9f / 16;
+
+        /** Default and minimum percentages for the PIP size logic. */
+        private final float mDefaultSizePercent;
+        private final float mMinimumSizePercent;
+
+        /** Aspect ratio that the PIP size spec logic optimizes for. */
+        private float mOptimizedAspectRatio;
+
+        private SizeSpecLargeScreenOptimizedImpl() {
+            mDefaultSizePercent = Float.parseFloat(SystemProperties
+                    .get("com.android.wm.shell.pip.phone.def_percentage", "0.6"));
+            mMinimumSizePercent = Float.parseFloat(SystemProperties
+                    .get("com.android.wm.shell.pip.phone.min_percentage", "0.5"));
+        }
+
+        @Override
+        public void reloadResources() {
+            final Resources res = mContext.getResources();
+
+            mOptimizedAspectRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio);
+            // make sure the optimized aspect ratio is valid with a default value to fall back to
+            if (mOptimizedAspectRatio > 1) {
+                mOptimizedAspectRatio = DEFAULT_OPTIMIZED_ASPECT_RATIO;
+            }
+        }
+
+        /**
+         * Calculates the max size of PIP.
+         *
+         * Optimizes for 16:9 aspect ratios, making them take full length of shortest display edge.
+         * As aspect ratio approaches values close to 1:1, the logic does not let PIP occupy the
+         * whole screen. A linear function is used to calculate these sizes.
+         *
+         * @param aspectRatio aspect ratio of the PIP window
+         * @return dimensions of the max size of the PIP
+         */
+        @Override
+        public Size getMaxSize(float aspectRatio) {
+            final int totalHorizontalPadding = getInsetBounds().left
+                    + (getDisplayBounds().width() - getInsetBounds().right);
+            final int totalVerticalPadding = getInsetBounds().top
+                    + (getDisplayBounds().height() - getInsetBounds().bottom);
+
+            final int shorterLength = (int) (1f * Math.min(
+                    getDisplayBounds().width() - totalHorizontalPadding,
+                    getDisplayBounds().height() - totalVerticalPadding));
+
+            int maxWidth, maxHeight;
+
+            // use the optimized max sizing logic only within a certain aspect ratio range
+            if (aspectRatio >= mOptimizedAspectRatio && aspectRatio <= 1 / mOptimizedAspectRatio) {
+                // this formula and its derivation is explained in b/198643358#comment16
+                maxWidth = (int) (mOptimizedAspectRatio * shorterLength
+                        + shorterLength * (aspectRatio - mOptimizedAspectRatio) / (1
+                        + aspectRatio));
+                maxHeight = (int) (maxWidth / aspectRatio);
+            } else {
+                if (aspectRatio > 1f) {
+                    maxWidth = shorterLength;
+                    maxHeight = (int) (maxWidth / aspectRatio);
+                } else {
+                    maxHeight = shorterLength;
+                    maxWidth = (int) (maxHeight * aspectRatio);
+                }
+            }
+
+            return new Size(maxWidth, maxHeight);
+        }
+
+        /**
+         * Decreases the dimensions by a percentage relative to max size to get default size.
+         *
+         * @param aspectRatio aspect ratio of the PIP window
+         * @return dimensions of the default size of the PIP
+         */
+        @Override
+        public Size getDefaultSize(float aspectRatio) {
+            Size minSize = this.getMinSize(aspectRatio);
+
+            if (mOverrideMinSize != null) {
+                return minSize;
+            }
+
+            Size maxSize = this.getMaxSize(aspectRatio);
+
+            int defaultWidth = Math.max((int) (maxSize.getWidth() * mDefaultSizePercent),
+                    minSize.getWidth());
+            int defaultHeight = Math.max((int) (maxSize.getHeight() * mDefaultSizePercent),
+                    minSize.getHeight());
+
+            return new Size(defaultWidth, defaultHeight);
+        }
+
+        /**
+         * Decreases the dimensions by a certain percentage relative to max size to get min size.
+         *
+         * @param aspectRatio aspect ratio of the PIP window
+         * @return dimensions of the min size of the PIP
+         */
+        @Override
+        public Size getMinSize(float aspectRatio) {
+            // if there is an overridden min size provided, return that
+            if (mOverrideMinSize != null) {
+                return adjustOverrideMinSizeToAspectRatio(aspectRatio);
+            }
+
+            Size maxSize = this.getMaxSize(aspectRatio);
+
+            int minWidth = (int) (maxSize.getWidth() * mMinimumSizePercent);
+            int minHeight = (int) (maxSize.getHeight() * mMinimumSizePercent);
+
+            // make sure the calculated min size is not smaller than the allowed default min size
+            if (aspectRatio > 1f) {
+                minHeight = (int) Math.max(minHeight, mDefaultMinSize);
+                minWidth = (int) (minHeight * aspectRatio);
+            } else {
+                minWidth = (int) Math.max(minWidth, mDefaultMinSize);
+                minHeight = (int) (minWidth / aspectRatio);
+            }
+            return new Size(minWidth, minHeight);
+        }
+
+        /**
+         * Returns the size for target aspect ratio making sure new size conforms with the rules.
+         *
+         * <p>Recalculates the dimensions such that the target aspect ratio is achieved, while
+         * maintaining the same maximum size to current size ratio.
+         *
+         * @param size current size
+         * @param aspectRatio target aspect ratio
+         */
+        @Override
+        public Size getSizeForAspectRatio(Size size, float aspectRatio) {
+            // getting the percentage of the max size that current size takes
+            float currAspectRatio = (float) size.getWidth() / size.getHeight();
+            Size currentMaxSize = getMaxSize(currAspectRatio);
+            float currentPercent = (float) size.getWidth() / currentMaxSize.getWidth();
+
+            // getting the max size for the target aspect ratio
+            Size updatedMaxSize = getMaxSize(aspectRatio);
+
+            int width = (int) (updatedMaxSize.getWidth() * currentPercent);
+            int height = (int) (updatedMaxSize.getHeight() * currentPercent);
+
+            // adjust the dimensions if below allowed min edge size
+            if (width < getMinEdgeSize() && aspectRatio <= 1) {
+                width = getMinEdgeSize();
+                height = (int) (width / aspectRatio);
+            } else if (height < getMinEdgeSize() && aspectRatio > 1) {
+                height = getMinEdgeSize();
+                width = (int) (height * aspectRatio);
+            }
+
+            // reduce the dimensions of the updated size to the calculated percentage
+            return new Size(width, height);
+        }
+    }
+
+    private class SizeSpecDefaultImpl implements SizeSpecSource {
+        private float mDefaultSizePercent;
+        private float mMinimumSizePercent;
+
+        @Override
+        public void reloadResources() {
+            final Resources res = mContext.getResources();
+
+            mMaxAspectRatioForMinSize = res.getFloat(
+                    R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
+            mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
+
+            mDefaultSizePercent = res.getFloat(R.dimen.config_pictureInPictureDefaultSizePercent);
+            mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1);
+        }
+
+        @Override
+        public Size getMaxSize(float aspectRatio) {
+            final int shorterLength = Math.min(getDisplayBounds().width(),
+                    getDisplayBounds().height());
+
+            final int totalHorizontalPadding = getInsetBounds().left
+                    + (getDisplayBounds().width() - getInsetBounds().right);
+            final int totalVerticalPadding = getInsetBounds().top
+                    + (getDisplayBounds().height() - getInsetBounds().bottom);
+
+            final int maxWidth, maxHeight;
+
+            if (aspectRatio > 1f) {
+                maxWidth = (int) Math.max(getDefaultSize(aspectRatio).getWidth(),
+                        shorterLength - totalHorizontalPadding);
+                maxHeight = (int) (maxWidth / aspectRatio);
+            } else {
+                maxHeight = (int) Math.max(getDefaultSize(aspectRatio).getHeight(),
+                        shorterLength - totalVerticalPadding);
+                maxWidth = (int) (maxHeight * aspectRatio);
+            }
+
+            return new Size(maxWidth, maxHeight);
+        }
+
+        @Override
+        public Size getDefaultSize(float aspectRatio) {
+            if (mOverrideMinSize != null) {
+                return this.getMinSize(aspectRatio);
+            }
+
+            final int smallestDisplaySize = Math.min(getDisplayBounds().width(),
+                    getDisplayBounds().height());
+            final int minSize = (int) Math.max(getMinEdgeSize(),
+                    smallestDisplaySize * mDefaultSizePercent);
+
+            final int width;
+            final int height;
+
+            if (aspectRatio <= mMinAspectRatioForMinSize
+                    || aspectRatio > mMaxAspectRatioForMinSize) {
+                // Beyond these points, we can just use the min size as the shorter edge
+                if (aspectRatio <= 1) {
+                    // Portrait, width is the minimum size
+                    width = minSize;
+                    height = Math.round(width / aspectRatio);
+                } else {
+                    // Landscape, height is the minimum size
+                    height = minSize;
+                    width = Math.round(height * aspectRatio);
+                }
+            } else {
+                // Within these points, ensure that the bounds fit within the radius of the limits
+                // at the points
+                final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
+                final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
+                height = (int) Math.round(Math.sqrt((radius * radius)
+                        / (aspectRatio * aspectRatio + 1)));
+                width = Math.round(height * aspectRatio);
+            }
+
+            return new Size(width, height);
+        }
+
+        @Override
+        public Size getMinSize(float aspectRatio) {
+            if (mOverrideMinSize != null) {
+                return adjustOverrideMinSizeToAspectRatio(aspectRatio);
+            }
+
+            final int shorterLength = Math.min(getDisplayBounds().width(),
+                    getDisplayBounds().height());
+            final int minWidth, minHeight;
+
+            if (aspectRatio > 1f) {
+                minWidth = (int) Math.min(getDefaultSize(aspectRatio).getWidth(),
+                        shorterLength * mMinimumSizePercent);
+                minHeight = (int) (minWidth / aspectRatio);
+            } else {
+                minHeight = (int) Math.min(getDefaultSize(aspectRatio).getHeight(),
+                        shorterLength * mMinimumSizePercent);
+                minWidth = (int) (minHeight * aspectRatio);
+            }
+
+            return new Size(minWidth, minHeight);
+        }
+
+        @Override
+        public Size getSizeForAspectRatio(Size size, float aspectRatio) {
+            final int smallestSize = Math.min(size.getWidth(), size.getHeight());
+            final int minSize = Math.max(getMinEdgeSize(), smallestSize);
+
+            final int width;
+            final int height;
+            if (aspectRatio <= 1) {
+                // Portrait, width is the minimum size.
+                width = minSize;
+                height = Math.round(width / aspectRatio);
+            } else {
+                // Landscape, height is the minimum size
+                height = minSize;
+                width = Math.round(height * aspectRatio);
+            }
+
+            return new Size(width, height);
+        }
+    }
+
+    public PipSizeSpecHandler(Context context) {
+        mContext = context;
+
+        boolean enablePipSizeLargeScreen = SystemProperties
+                .getBoolean("persist.wm.debug.enable_pip_size_large_screen", false);
+
+        // choose between two implementations of size spec logic
+        if (enablePipSizeLargeScreen) {
+            mSizeSpecSourceImpl = new SizeSpecLargeScreenOptimizedImpl();
+        } else {
+            mSizeSpecSourceImpl = new SizeSpecDefaultImpl();
+        }
+
+        reloadResources();
+    }
+
+    /** Reloads the resources */
+    public void onConfigurationChanged() {
+        reloadResources();
+    }
+
+    private void reloadResources() {
+        final Resources res = mContext.getResources();
+
+        mDefaultMinSize = res.getDimensionPixelSize(
+                R.dimen.default_minimal_size_pip_resizable_task);
+        mOverridableMinSize = res.getDimensionPixelSize(
+                R.dimen.overridable_minimal_size_pip_resizable_task);
+
+        final String screenEdgeInsetsDpString = res.getString(
+                R.string.config_defaultPictureInPictureScreenEdgeInsets);
+        final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
+                ? Size.parseSize(screenEdgeInsetsDpString)
+                : null;
+        mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
+                : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()),
+                        dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics()));
+
+        // update the internal resources of the size spec source's stub
+        mSizeSpecSourceImpl.reloadResources();
+    }
+
+    /** Returns the display's bounds. */
+    @NonNull
+    public Rect getDisplayBounds() {
+        return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+    }
+
+    /** Update the display layout. */
+    public void setDisplayLayout(@NonNull DisplayLayout displayLayout) {
+        mDisplayLayout.set(displayLayout);
+    }
+
+    public Point getScreenEdgeInsets() {
+        return mScreenEdgeInsets;
+    }
+
+    /**
+     * Returns the inset bounds the PIP window can be visible in.
+     */
+    public Rect getInsetBounds() {
+        Rect insetBounds = new Rect();
+        Rect insets = mDisplayLayout.stableInsets();
+        insetBounds.set(insets.left + mScreenEdgeInsets.x,
+                insets.top + mScreenEdgeInsets.y,
+                mDisplayLayout.width() - insets.right - mScreenEdgeInsets.x,
+                mDisplayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
+        return insetBounds;
+    }
+
+    /** Sets the preferred size of PIP as specified by the activity in PIP mode. */
+    public void setOverrideMinSize(@Nullable Size overrideMinSize) {
+        mOverrideMinSize = overrideMinSize;
+    }
+
+    /** Returns the preferred minimal size specified by the activity in PIP. */
+    @Nullable
+    public Size getOverrideMinSize() {
+        if (mOverrideMinSize != null
+                && (mOverrideMinSize.getWidth() < mOverridableMinSize
+                || mOverrideMinSize.getHeight() < mOverridableMinSize)) {
+            return new Size(mOverridableMinSize, mOverridableMinSize);
+        }
+
+        return mOverrideMinSize;
+    }
+
+    /** Returns the minimum edge size of the override minimum size, or 0 if not set. */
+    public int getOverrideMinEdgeSize() {
+        if (mOverrideMinSize == null) return 0;
+        return Math.min(getOverrideMinSize().getWidth(), getOverrideMinSize().getHeight());
+    }
+
+    public int getMinEdgeSize() {
+        return mOverrideMinSize == null ? mDefaultMinSize : getOverrideMinEdgeSize();
+    }
+
+    /**
+     * Returns the size for the max size spec.
+     */
+    public Size getMaxSize(float aspectRatio) {
+        return mSizeSpecSourceImpl.getMaxSize(aspectRatio);
+    }
+
+    /**
+     * Returns the size for the default size spec.
+     */
+    public Size getDefaultSize(float aspectRatio) {
+        return mSizeSpecSourceImpl.getDefaultSize(aspectRatio);
+    }
+
+    /**
+     * Returns the size for the min size spec.
+     */
+    public Size getMinSize(float aspectRatio) {
+        return mSizeSpecSourceImpl.getMinSize(aspectRatio);
+    }
+
+    /**
+     * Returns the adjusted size so that it conforms to the given aspectRatio.
+     *
+     * @param size current size
+     * @param aspectRatio target aspect ratio
+     */
+    public Size getSizeForAspectRatio(@NonNull Size size, float aspectRatio) {
+        if (size.equals(mOverrideMinSize)) {
+            return adjustOverrideMinSizeToAspectRatio(aspectRatio);
+        }
+
+        return mSizeSpecSourceImpl.getSizeForAspectRatio(size, aspectRatio);
+    }
+
+    /**
+     * Returns the adjusted overridden min size if it is set; otherwise, returns null.
+     *
+     * <p>Overridden min size needs to be adjusted in its own way while making sure that the target
+     * aspect ratio is maintained
+     *
+     * @param aspectRatio target aspect ratio
+     */
+    @Nullable
+    @VisibleForTesting
+    Size adjustOverrideMinSizeToAspectRatio(float aspectRatio) {
+        if (mOverrideMinSize == null) {
+            return null;
+        }
+        final Size size = getOverrideMinSize();
+        final float sizeAspectRatio = size.getWidth() / (float) size.getHeight();
+        if (sizeAspectRatio > aspectRatio) {
+            // Size is wider, fix the width and increase the height
+            return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio));
+        } else {
+            // Size is taller, fix the height and adjust the width.
+            return new Size((int) (size.getHeight() * aspectRatio), size.getHeight());
+        }
+    }
+
+    /** Dumps internal state. */
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl.toString());
+        pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout);
+        pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 83bc7c0..0e8d13d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -78,7 +78,8 @@
     private boolean mEnableResize;
     private final Context mContext;
     private final PipBoundsAlgorithm mPipBoundsAlgorithm;
-    private final @NonNull PipBoundsState mPipBoundsState;
+    @NonNull private final PipBoundsState mPipBoundsState;
+    @NonNull private final PipSizeSpecHandler mPipSizeSpecHandler;
     private final PipUiEventLogger mPipUiEventLogger;
     private final PipDismissTargetHandler mPipDismissTargetHandler;
     private final PipTaskOrganizer mPipTaskOrganizer;
@@ -99,7 +100,6 @@
 
     // The reference inset bounds, used to determine the dismiss fraction
     private final Rect mInsetBounds = new Rect();
-    private int mExpandedShortestEdgeSize;
 
     // Used to workaround an issue where the WM rotation happens before we are notified, allowing
     // us to send stale bounds
@@ -120,7 +120,6 @@
     private float mSavedSnapFraction = -1f;
     private boolean mSendingHoverAccessibilityEvents;
     private boolean mMovementWithinDismiss;
-    private float mMinimumSizePercent;
 
     // Touch state
     private final PipTouchState mTouchState;
@@ -174,6 +173,7 @@
             PhonePipMenuController menuController,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             @NonNull PipBoundsState pipBoundsState,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler,
             PipTaskOrganizer pipTaskOrganizer,
             PipMotionHelper pipMotionHelper,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -184,6 +184,7 @@
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
         mPipBoundsAlgorithm = pipBoundsAlgorithm;
         mPipBoundsState = pipBoundsState;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         mPipTaskOrganizer = pipTaskOrganizer;
         mMenuController = menuController;
         mPipUiEventLogger = pipUiEventLogger;
@@ -271,10 +272,7 @@
     private void reloadResources() {
         final Resources res = mContext.getResources();
         mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer);
-        mExpandedShortestEdgeSize = res.getDimensionPixelSize(
-                R.dimen.pip_expanded_shortest_edge_size);
         mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
-        mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1);
         mPipDismissTargetHandler.updateMagneticTargetSize();
     }
 
@@ -337,8 +335,10 @@
         mMotionHelper.synchronizePinnedStackBounds();
         reloadResources();
 
-        // Recreate the dismiss target for the new orientation.
-        mPipDismissTargetHandler.createOrUpdateDismissTarget();
+        if (mPipTaskOrganizer.isInPip()) {
+            // Recreate the dismiss target for the new orientation.
+            mPipDismissTargetHandler.createOrUpdateDismissTarget();
+        }
     }
 
     public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
@@ -405,10 +405,7 @@
 
         // Calculate the expanded size
         float aspectRatio = (float) normalBounds.width() / normalBounds.height();
-        Point displaySize = new Point();
-        mContext.getDisplay().getRealSize(displaySize);
-        Size expandedSize = mPipBoundsAlgorithm.getSizeForAspectRatio(
-                aspectRatio, mExpandedShortestEdgeSize, displaySize.x, displaySize.y);
+        Size expandedSize = mPipSizeSpecHandler.getDefaultSize(aspectRatio);
         mPipBoundsState.setExpandedBounds(
                 new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight()));
         Rect expandedMovementBounds = new Rect();
@@ -416,7 +413,7 @@
                 mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds,
                 bottomOffset);
 
-        updatePipSizeConstraints(insetBounds, normalBounds, aspectRatio);
+        updatePipSizeConstraints(normalBounds, aspectRatio);
 
         // The extra offset does not really affect the movement bounds, but are applied based on the
         // current state (ime showing, or shelf offset) when we need to actually shift
@@ -494,14 +491,14 @@
      * @param aspectRatio aspect ratio to use for the calculation of min/max size
      */
     public void updateMinMaxSize(float aspectRatio) {
-        updatePipSizeConstraints(mInsetBounds, mPipBoundsState.getNormalBounds(),
+        updatePipSizeConstraints(mPipBoundsState.getNormalBounds(),
                 aspectRatio);
     }
 
-    private void updatePipSizeConstraints(Rect insetBounds, Rect normalBounds,
+    private void updatePipSizeConstraints(Rect normalBounds,
             float aspectRatio) {
         if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
-            updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio);
+            updatePinchResizeSizeConstraints(aspectRatio);
         } else {
             mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
             mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
@@ -509,26 +506,13 @@
         }
     }
 
-    private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds,
-            float aspectRatio) {
-        final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(),
-                mPipBoundsState.getDisplayBounds().height());
-        final int totalHorizontalPadding = insetBounds.left
-                + (mPipBoundsState.getDisplayBounds().width() - insetBounds.right);
-        final int totalVerticalPadding = insetBounds.top
-                + (mPipBoundsState.getDisplayBounds().height() - insetBounds.bottom);
+    private void updatePinchResizeSizeConstraints(float aspectRatio) {
         final int minWidth, minHeight, maxWidth, maxHeight;
-        if (aspectRatio > 1f) {
-            minWidth = (int) Math.min(normalBounds.width(), shorterLength * mMinimumSizePercent);
-            minHeight = (int) (minWidth / aspectRatio);
-            maxWidth = (int) Math.max(normalBounds.width(), shorterLength - totalHorizontalPadding);
-            maxHeight = (int) (maxWidth / aspectRatio);
-        } else {
-            minHeight = (int) Math.min(normalBounds.height(), shorterLength * mMinimumSizePercent);
-            minWidth = (int) (minHeight * aspectRatio);
-            maxHeight = (int) Math.max(normalBounds.height(), shorterLength - totalVerticalPadding);
-            maxWidth = (int) (maxHeight * aspectRatio);
-        }
+
+        minWidth = mPipSizeSpecHandler.getMinSize(aspectRatio).getWidth();
+        minHeight = mPipSizeSpecHandler.getMinSize(aspectRatio).getHeight();
+        maxWidth = mPipSizeSpecHandler.getMaxSize(aspectRatio).getWidth();
+        maxHeight = mPipSizeSpecHandler.getMaxSize(aspectRatio).getHeight();
 
         mPipResizeGestureHandler.updateMinSize(minWidth, minHeight);
         mPipResizeGestureHandler.updateMaxSize(maxWidth, maxHeight);
@@ -1062,11 +1046,6 @@
         mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(),
                 mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0);
         mMotionHelper.onMovementBoundsChanged();
-
-        boolean isMenuExpanded = mMenuState == MENU_STATE_FULL;
-        mPipBoundsState.setMinEdgeSize(
-                isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize
-                        : mPipBoundsAlgorithm.getDefaultMinSize());
     }
 
     private Rect getMovementBounds(Rect curBounds) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index ce34d2f..22feb43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -39,8 +39,9 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
@@ -63,9 +64,10 @@
 
     public TvPipBoundsAlgorithm(Context context,
             @NonNull TvPipBoundsState tvPipBoundsState,
-            @NonNull PipSnapAlgorithm pipSnapAlgorithm) {
+            @NonNull PipSnapAlgorithm pipSnapAlgorithm,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
         super(context, tvPipBoundsState, pipSnapAlgorithm,
-                new PipKeepClearAlgorithm() {});
+                new PipKeepClearAlgorithmInterface() {}, pipSizeSpecHandler);
         this.mTvPipBoundsState = tvPipBoundsState;
         this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
         reloadResources(context);
@@ -370,7 +372,8 @@
             if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
                 expandedSize = mTvPipBoundsState.getTvExpandedSize();
             } else {
-                int maxHeight = displayLayout.height() - (2 * mScreenEdgeInsets.y)
+                int maxHeight = displayLayout.height()
+                        - (2 * mPipSizeSpecHandler.getScreenEdgeInsets().y)
                         - pipDecorations.top - pipDecorations.bottom;
                 float aspectRatioHeight = mFixedExpandedWidthInPx / expandedRatio;
 
@@ -393,7 +396,8 @@
             if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_VERTICAL) {
                 expandedSize = mTvPipBoundsState.getTvExpandedSize();
             } else {
-                int maxWidth = displayLayout.width() - (2 * mScreenEdgeInsets.x)
+                int maxWidth = displayLayout.width()
+                        - (2 * mPipSizeSpecHandler.getScreenEdgeInsets().x)
                         - pipDecorations.left - pipDecorations.right;
                 float aspectRatioWidth = mFixedExpandedHeightInPx * expandedRatio;
                 if (maxWidth > aspectRatioWidth) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index ca22882..4e3ee51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -30,6 +30,7 @@
 
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -64,8 +65,9 @@
     private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE;
     private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
 
-    public TvPipBoundsState(@NonNull Context context) {
-        super(context);
+    public TvPipBoundsState(@NonNull Context context,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
+        super(context, pipSizeSpecHandler);
         mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 4e1b046..6bc666f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -50,6 +50,7 @@
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellController;
@@ -102,6 +103,7 @@
 
     private final ShellController mShellController;
     private final TvPipBoundsState mTvPipBoundsState;
+    private final PipSizeSpecHandler mPipSizeSpecHandler;
     private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
     private final TvPipBoundsController mTvPipBoundsController;
     private final PipAppOpsListener mAppOpsListener;
@@ -133,6 +135,7 @@
             ShellInit shellInit,
             ShellController shellController,
             TvPipBoundsState tvPipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             TvPipBoundsController tvPipBoundsController,
             PipAppOpsListener pipAppOpsListener,
@@ -151,6 +154,7 @@
                 shellInit,
                 shellController,
                 tvPipBoundsState,
+                pipSizeSpecHandler,
                 tvPipBoundsAlgorithm,
                 tvPipBoundsController,
                 pipAppOpsListener,
@@ -171,6 +175,7 @@
             ShellInit shellInit,
             ShellController shellController,
             TvPipBoundsState tvPipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             TvPipBoundsController tvPipBoundsController,
             PipAppOpsListener pipAppOpsListener,
@@ -189,9 +194,13 @@
         mShellController = shellController;
         mDisplayController = displayController;
 
+        DisplayLayout layout = new DisplayLayout(context, context.getDisplay());
+
         mTvPipBoundsState = tvPipBoundsState;
+        mTvPipBoundsState.setDisplayLayout(layout);
         mTvPipBoundsState.setDisplayId(context.getDisplayId());
-        mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
+        mPipSizeSpecHandler = pipSizeSpecHandler;
+        mPipSizeSpecHandler.setDisplayLayout(layout);
         mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
         mTvPipBoundsController = tvPipBoundsController;
         mTvPipBoundsController.setListener(this);
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/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 8490f9f..0d9faa3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -308,7 +308,6 @@
             rawMapping.put(taskInfo.taskId, taskInfo);
         }
 
-        boolean desktopModeActive = DesktopModeStatus.isActive(mContext);
         ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>();
 
         // Pull out the pairs as we iterate back in the list
@@ -320,7 +319,7 @@
                 continue;
             }
 
-            if (desktopModeActive && mDesktopModeTaskRepository.isPresent()
+            if (DesktopModeStatus.isProto2Enabled() && mDesktopModeTaskRepository.isPresent()
                     && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
                 // Freeform tasks will be added as a separate entry
                 freeformTasks.add(taskInfo);
@@ -328,7 +327,7 @@
             }
 
             final int pairedTaskId = mSplitTasks.get(taskInfo.taskId);
-            if (!desktopModeActive && pairedTaskId != INVALID_TASK_ID && rawMapping.contains(
+            if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(
                     pairedTaskId)) {
                 final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
                 rawMapping.remove(pairedTaskId);
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 38099fc..94b9e90 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
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -27,6 +28,9 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit;
+import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
+import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
@@ -38,11 +42,9 @@
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
-import android.content.ActivityNotFoundException;
-import android.content.ComponentName;
+import android.app.TaskInfo;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -81,8 +83,8 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -317,10 +319,6 @@
         return mStageCoordinator;
     }
 
-    public boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
-        return mStageCoordinator.isValidToEnterSplitScreen(taskInfo);
-    }
-
     @Nullable
     public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
         if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) {
@@ -424,6 +422,19 @@
         mStageCoordinator.goToFullscreenFromSplit();
     }
 
+    /** Move the specified task to fullscreen, regardless of focus state. */
+    public void moveTaskToFullscreen(int taskId) {
+        mStageCoordinator.moveTaskToFullscreen(taskId);
+    }
+
+    public boolean isLaunchToSplit(TaskInfo taskInfo) {
+        return mStageCoordinator.isLaunchToSplit(taskInfo);
+    }
+
+    public int getActivateSplitPosition(TaskInfo taskInfo) {
+        return mStageCoordinator.getActivateSplitPosition(taskInfo);
+    }
+
     public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
         final int[] result = new int[1];
         IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
@@ -479,39 +490,54 @@
     @Override
     public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
             @Nullable Bundle options, UserHandle user) {
-        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
-            @Override
-            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
-                    RemoteAnimationTarget[] apps,
-                    RemoteAnimationTarget[] wallpapers,
-                    RemoteAnimationTarget[] nonApps,
-                    final IRemoteAnimationFinishedCallback finishedCallback) {
-                try {
-                    finishedCallback.onAnimationFinished();
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
-                }
-                final WindowContainerTransaction evictWct = new WindowContainerTransaction();
-                mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
-                mSyncQueue.queue(evictWct);
+        if (options == null) options = new Bundle();
+        final ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+
+        if (samePackage(packageName, getPackageName(reverseSplitPosition(position)))) {
+            if (supportMultiInstancesSplit(packageName)) {
+                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else if (isSplitScreenVisible()) {
+                mStageCoordinator.switchSplitPosition("startShortcut");
+                return;
+            } else {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Cancel entering split as not supporting multi-instances");
+                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+                        Toast.LENGTH_SHORT).show();
+                return;
             }
-            @Override
-            public void onAnimationCancelled(boolean isKeyguardOccluded) {
-            }
-        };
-        options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
-                null /* wct */);
-        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
-                0 /* duration */, 0 /* statusBarTransitionDelay */);
-        ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
-        activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
-        try {
-            LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
-            launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
-                    activityOptions.toBundle(), user);
-        } catch (ActivityNotFoundException e) {
-            Slog.e(TAG, "Failed to launch shortcut", e);
         }
+
+        mStageCoordinator.startShortcut(packageName, shortcutId, position,
+                activityOptions.toBundle(), user);
+    }
+
+    void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
+            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+            InstanceId instanceId) {
+        if (options1 == null) options1 = new Bundle();
+        final ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+
+        final String packageName1 = shortcutInfo.getPackage();
+        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+        if (samePackage(packageName1, packageName2)) {
+            if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
+                activityOptions.setApplyMultipleTaskFlagForShortcut(true);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else {
+                taskId = INVALID_TASK_ID;
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Cancel entering split as not supporting multi-instances");
+                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+                        Toast.LENGTH_SHORT).show();
+            }
+        }
+
+        mStageCoordinator.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
+                activityOptions.toBundle(), taskId, options2, splitPosition, splitRatio, adapter,
+                instanceId);
     }
 
     /**
@@ -529,23 +555,19 @@
             @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
             InstanceId instanceId) {
         Intent fillInIntent = null;
-        if (launchSameAppAdjacently(pendingIntent, taskId)) {
-            if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
+        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+        if (samePackage(packageName1, packageName2)) {
+            if (supportMultiInstancesSplit(packageName1)) {
                 fillInIntent = new Intent();
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
-                try {
-                    adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
-                    ActivityTaskManager.getService().startActivityFromRecents(taskId, options2);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error starting remote animation", e);
-                }
+                taskId = INVALID_TASK_ID;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
-                return;
             }
         }
         mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
@@ -556,8 +578,10 @@
             int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
             float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
         Intent fillInIntent = null;
-        if (launchSameAppAdjacently(pendingIntent, taskId)) {
-            if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
+        final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+        if (samePackage(packageName1, packageName2)) {
+            if (supportMultiInstancesSplit(packageName1)) {
                 fillInIntent = new Intent();
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -573,35 +597,32 @@
     }
 
     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;
-        if (launchSameAppAdjacently(pendingIntent1, pendingIntent2)) {
-            if (supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
+        final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
+        final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
+        if (samePackage(packageName1, packageName2)) {
+            if (supportMultiInstancesSplit(packageName1)) {
                 fillInIntent1 = new Intent();
                 fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 fillInIntent2 = new Intent();
                 fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
-                try {
-                    adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
-                    pendingIntent1.send();
-                } catch (RemoteException | PendingIntent.CanceledException e) {
-                    Slog.e(TAG, "Error starting remote animation", e);
-                }
+                pendingIntent2 = null;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
-                return;
             }
         }
-        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
@@ -613,13 +634,15 @@
         if (fillInIntent == null) fillInIntent = new Intent();
         fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
 
-        if (launchSameAppAdjacently(position, intent)) {
-            final ComponentName launching = intent.getIntent().getComponent();
-            if (supportMultiInstancesSplit(launching)) {
+        final String packageName1 = SplitScreenUtils.getPackageName(intent);
+        final String packageName2 = getPackageName(reverseSplitPosition(position));
+        if (SplitScreenUtils.samePackage(packageName1, packageName2)) {
+            if (supportMultiInstancesSplit(packageName1)) {
                 // To prevent accumulating large number of instances in the background, reuse task
                 // in the background with priority.
                 final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
-                        .map(recentTasks -> recentTasks.findTaskInBackground(launching))
+                        .map(recentTasks -> recentTasks.findTaskInBackground(
+                                intent.getIntent().getComponent()))
                         .orElse(null);
                 if (taskInfo != null) {
                     startTask(taskInfo.taskId, position, options);
@@ -647,63 +670,32 @@
         mStageCoordinator.startIntent(intent, fillInIntent, position, options);
     }
 
+    /** Retrieve package name of a specific split position if split screen is activated, otherwise
+     *  returns the package name of the top running task. */
     @Nullable
-    private String getPackageName(Intent intent) {
-        if (intent == null || intent.getComponent() == null) {
-            return null;
-        }
-        return intent.getComponent().getPackageName();
-    }
-
-    private boolean launchSameAppAdjacently(@SplitPosition int position,
-            PendingIntent pendingIntent) {
-        ActivityManager.RunningTaskInfo adjacentTaskInfo = null;
+    private String getPackageName(@SplitPosition int position) {
+        ActivityManager.RunningTaskInfo taskInfo;
         if (isSplitScreenVisible()) {
-            adjacentTaskInfo = getTaskInfo(SplitLayout.reversePosition(position));
+            taskInfo = getTaskInfo(position);
         } else {
-            adjacentTaskInfo = mRecentTasksOptional
-                    .map(recentTasks -> recentTasks.getTopRunningTask()).orElse(null);
-            if (!isValidToEnterSplitScreen(adjacentTaskInfo)) {
-                return false;
+            taskInfo = mRecentTasksOptional
+                    .map(recentTasks -> recentTasks.getTopRunningTask())
+                    .orElse(null);
+            if (!isValidToSplit(taskInfo)) {
+                return null;
             }
         }
 
-        if (adjacentTaskInfo == null) {
-            return false;
-        }
-
-        final String targetPackageName = getPackageName(pendingIntent.getIntent());
-        final String adjacentPackageName = getPackageName(adjacentTaskInfo.baseIntent);
-        return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
-    }
-
-    private boolean launchSameAppAdjacently(PendingIntent pendingIntent, int taskId) {
-        final ActivityManager.RunningTaskInfo adjacentTaskInfo =
-                mTaskOrganizer.getRunningTaskInfo(taskId);
-        if (adjacentTaskInfo == null) {
-            return false;
-        }
-        final String targetPackageName = getPackageName(pendingIntent.getIntent());
-        final String adjacentPackageName = getPackageName(adjacentTaskInfo.baseIntent);
-        return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
-    }
-
-    private boolean launchSameAppAdjacently(PendingIntent pendingIntent1,
-            PendingIntent pendingIntent2) {
-        final String targetPackageName = getPackageName(pendingIntent1.getIntent());
-        final String adjacentPackageName = getPackageName(pendingIntent2.getIntent());
-        return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
+        return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null;
     }
 
     @VisibleForTesting
-    /** Returns {@code true} if the component supports multi-instances split. */
-    boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
-        if (launching == null) return false;
-
-        final String packageName = launching.getPackageName();
-        for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
-            if (mAppsSupportMultiInstances[i].equals(packageName)) {
-                return true;
+    boolean supportMultiInstancesSplit(String packageName) {
+        if (packageName != null) {
+            for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
+                if (mAppsSupportMultiInstances[i].equals(packageName)) {
+                    return true;
+                }
             }
         }
 
@@ -1022,7 +1014,7 @@
                 InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController,
                     "startShortcutAndTaskWithLegacyTransition", (controller) ->
-                            controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition(
+                            controller.startShortcutAndTaskWithLegacyTransition(
                                     shortcutInfo, options1, taskId, options2, splitPosition,
                                     splitRatio, adapter, instanceId));
         }
@@ -1060,13 +1052,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 8ddc3c04..a673384 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
@@ -36,12 +36,11 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
@@ -75,9 +74,12 @@
 import android.app.ActivityOptions;
 import android.app.IActivityTaskManager;
 import android.app.PendingIntent;
+import android.app.TaskInfo;
 import android.app.WindowConfiguration;
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -87,6 +89,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 import android.util.Log;
 import android.util.Slog;
 import android.view.Choreographer;
@@ -122,6 +125,7 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.common.split.SplitWindowManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentTasksController;
@@ -206,6 +210,36 @@
 
     private DefaultMixedHandler mMixedHandler;
     private final Toast mSplitUnsupportedToast;
+    private SplitRequest mSplitRequest;
+
+    class SplitRequest {
+        @SplitPosition
+        int mActivatePosition;
+        int mActivateTaskId;
+        int mActivateTaskId2;
+        Intent mStartIntent;
+        Intent mStartIntent2;
+
+        SplitRequest(int taskId, Intent startIntent, int position) {
+            mActivateTaskId = taskId;
+            mStartIntent = startIntent;
+            mActivatePosition = position;
+        }
+        SplitRequest(Intent startIntent, int position) {
+            mStartIntent = startIntent;
+            mActivatePosition = position;
+        }
+        SplitRequest(Intent startIntent, Intent startIntent2, int position) {
+            mStartIntent = startIntent;
+            mStartIntent2 = startIntent2;
+            mActivatePosition = position;
+        }
+        SplitRequest(int taskId1, int taskId2, int position) {
+            mActivateTaskId = taskId1;
+            mActivateTaskId2 = taskId2;
+            mActivatePosition = position;
+        }
+    }
 
     private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
             new SplitWindowManager.ParentContainerCallbacks() {
@@ -370,7 +404,7 @@
         int sideStagePosition;
         if (stageType == STAGE_TYPE_MAIN) {
             targetStage = mMainStage;
-            sideStagePosition = SplitLayout.reversePosition(stagePosition);
+            sideStagePosition = reverseSplitPosition(stagePosition);
         } else if (stageType == STAGE_TYPE_SIDE) {
             targetStage = mSideStage;
             sideStagePosition = stagePosition;
@@ -391,6 +425,23 @@
         setSideStagePosition(sideStagePosition, wct);
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         targetStage.evictAllChildren(evictWct);
+
+        // Apply surface bounds before animation start.
+        SurfaceControl.Transaction startT = mTransactionPool.acquire();
+        if (startT != null) {
+            updateSurfaceBounds(mSplitLayout, startT, false /* applyResizingOffset */);
+            startT.apply();
+            mTransactionPool.release(startT);
+        }
+        // reparent the task to an invisible split root will make the activity invisible.  Reorder
+        // the root task to front to make the entering transition from pip to split smooth.
+        wct.reorder(mRootTaskInfo.token, true);
+        wct.setForceTranslucent(mRootTaskInfo.token, true);
+        wct.reorder(targetStage.mRootTaskInfo.token, true);
+        wct.setForceTranslucent(targetStage.mRootTaskInfo.token, true);
+        // prevent the fling divider to center transition
+        mIsDropEntering = true;
+
         targetStage.addTask(task, wct);
 
         if (ENABLE_SHELL_TRANSITIONS) {
@@ -428,6 +479,81 @@
         return mLogger;
     }
 
+    void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
+            Bundle options, UserHandle user) {
+        final boolean isEnteringSplit = !isSplitActive();
+
+        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+            @Override
+            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+                    RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers,
+                    RemoteAnimationTarget[] nonApps,
+                    final IRemoteAnimationFinishedCallback finishedCallback) {
+                boolean openingToSide = false;
+                if (apps != null) {
+                    for (int i = 0; i < apps.length; ++i) {
+                        if (apps[i].mode == MODE_OPENING
+                                && mSideStage.containsTask(apps[i].taskId)) {
+                            openingToSide = true;
+                            break;
+                        }
+                    }
+                } else if (mSideStage.getChildCount() != 0) {
+                    // There are chances the entering app transition got canceled by performing
+                    // rotation transition. Checks if there is any child task existed in split
+                    // screen before fallback to cancel entering flow.
+                    openingToSide = true;
+                }
+
+                if (isEnteringSplit && !openingToSide) {
+                    mMainExecutor.execute(() -> exitSplitScreen(
+                            mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
+                            EXIT_REASON_UNKNOWN));
+                }
+
+                if (finishedCallback != null) {
+                    try {
+                        finishedCallback.onAnimationFinished();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error finishing legacy transition: ", e);
+                    }
+                }
+
+                if (!isEnteringSplit && apps != null) {
+                    final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+                    prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+                    mSyncQueue.queue(evictWct);
+                }
+            }
+            @Override
+            public void onAnimationCancelled(boolean isKeyguardOccluded) {
+                if (isEnteringSplit) {
+                    mMainExecutor.execute(() -> exitSplitScreen(
+                            mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
+                            EXIT_REASON_UNKNOWN));
+                }
+            }
+        };
+        options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
+                null /* wct */);
+        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
+                0 /* duration */, 0 /* statusBarTransitionDelay */);
+        ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+        // Flag this as a no-user-action launch to prevent sending user leaving event to the current
+        // top activity since it's going to be put into another side of the split. This prevents the
+        // current top activity from going into pip mode due to user leaving event.
+        activityOptions.setApplyNoUserActionFlagForShortcut(true);
+        activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+        try {
+            LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+            launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
+                    activityOptions.toBundle(), user);
+        } catch (ActivityNotFoundException e) {
+            Slog.e(TAG, "Failed to launch shortcut", e);
+        }
+    }
+
     /** Launches an activity into split. */
     void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
             @Nullable Bundle options) {
@@ -477,9 +603,14 @@
                             break;
                         }
                     }
+                } else if (mSideStage.getChildCount() != 0) {
+                    // There are chances the entering app transition got canceled by performing
+                    // rotation transition. Checks if there is any child task existed in split
+                    // screen before fallback to cancel entering flow.
+                    openingToSide = true;
                 }
 
-                if (isEnteringSplit && !openingToSide) {
+                if (isEnteringSplit && !openingToSide && apps != null) {
                     mMainExecutor.execute(() -> exitSplitScreen(
                             mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
                             EXIT_REASON_UNKNOWN));
@@ -503,7 +634,7 @@
                 }
 
 
-                if (!isEnteringSplit && openingToSide) {
+                if (!isEnteringSplit && apps != null) {
                     final WindowContainerTransaction evictWct = new WindowContainerTransaction();
                     prepareEvictNonOpeningChildTasks(position, apps, evictWct);
                     mSyncQueue.queue(evictWct);
@@ -519,7 +650,7 @@
         if (isEnteringSplit && mLogger.isEnterRequestedByDrag()) {
             updateWindowBounds(mSplitLayout, wct);
         }
-
+        mSplitRequest = new SplitRequest(intent.getIntent(), position);
         wct.sendPendingIntent(intent, fillInIntent, options);
         mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
     }
@@ -605,25 +736,58 @@
             float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (options1 == null) options1 = new Bundle();
+        if (taskId2 == INVALID_TASK_ID) {
+            // Launching a solo task.
+            ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+            activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+            options1 = activityOptions.toBundle();
+            addActivityOptions(options1, null /* launchTarget */);
+            wct.startTask(taskId1, options1);
+            mSyncQueue.queue(wct);
+            return;
+        }
+
         addActivityOptions(options1, mSideStage);
         wct.startTask(taskId1, options1);
-
+        mSplitRequest = new SplitRequest(taskId1, taskId2, splitPosition);
         startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter,
                 instanceId);
     }
 
     /** 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();
-        addActivityOptions(options1, mSideStage);
-        wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+        if (pendingIntent2 == null) {
+            // Launching a solo task.
+            ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+            activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+            options1 = activityOptions.toBundle();
+            addActivityOptions(options1, null /* launchTarget */);
+            if (shortcutInfo1 != null) {
+                wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
+            } else {
+                wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+            }
+            mSyncQueue.queue(wct);
+            return;
+        }
 
-        startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, options2, splitPosition,
-                splitRatio, adapter, instanceId);
+        addActivityOptions(options1, mSideStage);
+        if (shortcutInfo1 != null) {
+            wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
+        } else {
+            wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+            mSplitRequest = new SplitRequest(pendingIntent1.getIntent(),
+                    pendingIntent2 != null ? pendingIntent2.getIntent() : null, splitPosition);
+        }
+        startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, shortcutInfo2, options2,
+                splitPosition, splitRatio, adapter, instanceId);
     }
 
     void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent,
@@ -632,9 +796,20 @@
             InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (options1 == null) options1 = new Bundle();
+        if (taskId == INVALID_TASK_ID) {
+            // Launching a solo task.
+            ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+            activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+            options1 = activityOptions.toBundle();
+            addActivityOptions(options1, null /* launchTarget */);
+            wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
+            mSyncQueue.queue(wct);
+            return;
+        }
+
         addActivityOptions(options1, mSideStage);
         wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
-
+        mSplitRequest = new SplitRequest(taskId, pendingIntent.getIntent(), splitPosition);
         startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter,
                 instanceId);
     }
@@ -646,27 +821,38 @@
             InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (options1 == null) options1 = new Bundle();
+        if (taskId == INVALID_TASK_ID) {
+            // Launching a solo task.
+            ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+            activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+            options1 = activityOptions.toBundle();
+            addActivityOptions(options1, null /* launchTarget */);
+            wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
+            mSyncQueue.queue(wct);
+            return;
+        }
+
         addActivityOptions(options1, mSideStage);
         wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
-
         startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter,
                 instanceId);
     }
 
     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);
     }
 
     /**
@@ -676,8 +862,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);
         }
@@ -695,7 +882,43 @@
         // Set false to avoid record new bounds with old task still on top;
         mShouldUpdateRecents = false;
         mIsSplitEntering = true;
+        if (mSplitRequest == null) {
+            mSplitRequest = new SplitRequest(mainTaskId,
+                    mainPendingIntent != null ? mainPendingIntent.getIntent() : null,
+                    sidePosition);
+        }
+        setSideStagePosition(sidePosition, wct);
+        if (!mMainStage.isActive()) {
+            mMainStage.activate(wct, false /* reparent */);
+        }
 
+        if (options == null) options = new Bundle();
+        addActivityOptions(options, mMainStage);
+        options = wrapAsSplitRemoteAnimation(adapter, options);
+
+        updateWindowBounds(mSplitLayout, wct);
+
+        // 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.sendPendingIntent(mainPendingIntent, mainFillInIntent, options);
+        }
+
+        wct.reorder(mRootTaskInfo.token, true);
+        wct.setForceTranslucent(mRootTaskInfo.token, false);
+
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> {
+            setDividerVisibility(true, t);
+        });
+
+        setEnterInstanceId(instanceId);
+    }
+
+    private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) {
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         if (isSplitScreenVisible()) {
             mMainStage.evictAllChildren(evictWct);
@@ -713,7 +936,7 @@
                         new IRemoteAnimationFinishedCallback.Stub() {
                             @Override
                             public void onAnimationFinished() throws RemoteException {
-                                onRemoteAnimationFinishedOrCancelled(false /* cancel */, evictWct);
+                                onRemoteAnimationFinishedOrCancelled(evictWct);
                                 finishedCallback.onAnimationFinished();
                             }
                         };
@@ -729,7 +952,7 @@
 
             @Override
             public void onAnimationCancelled(boolean isKeyguardOccluded) {
-                onRemoteAnimationFinishedOrCancelled(true /* cancel */, evictWct);
+                onRemoteAnimationFinishedOrCancelled(evictWct);
                 try {
                     adapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
                 } catch (RemoteException e) {
@@ -739,37 +962,9 @@
         };
         RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
                 wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
-
-        if (mainOptions == null) {
-            mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
-        } else {
-            ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
-            mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
-            mainOptions = mainActivityOptions.toBundle();
-        }
-
-        setSideStagePosition(sidePosition, wct);
-        if (!mMainStage.isActive()) {
-            mMainStage.activate(wct, false /* reparent */);
-        }
-
-        if (mainOptions == null) mainOptions = new Bundle();
-        addActivityOptions(mainOptions, mMainStage);
-        updateWindowBounds(mSplitLayout, wct);
-        if (mainTaskId == INVALID_TASK_ID) {
-            wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions);
-        } else {
-            wct.startTask(mainTaskId, mainOptions);
-        }
-        wct.reorder(mRootTaskInfo.token, true);
-        wct.setForceTranslucent(mRootTaskInfo.token, false);
-
-        mSyncQueue.queue(wct);
-        mSyncQueue.runInSync(t -> {
-            setDividerVisibility(true, t);
-        });
-
-        setEnterInstanceId(instanceId);
+        ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+        activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+        return activityOptions.toBundle();
     }
 
     private void setEnterInstanceId(InstanceId instanceId) {
@@ -778,14 +973,14 @@
         }
     }
 
-    private void onRemoteAnimationFinishedOrCancelled(boolean cancel,
-            WindowContainerTransaction evictWct) {
+    private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) {
         mIsSplitEntering = false;
         mShouldUpdateRecents = true;
+        mSplitRequest = null;
         // If any stage has no child after animation finished, it means that split will display
         // nothing, such status will happen if task and intent is same app but not support
         // multi-instance, we should exit split and expand that app as full screen.
-        if (!cancel && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
+        if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
             mMainExecutor.execute(() ->
                     exitSplitScreen(mMainStage.getChildCount() == 0
                             ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
@@ -859,7 +1054,7 @@
             case STAGE_TYPE_MAIN: {
                 if (position != SPLIT_POSITION_UNDEFINED) {
                     // Set the side stage opposite of what we want to the main stage.
-                    setSideStagePosition(SplitLayout.reversePosition(position), wct);
+                    setSideStagePosition(reverseSplitPosition(position), wct);
                 } else {
                     position = getMainStagePosition();
                 }
@@ -883,7 +1078,7 @@
 
     @SplitPosition
     int getMainStagePosition() {
-        return SplitLayout.reversePosition(mSideStagePosition);
+        return reverseSplitPosition(mSideStagePosition);
     }
 
     int getTaskId(@SplitPosition int splitPosition) {
@@ -910,7 +1105,7 @@
         mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
                 insets -> {
                     WindowContainerTransaction wct = new WindowContainerTransaction();
-                    setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), wct);
+                    setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct);
                     mSyncQueue.queue(wct);
                     mSyncQueue.runInSync(st -> {
                         updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */);
@@ -1052,6 +1247,7 @@
             mSideStage.removeAllTasks(wct, false /* toTop */);
             mMainStage.deactivate(wct, false /* toTop */);
             wct.reorder(mRootTaskInfo.token, false /* onTop */);
+            wct.setForceTranslucent(mRootTaskInfo.token, true);
             wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
             onTransitionAnimationComplete();
         } else {
@@ -1083,6 +1279,7 @@
                     mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */);
                     mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
                     finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
+                    finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
                     finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
                     mSyncQueue.queue(finishedWCT);
                     mSyncQueue.runInSync(at -> {
@@ -1228,8 +1425,10 @@
         return SPLIT_POSITION_UNDEFINED;
     }
 
-    private void addActivityOptions(Bundle opts, StageTaskListener stage) {
-        opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
+    private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) {
+        if (launchTarget != null) {
+            opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token);
+        }
         // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
         // will be canceled.
         opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true);
@@ -1463,6 +1662,12 @@
     }
 
     private void onStageVisibilityChanged(StageListenerImpl stageListener) {
+        // If split didn't active, just ignore this callback because we should already did these
+        // on #applyExitSplitScreen.
+        if (!isSplitActive()) {
+            return;
+        }
+
         final boolean sideStageVisible = mSideStageListener.mVisible;
         final boolean mainStageVisible = mMainStageListener.mVisible;
 
@@ -1471,24 +1676,25 @@
             return;
         }
 
+        // Check if it needs to dismiss split screen when both stage invisible.
+        if (!mainStageVisible && mExitSplitScreenOnHide) {
+            exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
+            return;
+        }
+
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (!mainStageVisible) {
+            // Split entering background.
             wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
                     true /* setReparentLeafTaskIfRelaunch */);
             wct.setForceTranslucent(mRootTaskInfo.token, true);
-            // Both stages are not visible, check if it needs to dismiss split screen.
-            if (mExitSplitScreenOnHide) {
-                exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
-            }
         } else {
             wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
                     false /* setReparentLeafTaskIfRelaunch */);
-            wct.setForceTranslucent(mRootTaskInfo.token, false);
         }
+
         mSyncQueue.queue(wct);
-        mSyncQueue.runInSync(t -> {
-            setDividerVisibility(mainStageVisible, t);
-        });
+        setDividerVisibility(mainStageVisible, null);
     }
 
     private void setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t) {
@@ -1570,6 +1776,10 @@
 
                 @Override
                 public void onAnimationEnd(Animator animation) {
+                    if (dividerLeash != null && dividerLeash.isValid()) {
+                        transaction.setAlpha(dividerLeash, 1);
+                        transaction.apply();
+                    }
                     mTransactionPool.release(transaction);
                     mDividerFadeInAnimator = null;
                 }
@@ -1602,7 +1812,7 @@
             mSplitLayout.init();
 
             final WindowContainerTransaction wct = new WindowContainerTransaction();
-            if (mLogger.isEnterRequestedByDrag()) {
+            if (mIsDropEntering) {
                 prepareEnterSplitScreen(wct);
             } else {
                 // TODO (b/238697912) : Add the validation to prevent entering non-recovered status
@@ -1627,6 +1837,7 @@
         }
         if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
             mShouldUpdateRecents = true;
+            mSplitRequest = null;
             updateRecentTasksSplitPair();
 
             if (!mLogger.hasStartedSession()) {
@@ -1641,12 +1852,6 @@
         }
     }
 
-    boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
-        return taskInfo.supportsMultiWindow
-                && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
-                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode());
-    }
-
     @Override
     public void onSnappedToDismiss(boolean bottomOrRight, int reason) {
         final boolean mainStageToTop =
@@ -2185,6 +2390,47 @@
         mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
     }
 
+    /** Move the specified task to fullscreen, regardless of focus state. */
+    public void moveTaskToFullscreen(int taskId) {
+        boolean leftOrTop;
+        if (mMainStage.containsTask(taskId)) {
+            leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+        } else if (mSideStage.containsTask(taskId)) {
+            leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
+        } else {
+            return;
+        }
+        mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
+
+    }
+
+    boolean isLaunchToSplit(TaskInfo taskInfo) {
+        return getActivateSplitPosition(taskInfo) != SPLIT_POSITION_UNDEFINED;
+    }
+
+    int getActivateSplitPosition(TaskInfo taskInfo) {
+        if (mSplitRequest == null || taskInfo == null) {
+            return SPLIT_POSITION_UNDEFINED;
+        }
+        if (mSplitRequest.mActivateTaskId != 0
+                && mSplitRequest.mActivateTaskId2 == taskInfo.taskId) {
+            return mSplitRequest.mActivatePosition;
+        }
+        if (mSplitRequest.mActivateTaskId == taskInfo.taskId) {
+            return mSplitRequest.mActivatePosition;
+        }
+        final String packageName1 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent);
+        final String basePackageName = SplitScreenUtils.getPackageName(taskInfo.baseIntent);
+        if (packageName1 != null && packageName1.equals(basePackageName)) {
+            return mSplitRequest.mActivatePosition;
+        }
+        final String packageName2 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent2);
+        if (packageName2 != null && packageName2.equals(basePackageName)) {
+            return mSplitRequest.mActivatePosition;
+        }
+        return SPLIT_POSITION_UNDEFINED;
+    }
+
     /** Synchronize split-screen state with transition and make appropriate preparations. */
     public void prepareDismissAnimation(@StageType int toStage, @ExitReason int dismissReason,
             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
index eab82f0..e0f3fcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
@@ -26,8 +26,10 @@
 import android.annotation.NonNull;
 import android.app.TaskInfo;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.util.SparseArray;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -36,6 +38,8 @@
 
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
@@ -51,7 +55,7 @@
  * instances of FullscreenUnfoldTaskAnimator.
  */
 public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
-        DisplayInsetsController.OnInsetsChangedListener {
+        DisplayInsetsController.OnInsetsChangedListener, ConfigurationChangeListener {
 
     private static final float[] FLOAT_9 = new float[9];
     private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
@@ -63,17 +67,21 @@
 
     private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
     private final int mExpandedTaskBarHeight;
-    private final float mWindowCornerRadiusPx;
     private final DisplayInsetsController mDisplayInsetsController;
     private final UnfoldBackgroundController mBackgroundController;
+    private final Context mContext;
+    private final ShellController mShellController;
 
     private InsetsSource mTaskbarInsetsSource;
+    private float mWindowCornerRadiusPx;
 
     public FullscreenUnfoldTaskAnimator(Context context,
             @NonNull UnfoldBackgroundController backgroundController,
-            DisplayInsetsController displayInsetsController) {
+            ShellController shellController, DisplayInsetsController displayInsetsController) {
+        mContext = context;
         mDisplayInsetsController = displayInsetsController;
         mBackgroundController = backgroundController;
+        mShellController = shellController;
         mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.taskbar_frame_height);
         mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
@@ -81,6 +89,14 @@
 
     public void init() {
         mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+        mShellController.addConfigurationChangeListener(this);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfiguration) {
+        Trace.beginSection("FullscreenUnfoldTaskAnimator#onConfigurationChanged");
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        Trace.endSection();
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
index 6e10ebe..addd0a6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
@@ -28,8 +28,10 @@
 import android.animation.TypeEvaluator;
 import android.app.TaskInfo;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Insets;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.util.SparseArray;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -42,6 +44,8 @@
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
 import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
@@ -62,16 +66,18 @@
  * They use independent instances of SplitTaskUnfoldAnimator.
  */
 public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
-        DisplayInsetsController.OnInsetsChangedListener, SplitScreenListener {
+        DisplayInsetsController.OnInsetsChangedListener, SplitScreenListener,
+        ConfigurationChangeListener {
 
     private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
     private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
 
+    private final Context mContext;
     private final Executor mExecutor;
     private final DisplayInsetsController mDisplayInsetsController;
     private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
     private final int mExpandedTaskBarHeight;
-    private final float mWindowCornerRadiusPx;
+    private final ShellController mShellController;
     private final Lazy<Optional<SplitScreenController>> mSplitScreenController;
     private final UnfoldBackgroundController mUnfoldBackgroundController;
 
@@ -79,6 +85,7 @@
     private final Rect mSideStageBounds = new Rect();
     private final Rect mRootStageBounds = new Rect();
 
+    private float mWindowCornerRadiusPx;
     private InsetsSource mTaskbarInsetsSource;
 
     @SplitPosition
@@ -88,10 +95,12 @@
 
     public SplitTaskUnfoldAnimator(Context context, Executor executor,
             Lazy<Optional<SplitScreenController>> splitScreenController,
-            UnfoldBackgroundController unfoldBackgroundController,
+            ShellController shellController, UnfoldBackgroundController unfoldBackgroundController,
             DisplayInsetsController displayInsetsController) {
         mDisplayInsetsController = displayInsetsController;
         mExecutor = executor;
+        mContext = context;
+        mShellController = shellController;
         mUnfoldBackgroundController = unfoldBackgroundController;
         mSplitScreenController = splitScreenController;
         mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
@@ -103,6 +112,14 @@
     @Override
     public void init() {
         mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+        mShellController.addConfigurationChangeListener(this);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfiguration) {
+        Trace.beginSection("SplitTaskUnfoldAnimator#onConfigurationChanged");
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        Trace.endSection();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
new file mode 100644
index 0000000..9224b3c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -0,0 +1,275 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.os.Handler;
+import android.util.SparseArray;
+import android.view.Choreographer;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * View model for the window decoration with a caption and shadows. Works with
+ * {@link CaptionWindowDecoration}.
+ */
+public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private final Context mContext;
+    private final Handler mMainHandler;
+    private final Choreographer mMainChoreographer;
+    private final DisplayController mDisplayController;
+    private final SyncTransactionQueue mSyncQueue;
+    private TaskOperations mTaskOperations;
+
+    private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
+
+    public CaptionWindowDecorViewModel(
+            Context context,
+            Handler mainHandler,
+            Choreographer mainChoreographer,
+            ShellTaskOrganizer taskOrganizer,
+            DisplayController displayController,
+            SyncTransactionQueue syncQueue) {
+        mContext = context;
+        mMainHandler = mainHandler;
+        mMainChoreographer = mainChoreographer;
+        mTaskOrganizer = taskOrganizer;
+        mDisplayController = displayController;
+        mSyncQueue = syncQueue;
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
+        }
+    }
+
+    @Override
+    public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
+        mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
+    }
+
+    @Override
+    public boolean onTaskOpening(
+            RunningTaskInfo taskInfo,
+            SurfaceControl taskSurface,
+            SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT) {
+        if (!shouldShowWindowDecor(taskInfo)) return false;
+        createWindowDecoration(taskInfo, taskSurface, startT, finishT);
+        return true;
+    }
+
+    @Override
+    public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+        final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+
+        if (decoration == null) return;
+
+        decoration.relayout(taskInfo);
+        setupCaptionColor(taskInfo, decoration);
+    }
+
+    @Override
+    public void onTaskChanging(
+            RunningTaskInfo taskInfo,
+            SurfaceControl taskSurface,
+            SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT) {
+        final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+
+        if (!shouldShowWindowDecor(taskInfo)) {
+            if (decoration != null) {
+                destroyWindowDecoration(taskInfo);
+            }
+            return;
+        }
+
+        if (decoration == null) {
+            createWindowDecoration(taskInfo, taskSurface, startT, finishT);
+        } else {
+            decoration.relayout(taskInfo, startT, finishT);
+        }
+    }
+
+    @Override
+    public void onTaskClosing(
+            RunningTaskInfo taskInfo,
+            SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT) {
+        final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+        if (decoration == null) return;
+
+        decoration.relayout(taskInfo, startT, finishT);
+    }
+
+    @Override
+    public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
+        final CaptionWindowDecoration decoration =
+                mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
+        if (decoration == null) return;
+
+        decoration.close();
+    }
+
+    private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
+        final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
+        decoration.setCaptionColor(statusBarColor);
+    }
+
+    private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
+        return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+                || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
+                && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode()
+                == WINDOWING_MODE_FREEFORM);
+    }
+
+    private void createWindowDecoration(
+            RunningTaskInfo taskInfo,
+            SurfaceControl taskSurface,
+            SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT) {
+        final CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+        if (oldDecoration != null) {
+            // close the old decoration if it exists to avoid two window decorations being added
+            oldDecoration.close();
+        }
+        final CaptionWindowDecoration windowDecoration =
+                new CaptionWindowDecoration(
+                        mContext,
+                        mDisplayController,
+                        mTaskOrganizer,
+                        taskInfo,
+                        taskSurface,
+                        mMainHandler,
+                        mMainChoreographer,
+                        mSyncQueue);
+        mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
+
+        final TaskPositioner taskPositioner =
+                new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController);
+        final CaptionTouchEventListener touchEventListener =
+                new CaptionTouchEventListener(taskInfo, taskPositioner);
+        windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
+        windowDecoration.setDragPositioningCallback(taskPositioner);
+        windowDecoration.setDragDetector(touchEventListener.mDragDetector);
+        windowDecoration.relayout(taskInfo, startT, finishT);
+        setupCaptionColor(taskInfo, windowDecoration);
+    }
+
+    private class CaptionTouchEventListener implements
+            View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
+
+        private final int mTaskId;
+        private final WindowContainerToken mTaskToken;
+        private final DragPositioningCallback mDragPositioningCallback;
+        private final DragDetector mDragDetector;
+
+        private int mDragPointerId = -1;
+
+        private CaptionTouchEventListener(
+                RunningTaskInfo taskInfo,
+                DragPositioningCallback dragPositioningCallback) {
+            mTaskId = taskInfo.taskId;
+            mTaskToken = taskInfo.token;
+            mDragPositioningCallback = dragPositioningCallback;
+            mDragDetector = new DragDetector(this);
+        }
+
+        @Override
+        public void onClick(View v) {
+            final int id = v.getId();
+            if (id == R.id.close_window) {
+                mTaskOperations.closeTask(mTaskToken);
+            } else if (id == R.id.back_button) {
+                mTaskOperations.injectBackKey();
+            } else if (id == R.id.minimize_window) {
+                mTaskOperations.minimizeTask(mTaskToken);
+            } else if (id == R.id.maximize_window) {
+                RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+                mTaskOperations.maximizeTask(taskInfo);
+            }
+        }
+
+        @Override
+        public boolean onTouch(View v, MotionEvent e) {
+            if (v.getId() != R.id.caption) {
+                return false;
+            }
+            mDragDetector.onMotionEvent(e);
+
+            if (e.getAction() != MotionEvent.ACTION_DOWN) {
+                return false;
+            }
+            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+            if (taskInfo.isFocused) {
+                return false;
+            }
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            wct.reorder(mTaskToken, true /* onTop */);
+            mSyncQueue.queue(wct);
+            return true;
+        }
+
+        /**
+         * @param e {@link MotionEvent} to process
+         * @return {@code true} if a drag is happening; or {@code false} if it is not
+         */
+        @Override
+        public boolean handleMotionEvent(MotionEvent e) {
+            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+            if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+                return false;
+            }
+            switch (e.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN: {
+                    mDragPointerId = e.getPointerId(0);
+                    mDragPositioningCallback.onDragPositioningStart(
+                            0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
+                    break;
+                }
+                case MotionEvent.ACTION_MOVE: {
+                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+                    mDragPositioningCallback.onDragPositioningMove(
+                            e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+                    break;
+                }
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL: {
+                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+                    mDragPositioningCallback.onDragPositioningEnd(
+                            e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+                    break;
+                }
+            }
+            return true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
new file mode 100644
index 0000000..060dc4e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -0,0 +1,231 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.VectorDrawable;
+import android.os.Handler;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
+ * {@link CaptionWindowDecorViewModel}. The caption bar contains a back button, minimize button,
+ * maximize button and close button.
+ */
+public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
+    private final Handler mHandler;
+    private final Choreographer mChoreographer;
+    private final SyncTransactionQueue mSyncQueue;
+
+    private View.OnClickListener mOnCaptionButtonClickListener;
+    private View.OnTouchListener mOnCaptionTouchListener;
+    private DragPositioningCallback mDragPositioningCallback;
+    private DragResizeInputListener mDragResizeListener;
+    private DragDetector mDragDetector;
+
+    private RelayoutParams mRelayoutParams = new RelayoutParams();
+    private final RelayoutResult<WindowDecorLinearLayout> mResult =
+            new RelayoutResult<>();
+
+    CaptionWindowDecoration(
+            Context context,
+            DisplayController displayController,
+            ShellTaskOrganizer taskOrganizer,
+            RunningTaskInfo taskInfo,
+            SurfaceControl taskSurface,
+            Handler handler,
+            Choreographer choreographer,
+            SyncTransactionQueue syncQueue) {
+        super(context, displayController, taskOrganizer, taskInfo, taskSurface);
+
+        mHandler = handler;
+        mChoreographer = choreographer;
+        mSyncQueue = syncQueue;
+    }
+
+    void setCaptionListeners(
+            View.OnClickListener onCaptionButtonClickListener,
+            View.OnTouchListener onCaptionTouchListener) {
+        mOnCaptionButtonClickListener = onCaptionButtonClickListener;
+        mOnCaptionTouchListener = onCaptionTouchListener;
+    }
+
+    void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
+        mDragPositioningCallback = dragPositioningCallback;
+    }
+
+    void setDragDetector(DragDetector dragDetector) {
+        mDragDetector = dragDetector;
+        mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
+    }
+
+    @Override
+    void relayout(RunningTaskInfo taskInfo) {
+        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        relayout(taskInfo, t, t);
+        mSyncQueue.runInSync(transaction -> {
+            transaction.merge(t);
+            t.close();
+        });
+    }
+
+    void relayout(RunningTaskInfo taskInfo,
+            SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+        final int shadowRadiusID = taskInfo.isFocused
+                ? R.dimen.freeform_decor_shadow_focused_thickness
+                : R.dimen.freeform_decor_shadow_unfocused_thickness;
+        final boolean isFreeform =
+                taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+        final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
+
+        final WindowDecorLinearLayout oldRootView = mResult.mRootView;
+        final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        final int outsetLeftId = R.dimen.freeform_resize_handle;
+        final int outsetTopId = R.dimen.freeform_resize_handle;
+        final int outsetRightId = R.dimen.freeform_resize_handle;
+        final int outsetBottomId = R.dimen.freeform_resize_handle;
+
+        mRelayoutParams.reset();
+        mRelayoutParams.mRunningTaskInfo = taskInfo;
+        mRelayoutParams.mLayoutResId = R.layout.caption_window_decor;
+        mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+        mRelayoutParams.mShadowRadiusId = shadowRadiusID;
+        if (isDragResizeable) {
+            mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+        }
+
+        relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
+        // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
+
+        mTaskOrganizer.applyTransaction(wct);
+
+        if (mResult.mRootView == null) {
+            // This means something blocks the window decor from showing, e.g. the task is hidden.
+            // Nothing is set up in this case including the decoration surface.
+            return;
+        }
+        if (oldRootView != mResult.mRootView) {
+            setupRootView();
+        }
+
+        if (!isDragResizeable) {
+            closeDragResizeListener();
+            return;
+        }
+
+        if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
+            closeDragResizeListener();
+            mDragResizeListener = new DragResizeInputListener(
+                    mContext,
+                    mHandler,
+                    mChoreographer,
+                    mDisplay.getDisplayId(),
+                    mDecorationContainerSurface,
+                    mDragPositioningCallback);
+        }
+
+        final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
+                .getScaledTouchSlop();
+        mDragDetector.setTouchSlop(touchSlop);
+
+        final int resize_handle = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_handle);
+        final int resize_corner = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_corner);
+        mDragResizeListener.setGeometry(
+                mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
+    }
+
+    /**
+     * Sets up listeners when a new root view is created.
+     */
+    private void setupRootView() {
+        final View caption = mResult.mRootView.findViewById(R.id.caption);
+        caption.setOnTouchListener(mOnCaptionTouchListener);
+        final View close = caption.findViewById(R.id.close_window);
+        close.setOnClickListener(mOnCaptionButtonClickListener);
+        final View back = caption.findViewById(R.id.back_button);
+        back.setOnClickListener(mOnCaptionButtonClickListener);
+        final View minimize = caption.findViewById(R.id.minimize_window);
+        minimize.setOnClickListener(mOnCaptionButtonClickListener);
+        final View maximize = caption.findViewById(R.id.maximize_window);
+        maximize.setOnClickListener(mOnCaptionButtonClickListener);
+    }
+
+    void setCaptionColor(int captionColor) {
+        if (mResult.mRootView == null) {
+            return;
+        }
+
+        final View caption = mResult.mRootView.findViewById(R.id.caption);
+        final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground();
+        captionDrawable.setColor(captionColor);
+
+        final int buttonTintColorRes =
+                Color.valueOf(captionColor).luminance() < 0.5
+                        ? R.color.decor_button_light_color
+                        : R.color.decor_button_dark_color;
+        final ColorStateList buttonTintColor =
+                caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
+
+        final View back = caption.findViewById(R.id.back_button);
+        final VectorDrawable backBackground = (VectorDrawable) back.getBackground();
+        backBackground.setTintList(buttonTintColor);
+
+        final View minimize = caption.findViewById(R.id.minimize_window);
+        final VectorDrawable minimizeBackground = (VectorDrawable) minimize.getBackground();
+        minimizeBackground.setTintList(buttonTintColor);
+
+        final View maximize = caption.findViewById(R.id.maximize_window);
+        final VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground();
+        maximizeBackground.setTintList(buttonTintColor);
+
+        final View close = caption.findViewById(R.id.close_window);
+        final VectorDrawable closeBackground = (VectorDrawable) close.getBackground();
+        closeBackground.setTintList(buttonTintColor);
+    }
+
+    private void closeDragResizeListener() {
+        if (mDragResizeListener == null) {
+            return;
+        }
+        mDragResizeListener.close();
+        mDragResizeListener = null;
+    }
+
+    @Override
+    public void close() {
+        closeDragResizeListener();
+        super.close();
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 00aab67..c517f7b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -20,29 +20,27 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityTaskManager;
 import android.content.Context;
+import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.SystemClock;
-import android.util.Log;
 import android.util.SparseArray;
 import android.view.Choreographer;
 import android.view.InputChannel;
-import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.InputMonitor;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
 
 import androidx.annotation.Nullable;
 
@@ -55,7 +53,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
-import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import java.util.Optional;
 
@@ -74,9 +72,8 @@
     private final Choreographer mMainChoreographer;
     private final DisplayController mDisplayController;
     private final SyncTransactionQueue mSyncQueue;
-    private FreeformTaskTransitionStarter mTransitionStarter;
-    private Optional<DesktopModeController> mDesktopModeController;
-    private Optional<DesktopTasksController> mDesktopTasksController;
+    private final Optional<DesktopModeController> mDesktopModeController;
+    private final Optional<DesktopTasksController> mDesktopTasksController;
     private boolean mTransitionDragActive;
 
     private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -84,7 +81,10 @@
     private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId =
             new SparseArray<>();
     private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
-    private InputMonitorFactory mInputMonitorFactory;
+    private final InputMonitorFactory mInputMonitorFactory;
+    private TaskOperations mTaskOperations;
+
+    private Optional<SplitScreenController> mSplitScreenController;
 
     public DesktopModeWindowDecorViewModel(
             Context context,
@@ -94,7 +94,8 @@
             DisplayController displayController,
             SyncTransactionQueue syncQueue,
             Optional<DesktopModeController> desktopModeController,
-            Optional<DesktopTasksController> desktopTasksController) {
+            Optional<DesktopTasksController> desktopTasksController,
+            Optional<SplitScreenController> splitScreenController) {
         this(
                 context,
                 mainHandler,
@@ -104,6 +105,7 @@
                 syncQueue,
                 desktopModeController,
                 desktopTasksController,
+                splitScreenController,
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory());
     }
@@ -118,6 +120,7 @@
             SyncTransactionQueue syncQueue,
             Optional<DesktopModeController> desktopModeController,
             Optional<DesktopTasksController> desktopTasksController,
+            Optional<SplitScreenController> splitScreenController,
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory) {
         mContext = context;
@@ -126,6 +129,7 @@
         mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mTaskOrganizer = taskOrganizer;
         mDisplayController = displayController;
+        mSplitScreenController = splitScreenController;
         mSyncQueue = syncQueue;
         mDesktopModeController = desktopModeController;
         mDesktopTasksController = desktopTasksController;
@@ -136,7 +140,7 @@
 
     @Override
     public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
-        mTransitionStarter = transitionStarter;
+        mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
     }
 
     @Override
@@ -162,6 +166,7 @@
         }
 
         decoration.relayout(taskInfo);
+        setupCaptionColor(taskInfo, decoration);
     }
 
     @Override
@@ -204,46 +209,48 @@
         if (decoration == null) return;
 
         decoration.close();
-        int displayId = taskInfo.displayId;
+        final int displayId = taskInfo.displayId;
         if (mEventReceiversByDisplay.contains(displayId)) {
             removeTaskFromEventReceiver(displayId);
         }
     }
 
-    private class CaptionTouchEventListener implements
-            View.OnClickListener, View.OnTouchListener {
+    private class DesktopModeTouchEventListener implements
+            View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
 
         private final int mTaskId;
         private final WindowContainerToken mTaskToken;
-        private final DragResizeCallback mDragResizeCallback;
+        private final DragPositioningCallback mDragPositioningCallback;
         private final DragDetector mDragDetector;
 
         private int mDragPointerId = -1;
 
-        private CaptionTouchEventListener(
+        private DesktopModeTouchEventListener(
                 RunningTaskInfo taskInfo,
-                DragResizeCallback dragResizeCallback,
-                DragDetector dragDetector) {
+                DragPositioningCallback dragPositioningCallback) {
             mTaskId = taskInfo.taskId;
             mTaskToken = taskInfo.token;
-            mDragResizeCallback = dragResizeCallback;
-            mDragDetector = dragDetector;
+            mDragPositioningCallback = dragPositioningCallback;
+            mDragDetector = new DragDetector(this);
         }
 
         @Override
         public void onClick(View v) {
-            DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final int id = v.getId();
-            if (id == R.id.close_window) {
-                WindowContainerTransaction wct = new WindowContainerTransaction();
-                wct.removeTask(mTaskToken);
-                if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-                    mTransitionStarter.startRemoveTransition(wct);
-                } else {
-                    mSyncQueue.queue(wct);
+            if (id == R.id.close_window || id == R.id.close_button) {
+                mTaskOperations.closeTask(mTaskToken);
+                if (mSplitScreenController.isPresent()
+                        && mSplitScreenController.get().isSplitScreenVisible()) {
+                    int remainingTaskPosition = mTaskId == mSplitScreenController.get()
+                            .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT).taskId
+                            ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
+                    ActivityManager.RunningTaskInfo remainingTask = mSplitScreenController.get()
+                            .getTaskInfo(remainingTaskPosition);
+                    mSplitScreenController.get().moveTaskToFullscreen(remainingTask.taskId);
                 }
             } else if (id == R.id.back_button) {
-                injectBackKey();
+                mTaskOperations.injectBackKey();
             } else if (id == R.id.caption_handle) {
                 decoration.createHandleMenu();
             } else if (id == R.id.desktop_button) {
@@ -255,106 +262,84 @@
                 mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId));
                 decoration.closeHandleMenu();
                 decoration.setButtonVisibility(false);
-            }
-        }
-
-        private void injectBackKey() {
-            sendBackEvent(KeyEvent.ACTION_DOWN);
-            sendBackEvent(KeyEvent.ACTION_UP);
-        }
-
-        private void sendBackEvent(int action) {
-            final long when = SystemClock.uptimeMillis();
-            final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK,
-                    0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD,
-                    0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
-                    InputDevice.SOURCE_KEYBOARD);
-
-            ev.setDisplayId(mContext.getDisplay().getDisplayId());
-            if (!InputManager.getInstance()
-                    .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
-                Log.e(TAG, "Inject input event fail");
+            } else if (id == R.id.collapse_menu_button) {
+                decoration.closeHandleMenu();
             }
         }
 
         @Override
         public boolean onTouch(View v, MotionEvent e) {
-            boolean isDrag = false;
-            int id = v.getId();
+            final int id = v.getId();
             if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) {
                 return false;
             }
-            if (id == R.id.caption_handle) {
-                isDrag = mDragDetector.detectDragEvent(e);
-                handleEventForMove(e);
+            switch (e.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    mDragDetector.onMotionEvent(e);
+                    final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+                    if (taskInfo.isFocused) {
+                        return mDragDetector.isDragEvent();
+                    }
+                    return false;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    boolean res = mDragDetector.isDragEvent();
+                    mDragDetector.onMotionEvent(e);
+                    return res;
+                default:
+                    mDragDetector.onMotionEvent(e);
+                    return mDragDetector.isDragEvent();
             }
-            if (e.getAction() != MotionEvent.ACTION_DOWN) {
-                return isDrag;
-            }
-            RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
-            if (taskInfo.isFocused) {
-                return isDrag;
-            }
-            WindowContainerTransaction wct = new WindowContainerTransaction();
-            wct.reorder(mTaskToken, true /* onTop */);
-            mSyncQueue.queue(wct);
-            return true;
         }
 
         /**
          * @param e {@link MotionEvent} to process
-         * @return {@code true} if a drag is happening; or {@code false} if it is not
+         * @return {@code true} if the motion event is handled.
          */
-        private void handleEventForMove(MotionEvent e) {
-            RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+        @Override
+        public boolean handleMotionEvent(MotionEvent e) {
+            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
             if (DesktopModeStatus.isProto2Enabled()
                     && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
-                return;
+                return false;
             }
             if (DesktopModeStatus.isProto1Enabled() && mDesktopModeController.isPresent()
-                    && mDesktopModeController.get().getDisplayAreaWindowingMode(
-                    taskInfo.displayId)
+                    && mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId)
                     == WINDOWING_MODE_FULLSCREEN) {
-                return;
+                return false;
             }
             switch (e.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN: {
                     mDragPointerId = e.getPointerId(0);
-                    mDragResizeCallback.onDragResizeStart(
+                    mDragPositioningCallback.onDragPositioningStart(
                             0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
                     break;
                 }
                 case MotionEvent.ACTION_MOVE: {
-                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
-                    mDragResizeCallback.onDragResizeMove(
+                    final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+                    mDragPositioningCallback.onDragPositioningMove(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     break;
                 }
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL: {
-                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
-                    int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId)
-                            .stableInsets().top;
-                    mDragResizeCallback.onDragResizeEnd(
+                    final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+                    final int statusBarHeight = mDisplayController
+                            .getDisplayLayout(taskInfo.displayId).stableInsets().top;
+                    mDragPositioningCallback.onDragPositioningEnd(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     if (e.getRawY(dragPointerIdx) <= statusBarHeight) {
-                        if (DesktopModeStatus.isProto2Enabled()) {
-                            if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
-                                // Switch a single task to fullscreen
-                                mDesktopTasksController.ifPresent(
-                                        c -> c.moveToFullscreen(taskInfo));
-                            }
-                        } else if (DesktopModeStatus.isProto1Enabled()) {
-                            if (DesktopModeStatus.isActive(mContext)) {
-                                // Turn off desktop mode
-                                mDesktopModeController.ifPresent(
-                                        c -> c.setDesktopModeActive(false));
-                            }
+                        if (DesktopModeStatus.isProto2Enabled()
+                                && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                            // Switch a single task to fullscreen
+                            mDesktopTasksController.ifPresent(
+                                    c -> c.moveToFullscreen(taskInfo));
                         }
                     }
                     break;
                 }
             }
+            return true;
         }
     }
 
@@ -408,7 +393,7 @@
      */
     private void incrementEventReceiverTasks(int displayId) {
         if (mEventReceiversByDisplay.contains(displayId)) {
-            EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+            final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
             eventReceiver.incrementTaskNumber();
         } else {
             createInputChannel(displayId);
@@ -418,7 +403,7 @@
     // If all tasks on this display are gone, we don't need to monitor its input.
     private void removeTaskFromEventReceiver(int displayId) {
         if (!mEventReceiversByDisplay.contains(displayId)) return;
-        EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+        final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
         if (eventReceiver == null) return;
         eventReceiver.decrementTaskNumber();
         if (eventReceiver.getTasksOnDisplay() == 0) {
@@ -432,18 +417,14 @@
      * @param ev the {@link MotionEvent} received by {@link EventReceiver}
      */
     private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
+        final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev);
         if (DesktopModeStatus.isProto2Enabled()) {
-            DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
-            if (focusedDecor == null
-                    || focusedDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
-                handleCaptionThroughStatusBar(ev);
-            }
-        } else if (DesktopModeStatus.isProto1Enabled()) {
-            if (!DesktopModeStatus.isActive(mContext)) {
-                handleCaptionThroughStatusBar(ev);
+            if (relevantDecor == null
+                    || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+                handleCaptionThroughStatusBar(ev, relevantDecor);
             }
         }
-        handleEventOutsideFocusedCaption(ev);
+        handleEventOutsideFocusedCaption(ev, relevantDecor);
         // Prevent status bar from reacting to a caption drag.
         if (DesktopModeStatus.isProto2Enabled()) {
             if (mTransitionDragActive) {
@@ -457,16 +438,16 @@
     }
 
     // If an UP/CANCEL action is received outside of caption bounds, turn off handle menu
-    private void handleEventOutsideFocusedCaption(MotionEvent ev) {
-        int action = ev.getActionMasked();
+    private void handleEventOutsideFocusedCaption(MotionEvent ev,
+            DesktopModeWindowDecoration relevantDecor) {
+        final int action = ev.getActionMasked();
         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
-            DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
-            if (focusedDecor == null) {
+            if (relevantDecor == null) {
                 return;
             }
 
             if (!mTransitionDragActive) {
-                focusedDecor.closeHandleMenuIfNeeded(ev);
+                relevantDecor.closeHandleMenuIfNeeded(ev);
             }
         }
     }
@@ -476,42 +457,38 @@
      * Perform caption actions if not able to through normal means.
      * Turn on desktop mode if handle is dragged below status bar.
      */
-    private void handleCaptionThroughStatusBar(MotionEvent ev) {
+    private void handleCaptionThroughStatusBar(MotionEvent ev,
+            DesktopModeWindowDecoration relevantDecor) {
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_DOWN: {
                 // Begin drag through status bar if applicable.
-                DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
-                if (focusedDecor != null) {
+                if (relevantDecor != null) {
                     boolean dragFromStatusBarAllowed = false;
                     if (DesktopModeStatus.isProto2Enabled()) {
                         // In proto2 any full screen task can be dragged to freeform
-                        dragFromStatusBarAllowed = focusedDecor.mTaskInfo.getWindowingMode()
+                        dragFromStatusBarAllowed = relevantDecor.mTaskInfo.getWindowingMode()
                                 == WINDOWING_MODE_FULLSCREEN;
-                    } else if (DesktopModeStatus.isProto1Enabled()) {
-                        // In proto1 task can be dragged to freeform when not in desktop mode
-                        dragFromStatusBarAllowed = !DesktopModeStatus.isActive(mContext);
                     }
 
-                    if (dragFromStatusBarAllowed && focusedDecor.checkTouchEventInHandle(ev)) {
+                    if (dragFromStatusBarAllowed && relevantDecor.checkTouchEventInHandle(ev)) {
                         mTransitionDragActive = true;
                     }
                 }
                 break;
             }
             case MotionEvent.ACTION_UP: {
-                DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
-                if (focusedDecor == null) {
+                if (relevantDecor == null) {
                     mTransitionDragActive = false;
                     return;
                 }
                 if (mTransitionDragActive) {
                     mTransitionDragActive = false;
-                    int statusBarHeight = mDisplayController
-                            .getDisplayLayout(focusedDecor.mTaskInfo.displayId).stableInsets().top;
+                    final int statusBarHeight = mDisplayController
+                            .getDisplayLayout(relevantDecor.mTaskInfo.displayId).stableInsets().top;
                     if (ev.getY() > statusBarHeight) {
                         if (DesktopModeStatus.isProto2Enabled()) {
                             mDesktopTasksController.ifPresent(
-                                    c -> c.moveToDesktop(focusedDecor.mTaskInfo));
+                                    c -> c.moveToDesktop(relevantDecor.mTaskInfo));
                         } else if (DesktopModeStatus.isProto1Enabled()) {
                             mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
                         }
@@ -519,7 +496,7 @@
                         return;
                     }
                 }
-                focusedDecor.checkClickEvent(ev);
+                relevantDecor.checkClickEvent(ev);
                 break;
             }
             case MotionEvent.ACTION_CANCEL: {
@@ -529,11 +506,43 @@
     }
 
     @Nullable
+    private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
+        if (mSplitScreenController.isPresent()
+                && mSplitScreenController.get().isSplitScreenVisible()) {
+            // We can't look at focused task here as only one task will have focus.
+            return getSplitScreenDecor(ev);
+        } else {
+            return getFocusedDecor();
+        }
+    }
+
+    @Nullable
+    private DesktopModeWindowDecoration getSplitScreenDecor(MotionEvent ev) {
+        ActivityManager.RunningTaskInfo topOrLeftTask =
+                mSplitScreenController.get().getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+        ActivityManager.RunningTaskInfo bottomOrRightTask =
+                mSplitScreenController.get().getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+        if (topOrLeftTask != null && topOrLeftTask.getConfiguration()
+                .windowConfiguration.getBounds().contains((int) ev.getX(), (int) ev.getY())) {
+            return mWindowDecorByTaskId.get(topOrLeftTask.taskId);
+        } else if (bottomOrRightTask != null && bottomOrRightTask.getConfiguration()
+                .windowConfiguration.getBounds().contains((int) ev.getX(), (int) ev.getY())) {
+            Rect bottomOrRightBounds = bottomOrRightTask.getConfiguration().windowConfiguration
+                    .getBounds();
+            ev.offsetLocation(-bottomOrRightBounds.left, -bottomOrRightBounds.top);
+            return mWindowDecorByTaskId.get(bottomOrRightTask.taskId);
+        } else {
+            return null;
+        }
+
+    }
+
+    @Nullable
     private DesktopModeWindowDecoration getFocusedDecor() {
-        int size = mWindowDecorByTaskId.size();
+        final int size = mWindowDecorByTaskId.size();
         DesktopModeWindowDecoration focusedDecor = null;
         for (int i = 0; i < size; i++) {
-            DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
+            final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
             if (decor != null && decor.isFocused()) {
                 focusedDecor = decor;
                 break;
@@ -543,24 +552,31 @@
     }
 
     private void createInputChannel(int displayId) {
-        InputManager inputManager = InputManager.getInstance();
-        InputMonitor inputMonitor =
+        final InputManager inputManager = InputManager.getInstance();
+        final InputMonitor inputMonitor =
                 mInputMonitorFactory.create(inputManager, mContext);
-        EventReceiver eventReceiver = new EventReceiver(inputMonitor,
+        final EventReceiver eventReceiver = new EventReceiver(inputMonitor,
                 inputMonitor.getInputChannel(), Looper.myLooper());
         mEventReceiversByDisplay.put(displayId, eventReceiver);
     }
 
     private void disposeInputChannel(int displayId) {
-        EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId);
+        final EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId);
         if (eventReceiver != null) {
             eventReceiver.dispose();
         }
     }
 
+    private void setupCaptionColor(RunningTaskInfo taskInfo,
+            DesktopModeWindowDecoration decoration) {
+        if (taskInfo == null || taskInfo.taskDescription == null) return;
+        final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
+        decoration.setCaptionColor(statusBarColor);
+    }
+
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
-        return DesktopModeStatus.isAnyEnabled()
+        return DesktopModeStatus.isProto2Enabled()
                 && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
                 && mDisplayController.getDisplayContext(taskInfo.displayId)
                 .getResources().getConfiguration().smallestScreenWidthDp >= 600;
@@ -571,7 +587,7 @@
             SurfaceControl taskSurface,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
-        DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+        final DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
         if (oldDecoration != null) {
             // close the old decoration if it exists to avoid two window decorations being added
             oldDecoration.close();
@@ -588,14 +604,16 @@
                         mSyncQueue);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
-        TaskPositioner taskPositioner =
-                new TaskPositioner(mTaskOrganizer, windowDecoration, mDragStartListener);
-        CaptionTouchEventListener touchEventListener =
-                new CaptionTouchEventListener(
-                        taskInfo, taskPositioner, windowDecoration.getDragDetector());
+        final TaskPositioner taskPositioner =
+                new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController,
+                        mDragStartListener);
+        final DesktopModeTouchEventListener touchEventListener =
+                new DesktopModeTouchEventListener(taskInfo, taskPositioner);
         windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
-        windowDecoration.setDragResizeCallback(taskPositioner);
+        windowDecoration.setDragPositioningCallback(taskPositioner);
+        windowDecoration.setDragDetector(touchEventListener.mDragDetector);
         windowDecoration.relayout(taskInfo, startT, finishT);
+        setupCaptionColor(taskInfo, windowDecoration);
         incrementEventReceiverTasks(taskInfo.displayId);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 9c2beb9..0779f1d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -20,19 +20,25 @@
 
 import android.app.ActivityManager;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.PointF;
-import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
+import android.util.Log;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.widget.ImageView;
+import android.widget.TextView;
 import android.window.WindowContainerTransaction;
 
 import com.android.wm.shell.R;
@@ -49,25 +55,26 @@
  * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
  */
 public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
+    private static final String TAG = "DesktopModeWindowDecoration";
     private final Handler mHandler;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
 
     private View.OnClickListener mOnCaptionButtonClickListener;
     private View.OnTouchListener mOnCaptionTouchListener;
-    private DragResizeCallback mDragResizeCallback;
-
+    private DragPositioningCallback mDragPositioningCallback;
     private DragResizeInputListener mDragResizeListener;
+    private DragDetector mDragDetector;
 
     private RelayoutParams mRelayoutParams = new RelayoutParams();
+    private final int mCaptionMenuHeightId = R.dimen.freeform_decor_caption_menu_height;
     private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
             new WindowDecoration.RelayoutResult<>();
 
     private boolean mDesktopActive;
-
-    private DragDetector mDragDetector;
-
     private AdditionalWindow mHandleMenu;
+    private final int mHandleMenuWidthId = R.dimen.freeform_decor_caption_menu_width;
+    private PointF mHandleMenuPosition = new PointF();
 
     DesktopModeWindowDecoration(
             Context context,
@@ -84,7 +91,6 @@
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
         mDesktopActive = DesktopModeStatus.isActive(mContext);
-        mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
     }
 
     void setCaptionListeners(
@@ -94,12 +100,13 @@
         mOnCaptionTouchListener = onCaptionTouchListener;
     }
 
-    void setDragResizeCallback(DragResizeCallback dragResizeCallback) {
-        mDragResizeCallback = dragResizeCallback;
+    void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
+        mDragPositioningCallback = dragPositioningCallback;
     }
 
-    DragDetector getDragDetector() {
-        return mDragDetector;
+    void setDragDetector(DragDetector dragDetector) {
+        mDragDetector = dragDetector;
+        mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
     }
 
     @Override
@@ -121,38 +128,26 @@
                 taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
         final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
 
-        WindowDecorLinearLayout oldRootView = mResult.mRootView;
+        final WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
 
-        int outsetLeftId = R.dimen.freeform_resize_handle;
-        int outsetTopId = R.dimen.freeform_resize_handle;
-        int outsetRightId = R.dimen.freeform_resize_handle;
-        int outsetBottomId = R.dimen.freeform_resize_handle;
+        final int outsetLeftId = R.dimen.freeform_resize_handle;
+        final int outsetTopId = R.dimen.freeform_resize_handle;
+        final int outsetRightId = R.dimen.freeform_resize_handle;
+        final int outsetBottomId = R.dimen.freeform_resize_handle;
 
         mRelayoutParams.reset();
         mRelayoutParams.mRunningTaskInfo = taskInfo;
         mRelayoutParams.mLayoutResId = R.layout.desktop_mode_window_decor;
         mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
-        mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
         mRelayoutParams.mShadowRadiusId = shadowRadiusID;
         if (isDragResizeable) {
             mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
         }
-        final Resources resources = mDecorWindowContext.getResources();
-        final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
-        final int captionHeight = loadDimensionPixelSize(resources,
-                mRelayoutParams.mCaptionHeightId);
-        final int captionWidth = loadDimensionPixelSize(resources,
-                mRelayoutParams.mCaptionWidthId);
-        final int captionLeft = taskBounds.width() / 2
-                - captionWidth / 2;
-        final int captionTop = taskBounds.top
-                <= captionHeight / 2 ? 0 : -captionHeight / 2;
-        mRelayoutParams.setCaptionPosition(captionLeft, captionTop);
 
         relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
-        taskInfo = null; // Clear it just in case we use it accidentally
+        // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
 
         mTaskOrganizer.applyTransaction(wct);
 
@@ -194,15 +189,16 @@
                     mChoreographer,
                     mDisplay.getDisplayId(),
                     mDecorationContainerSurface,
-                    mDragResizeCallback);
+                    mDragPositioningCallback);
         }
 
-        int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop();
+        final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
+                .getScaledTouchSlop();
         mDragDetector.setTouchSlop(touchSlop);
 
-        int resize_handle = mResult.mRootView.getResources()
+        final int resize_handle = mResult.mRootView.getResources()
                 .getDimensionPixelSize(R.dimen.freeform_resize_handle);
-        int resize_corner = mResult.mRootView.getResources()
+        final int resize_corner = mResult.mRootView.getResources()
                 .getDimensionPixelSize(R.dimen.freeform_resize_corner);
         mDragResizeListener.setGeometry(
                 mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
@@ -212,40 +208,65 @@
      * Sets up listeners when a new root view is created.
      */
     private void setupRootView() {
-        View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+        final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
         caption.setOnTouchListener(mOnCaptionTouchListener);
-        View close = caption.findViewById(R.id.close_window);
-        close.setOnClickListener(mOnCaptionButtonClickListener);
-        View back = caption.findViewById(R.id.back_button);
-        back.setOnClickListener(mOnCaptionButtonClickListener);
-        View handle = caption.findViewById(R.id.caption_handle);
+        final View handle = caption.findViewById(R.id.caption_handle);
         handle.setOnTouchListener(mOnCaptionTouchListener);
         handle.setOnClickListener(mOnCaptionButtonClickListener);
+        if (DesktopModeStatus.isProto1Enabled()) {
+            final View back = caption.findViewById(R.id.back_button);
+            back.setOnClickListener(mOnCaptionButtonClickListener);
+            final View close = caption.findViewById(R.id.close_window);
+            close.setOnClickListener(mOnCaptionButtonClickListener);
+        }
         updateButtonVisibility();
     }
 
     private void setupHandleMenu() {
-        View menu = mHandleMenu.mWindowViewHost.getView();
-        View fullscreen = menu.findViewById(R.id.fullscreen_button);
+        final View menu = mHandleMenu.mWindowViewHost.getView();
+        final View fullscreen = menu.findViewById(R.id.fullscreen_button);
         fullscreen.setOnClickListener(mOnCaptionButtonClickListener);
-        View desktop = menu.findViewById(R.id.desktop_button);
-        desktop.setOnClickListener(mOnCaptionButtonClickListener);
-        View split = menu.findViewById(R.id.split_screen_button);
+        final View desktop = menu.findViewById(R.id.desktop_button);
+        if (DesktopModeStatus.isProto2Enabled()) {
+            desktop.setOnClickListener(mOnCaptionButtonClickListener);
+        } else if (DesktopModeStatus.isProto1Enabled()) {
+            desktop.setVisibility(View.GONE);
+        }
+        final View split = menu.findViewById(R.id.split_screen_button);
         split.setOnClickListener(mOnCaptionButtonClickListener);
-        View more = menu.findViewById(R.id.more_button);
-        more.setOnClickListener(mOnCaptionButtonClickListener);
+        final View close = menu.findViewById(R.id.close_button);
+        close.setOnClickListener(mOnCaptionButtonClickListener);
+        final View collapse = menu.findViewById(R.id.collapse_menu_button);
+        collapse.setOnClickListener(mOnCaptionButtonClickListener);
+        menu.setOnTouchListener(mOnCaptionTouchListener);
+
+        String packageName = mTaskInfo.baseActivity.getPackageName();
+        PackageManager pm = mContext.getApplicationContext().getPackageManager();
+        // TODO(b/268363572): Use IconProvider or BaseIconCache to set drawable/name.
+        try {
+            ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
+                    PackageManager.ApplicationInfoFlags.of(0));
+            final ImageView appIcon = menu.findViewById(R.id.application_icon);
+            appIcon.setImageDrawable(pm.getApplicationIcon(applicationInfo));
+            final TextView appName = menu.findViewById(R.id.application_name);
+            appName.setText(pm.getApplicationLabel(applicationInfo));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Package not found: " + packageName, e);
+        }
     }
 
     /**
      * Sets caption visibility based on task focus.
-     *
+     * Note: Only applicable to Desktop Proto 1; Proto 2 only closes handle menu on focus loss
      * @param visible whether or not the caption should be visible
      */
     private void setCaptionVisibility(boolean visible) {
-        int v = visible ? View.VISIBLE : View.GONE;
-        View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
-        captionView.setVisibility(v);
         if (!visible) closeHandleMenu();
+        if (!DesktopModeStatus.isProto1Enabled()) return;
+        final int v = visible ? View.VISIBLE : View.GONE;
+        final View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+        captionView.setVisibility(v);
+
     }
 
     /**
@@ -264,27 +285,56 @@
      * Show or hide buttons
      */
     void setButtonVisibility(boolean visible) {
-        int visibility = visible ? View.VISIBLE : View.GONE;
-        View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
-        View back = caption.findViewById(R.id.back_button);
-        View close = caption.findViewById(R.id.close_window);
+        final int visibility = visible && DesktopModeStatus.isProto1Enabled()
+                ? View.VISIBLE : View.GONE;
+        final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+        final View back = caption.findViewById(R.id.back_button);
+        final View close = caption.findViewById(R.id.close_window);
         back.setVisibility(visibility);
         close.setVisibility(visibility);
-        int buttonTintColorRes =
+        final int buttonTintColorRes =
                 mDesktopActive ? R.color.decor_button_dark_color
                         : R.color.decor_button_light_color;
-        ColorStateList buttonTintColor =
+        final ColorStateList buttonTintColor =
                 caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
-        View handle = caption.findViewById(R.id.caption_handle);
-        VectorDrawable handleBackground = (VectorDrawable) handle.getBackground();
+        final View handle = caption.findViewById(R.id.caption_handle);
+        final VectorDrawable handleBackground = (VectorDrawable) handle.getBackground();
         handleBackground.setTintList(buttonTintColor);
-        caption.getBackground().setTint(visible ? Color.WHITE : Color.TRANSPARENT);
     }
 
     boolean isHandleMenuActive() {
         return mHandleMenu != null;
     }
 
+    void setCaptionColor(int captionColor) {
+        if (mResult.mRootView == null) {
+            return;
+        }
+
+        final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+        final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground();
+        captionDrawable.setColor(captionColor);
+
+        final int buttonTintColorRes =
+                Color.valueOf(captionColor).luminance() < 0.5
+                        ? R.color.decor_button_light_color
+                        : R.color.decor_button_dark_color;
+        final ColorStateList buttonTintColor =
+                caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
+
+        final View handle = caption.findViewById(R.id.caption_handle);
+        final Drawable handleBackground = handle.getBackground();
+        handleBackground.setTintList(buttonTintColor);
+        if (DesktopModeStatus.isProto1Enabled()) {
+            final View back = caption.findViewById(R.id.back_button);
+            final Drawable backBackground = back.getBackground();
+            backBackground.setTintList(buttonTintColor);
+            final View close = caption.findViewById(R.id.close_window);
+            final Drawable closeBackground = close.getBackground();
+            closeBackground.setTintList(buttonTintColor);
+        }
+    }
+
     private void closeDragResizeListener() {
         if (mDragResizeListener == null) {
             return;
@@ -297,16 +347,25 @@
      * Create and display handle menu window
      */
     void createHandleMenu() {
-        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
         final Resources resources = mDecorWindowContext.getResources();
-        int x = mRelayoutParams.mCaptionX;
-        int y = mRelayoutParams.mCaptionY;
-        int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
-        int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+        final int captionWidth = mTaskInfo.getConfiguration()
+                .windowConfiguration.getBounds().width();
+        final int menuWidth = loadDimensionPixelSize(resources, mHandleMenuWidthId);
+        final int menuHeight = loadDimensionPixelSize(resources, mCaptionMenuHeightId);
+
+        // Elevation gives the appearance of a changed x/y coordinate; this is to fix that
+        int elevationOffset = 2 * loadDimensionPixelSize(resources,
+                R.dimen.caption_menu_elevation);
+
+        final int x = mRelayoutParams.mCaptionX + (captionWidth / 2) - (menuWidth / 2)
+                - mResult.mDecorContainerOffsetX - elevationOffset;
+        final int y =
+                mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY - elevationOffset;
+        mHandleMenuPosition.set(x, y);
         String namePrefix = "Caption Menu";
-        mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t,
-                x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY,
-                width, height);
+        mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t, x, y,
+                menuWidth, menuHeight, 2 * elevationOffset);
         mSyncQueue.runInSync(transaction -> {
             transaction.merge(t);
             t.close();
@@ -335,10 +394,17 @@
      * @param ev the tapped point to compare against
      */
     void closeHandleMenuIfNeeded(MotionEvent ev) {
-        if (isHandleMenuActive()) {
-            if (!checkEventInCaptionView(ev, R.id.desktop_mode_caption)) {
-                closeHandleMenu();
-            }
+        if (!isHandleMenuActive()) return;
+
+        // When this is called before the layout is fully inflated, width will be 0.
+        // Menu is not visible in this scenario, so skip the check if that is the case.
+        if (mHandleMenu.mWindowViewHost.getView().getWidth() == 0) return;
+
+        PointF inputPoint = offsetCaptionLocation(ev);
+        if (!pointInView(mHandleMenu.mWindowViewHost.getView(),
+                inputPoint.x - mHandleMenuPosition.x - mResult.mDecorContainerOffsetX,
+                inputPoint.y - mHandleMenuPosition.y - mResult.mDecorContainerOffsetY)) {
+            closeHandleMenu();
         }
     }
 
@@ -353,8 +419,8 @@
      * @return the point of the input in local space
      */
     private PointF offsetCaptionLocation(MotionEvent ev) {
-        PointF result = new PointF(ev.getX(), ev.getY());
-        Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
+        final PointF result = new PointF(ev.getX(), ev.getY());
+        final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
                 .positionInParent;
         result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY);
         result.offset(-positionInParent.x, -positionInParent.y);
@@ -370,9 +436,9 @@
      */
     private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) {
         if (mResult.mRootView == null) return false;
-        PointF inputPoint = offsetCaptionLocation(ev);
-        View view = mResult.mRootView.findViewById(layoutId);
-        return view != null && view.pointInView(inputPoint.x, inputPoint.y, 0);
+        final PointF inputPoint = offsetCaptionLocation(ev);
+        final View view = mResult.mRootView.findViewById(layoutId);
+        return view != null && pointInView(view, inputPoint.x, inputPoint.y);
     }
 
     boolean checkTouchEventInHandle(MotionEvent ev) {
@@ -389,32 +455,35 @@
      */
     void checkClickEvent(MotionEvent ev) {
         if (mResult.mRootView == null) return;
-        View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
-        PointF inputPoint = offsetCaptionLocation(ev);
         if (!isHandleMenuActive()) {
-            View handle = caption.findViewById(R.id.caption_handle);
-            clickIfPointInView(inputPoint, handle);
+            final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+            final View handle = caption.findViewById(R.id.caption_handle);
+            clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle);
         } else {
-            View menu = mHandleMenu.mWindowViewHost.getView();
-            View fullscreen = menu.findViewById(R.id.fullscreen_button);
-            if (clickIfPointInView(inputPoint, fullscreen)) return;
-            View desktop = menu.findViewById(R.id.desktop_button);
-            if (clickIfPointInView(inputPoint, desktop)) return;
-            View split = menu.findViewById(R.id.split_screen_button);
-            if (clickIfPointInView(inputPoint, split)) return;
-            View more = menu.findViewById(R.id.more_button);
-            clickIfPointInView(inputPoint, more);
+            final View menu = mHandleMenu.mWindowViewHost.getView();
+            final int captionWidth = mTaskInfo.getConfiguration().windowConfiguration
+                    .getBounds().width();
+            final int menuX = mRelayoutParams.mCaptionX + (captionWidth / 2)
+                    - (menu.getWidth() / 2);
+            final PointF inputPoint = new PointF(ev.getX() - menuX, ev.getY());
+            final View collapse = menu.findViewById(R.id.collapse_menu_button);
+            if (clickIfPointInView(inputPoint, collapse)) return;
         }
     }
 
     private boolean clickIfPointInView(PointF inputPoint, View v) {
-        if (v.pointInView(inputPoint.x - v.getLeft(), inputPoint.y, 0)) {
+        if (pointInView(v, inputPoint.x, inputPoint.y)) {
             mOnCaptionButtonClickListener.onClick(v);
             return true;
         }
         return false;
     }
 
+    private boolean pointInView(View v, float x, float y) {
+        return v != null && v.getLeft() <= x && v.getRight() >= x
+                && v.getTop() <= y && v.getBottom() >= y;
+    }
+
     @Override
     public void close() {
         closeDragResizeListener();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
index 0abe8ab..cf1850b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.windowdecor;
 
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_MOVE;
@@ -25,63 +26,86 @@
 import android.view.MotionEvent;
 
 /**
- * A detector for touch inputs that differentiates between drag and click inputs.
+ * A detector for touch inputs that differentiates between drag and click inputs. It receives a flow
+ * of {@link MotionEvent} and generates a new flow of motion events with slop in consideration to
+ * the event handler. In particular, it always passes down, up and cancel events. It'll pass move
+ * events only when there is at least one move event that's beyond the slop threshold. For the
+ * purpose of convenience it also passes all events of other actions.
+ *
  * All touch events must be passed through this class to track a drag event.
  */
-public class DragDetector {
+class DragDetector {
+    private final MotionEventHandler mEventHandler;
+
+    private final PointF mInputDownPoint = new PointF();
     private int mTouchSlop;
-    private PointF mInputDownPoint;
     private boolean mIsDragEvent;
     private int mDragPointerId;
-    public DragDetector(int touchSlop) {
-        mTouchSlop = touchSlop;
-        mInputDownPoint = new PointF();
-        mIsDragEvent = false;
-        mDragPointerId = -1;
+
+    private boolean mResultOfDownAction;
+
+    DragDetector(MotionEventHandler eventHandler) {
+        resetState();
+        mEventHandler = eventHandler;
     }
 
     /**
-     * Determine if {@link MotionEvent} is part of a drag event.
-     * @return {@code true} if this is a drag event, {@code false} if not
-     */
-    public boolean detectDragEvent(MotionEvent ev) {
-        switch (ev.getAction()) {
+     * The receiver of the {@link MotionEvent} flow.
+     *
+     * @return the result returned by {@link #mEventHandler}, or the result when
+     * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
+    */
+    boolean onMotionEvent(MotionEvent ev) {
+        switch (ev.getActionMasked()) {
             case ACTION_DOWN: {
+                // Only touch screens generate noisy moves.
+                mIsDragEvent = (ev.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN;
                 mDragPointerId = ev.getPointerId(0);
                 float rawX = ev.getRawX(0);
                 float rawY = ev.getRawY(0);
                 mInputDownPoint.set(rawX, rawY);
-                return false;
+                mResultOfDownAction = mEventHandler.handleMotionEvent(ev);
+                return mResultOfDownAction;
             }
             case ACTION_MOVE: {
                 if (!mIsDragEvent) {
                     int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
                     float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
                     float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
-                    if (Math.hypot(dx, dy) > mTouchSlop) {
-                        mIsDragEvent = true;
-                    }
+                    mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop;
                 }
-                return mIsDragEvent;
+                if (mIsDragEvent) {
+                    return mEventHandler.handleMotionEvent(ev);
+                } else {
+                    return mResultOfDownAction;
+                }
             }
-            case ACTION_UP: {
-                boolean result = mIsDragEvent;
-                mIsDragEvent = false;
-                mInputDownPoint.set(0, 0);
-                mDragPointerId = -1;
-                return result;
-            }
+            case ACTION_UP:
             case ACTION_CANCEL: {
-                mIsDragEvent = false;
-                mInputDownPoint.set(0, 0);
-                mDragPointerId = -1;
-                return false;
+                resetState();
+                return mEventHandler.handleMotionEvent(ev);
             }
+            default:
+                return mEventHandler.handleMotionEvent(ev);
         }
+    }
+
+    void setTouchSlop(int touchSlop) {
+        mTouchSlop = touchSlop;
+    }
+
+    boolean isDragEvent() {
         return mIsDragEvent;
     }
 
-    public void setTouchSlop(int touchSlop) {
-        mTouchSlop = touchSlop;
+    private void resetState() {
+        mIsDragEvent = false;
+        mInputDownPoint.set(0, 0);
+        mDragPointerId = -1;
+        mResultOfDownAction = false;
+    }
+
+    interface MotionEventHandler {
+        boolean handleMotionEvent(MotionEvent ev);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
similarity index 76%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
index ee160a1..0191c60 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
@@ -19,28 +19,28 @@
 /**
  * Callback called when receiving drag-resize or drag-move related input events.
  */
-public interface DragResizeCallback {
+public interface DragPositioningCallback {
     /**
-     * Called when a drag resize starts.
+     * Called when a drag-resize or drag-move starts.
      *
      * @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use
      *                 {@code 0} to indicate it's a move
-     * @param x x coordinate in window decoration coordinate system where the drag resize starts
-     * @param y y coordinate in window decoration coordinate system where the drag resize starts
+     * @param x x coordinate in window decoration coordinate system where the drag starts
+     * @param y y coordinate in window decoration coordinate system where the drag starts
      */
-    void onDragResizeStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
+    void onDragPositioningStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
 
     /**
-     * Called when the pointer moves during a drag resize.
+     * Called when the pointer moves during a drag-resize or drag-move.
      * @param x x coordinate in window decoration coordinate system of the new pointer location
      * @param y y coordinate in window decoration coordinate system of the new pointer location
      */
-    void onDragResizeMove(float x, float y);
+    void onDragPositioningMove(float x, float y);
 
     /**
-     * Called when a drag resize stops.
+     * Called when a drag-resize or drag-move stops.
      * @param x x coordinate in window decoration coordinate system where the drag resize stops
      * @param y y coordinate in window decoration coordinate system where the drag resize stops
      */
-    void onDragResizeEnd(float x, float y);
+    void onDragPositioningEnd(float x, float y);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index d3f1332..81c4176 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -48,7 +48,6 @@
  * Task edges are for resizing with a mouse.
  * Task corners are for resizing with touch input.
  */
-// TODO(b/251270585): investigate how to pass taps in corners to the tasks
 class DragResizeInputListener implements AutoCloseable {
     private static final String TAG = "DragResizeInputListener";
 
@@ -63,7 +62,7 @@
     private final SurfaceControl mDecorationSurface;
     private final InputChannel mInputChannel;
     private final TaskResizeInputEventReceiver mInputEventReceiver;
-    private final com.android.wm.shell.windowdecor.DragResizeCallback mCallback;
+    private final DragPositioningCallback mCallback;
 
     private int mWidth;
     private int mHeight;
@@ -84,7 +83,7 @@
             Choreographer choreographer,
             int displayId,
             SurfaceControl decorationSurface,
-            DragResizeCallback callback) {
+            DragPositioningCallback callback) {
         mInputManager = context.getSystemService(InputManager.class);
         mHandler = handler;
         mChoreographer = choreographer;
@@ -115,7 +114,8 @@
         mInputEventReceiver = new TaskResizeInputEventReceiver(
                 mInputChannel, mHandler, mChoreographer);
         mCallback = callback;
-        mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
+        mDragDetector = new DragDetector(mInputEventReceiver);
+        mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
     }
 
     /**
@@ -215,6 +215,7 @@
 
     @Override
     public void close() {
+        mInputEventReceiver.dispose();
         mInputChannel.dispose();
         try {
             mWindowSession.remove(mFakeWindow);
@@ -223,12 +224,12 @@
         }
     }
 
-    private class TaskResizeInputEventReceiver extends InputEventReceiver {
+    private class TaskResizeInputEventReceiver extends InputEventReceiver
+            implements DragDetector.MotionEventHandler {
         private final Choreographer mChoreographer;
         private final Runnable mConsumeBatchEventRunnable;
         private boolean mConsumeBatchEventScheduled;
         private boolean mShouldHandleEvents;
-        private boolean mDragging;
 
         private TaskResizeInputEventReceiver(
                 InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -270,15 +271,15 @@
             if (!(inputEvent instanceof MotionEvent)) {
                 return false;
             }
+            return mDragDetector.onMotionEvent((MotionEvent) inputEvent);
+        }
 
-            MotionEvent e = (MotionEvent) inputEvent;
+        @Override
+        public boolean handleMotionEvent(MotionEvent e) {
             boolean result = false;
             // Check if this is a touch event vs mouse event.
             // Touch events are tracked in four corners. Other events are tracked in resize edges.
             boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
-            if (isTouch) {
-                mDragging = mDragDetector.detectDragEvent(e);
-            }
             switch (e.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN: {
                     float x = e.getX(0);
@@ -293,7 +294,7 @@
                         float rawX = e.getRawX(0);
                         float rawY = e.getRawY(0);
                         int ctrlType = calculateCtrlType(isTouch, x, y);
-                        mCallback.onDragResizeStart(ctrlType, rawX, rawY);
+                        mCallback.onDragPositioningStart(ctrlType, rawX, rawY);
                         result = true;
                     }
                     break;
@@ -305,24 +306,17 @@
                     int dragPointerIndex = e.findPointerIndex(mDragPointerId);
                     float rawX = e.getRawX(dragPointerIndex);
                     float rawY = e.getRawY(dragPointerIndex);
-                    if (!isTouch) {
-                        // For all other types allow immediate dragging.
-                        mDragging = true;
-                    }
-                    if (mDragging) {
-                        mCallback.onDragResizeMove(rawX, rawY);
-                        result = true;
-                    }
+                    mCallback.onDragPositioningMove(rawX, rawY);
+                    result = true;
                     break;
                 }
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL: {
-                    if (mShouldHandleEvents && mDragging) {
+                    if (mShouldHandleEvents) {
                         int dragPointerIndex = e.findPointerIndex(mDragPointerId);
-                        mCallback.onDragResizeEnd(
+                        mCallback.onDragPositioningEnd(
                                 e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
                     }
-                    mDragging = false;
                     mShouldHandleEvents = false;
                     mDragPointerId = -1;
                     result = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
new file mode 100644
index 0000000..aea3404
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
@@ -0,0 +1,112 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * Utility class to handle task operations performed on a window decoration.
+ */
+class TaskOperations {
+    private static final String TAG = "TaskOperations";
+
+    private final FreeformTaskTransitionStarter mTransitionStarter;
+    private final Context mContext;
+    private final SyncTransactionQueue mSyncQueue;
+
+    TaskOperations(FreeformTaskTransitionStarter transitionStarter, Context context,
+            SyncTransactionQueue syncQueue) {
+        mTransitionStarter = transitionStarter;
+        mContext = context;
+        mSyncQueue = syncQueue;
+    }
+
+    void injectBackKey() {
+        sendBackEvent(KeyEvent.ACTION_DOWN);
+        sendBackEvent(KeyEvent.ACTION_UP);
+    }
+
+    private void sendBackEvent(int action) {
+        final long when = SystemClock.uptimeMillis();
+        final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK,
+                0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD,
+                0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+                InputDevice.SOURCE_KEYBOARD);
+
+        ev.setDisplayId(mContext.getDisplay().getDisplayId());
+        if (!InputManager.getInstance()
+                .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
+            Log.e(TAG, "Inject input event fail");
+        }
+    }
+
+    void closeTask(WindowContainerToken taskToken) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.removeTask(taskToken);
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitionStarter.startRemoveTransition(wct);
+        } else {
+            mSyncQueue.queue(wct);
+        }
+    }
+
+    void minimizeTask(WindowContainerToken taskToken) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.reorder(taskToken, false);
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitionStarter.startMinimizedModeTransition(wct);
+        } else {
+            mSyncQueue.queue(wct);
+        }
+    }
+
+    void maximizeTask(RunningTaskInfo taskInfo) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
+                ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM;
+        int displayWindowingMode =
+                taskInfo.configuration.windowConfiguration.getDisplayWindowingMode();
+        wct.setWindowingMode(taskInfo.token,
+                targetWindowingMode == displayWindowingMode
+                        ? WINDOWING_MODE_UNDEFINED : targetWindowingMode);
+        if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            wct.setBounds(taskInfo.token, null);
+        }
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct);
+        } else {
+            mSyncQueue.queue(wct);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
index a49a300..a3d364a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
@@ -19,11 +19,13 @@
 import android.annotation.IntDef;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.util.DisplayMetrics;
 import android.window.WindowContainerTransaction;
 
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
 
-class TaskPositioner implements DragResizeCallback {
+class TaskPositioner implements DragPositioningCallback {
 
     @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
     @interface CtrlType {}
@@ -35,92 +37,138 @@
     static final int CTRL_TYPE_BOTTOM = 8;
 
     private final ShellTaskOrganizer mTaskOrganizer;
+    private final DisplayController mDisplayController;
     private final WindowDecoration mWindowDecoration;
 
     private final Rect mTaskBoundsAtDragStart = new Rect();
-    private final PointF mResizeStartPoint = new PointF();
-    private final Rect mResizeTaskBounds = new Rect();
-    // Whether the |dragResizing| hint should be sent with the next bounds change WCT.
-    // Used to optimized fluid resizing of freeform tasks.
-    private boolean mPendingDragResizeHint = false;
+    private final PointF mRepositionStartPoint = new PointF();
+    private final Rect mRepositionTaskBounds = new Rect();
+    private boolean mHasMoved = false;
 
     private int mCtrlType;
     private DragStartListener mDragStartListener;
 
     TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
-            DragStartListener dragStartListener) {
+            DisplayController displayController) {
+        this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {});
+    }
+
+    TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
+            DisplayController displayController, DragStartListener dragStartListener) {
         mTaskOrganizer = taskOrganizer;
         mWindowDecoration = windowDecoration;
+        mDisplayController = displayController;
         mDragStartListener = dragStartListener;
     }
 
     @Override
-    public void onDragResizeStart(int ctrlType, float x, float y) {
-        if (ctrlType != CTRL_TYPE_UNDEFINED) {
-            // The task is being resized, send the |dragResizing| hint to core with the first
-            // bounds-change wct.
-            mPendingDragResizeHint = true;
-        }
+    public void onDragPositioningStart(int ctrlType, float x, float y) {
+        mHasMoved = false;
 
         mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
         mCtrlType = ctrlType;
 
         mTaskBoundsAtDragStart.set(
                 mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
-        mResizeStartPoint.set(x, y);
+        mRepositionStartPoint.set(x, y);
     }
 
     @Override
-    public void onDragResizeMove(float x, float y) {
+    public void onDragPositioningMove(float x, float y) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (changeBounds(wct, x, y)) {
-            if (mPendingDragResizeHint) {
+            // The task is being resized, send the |dragResizing| hint to core with the first
+            // bounds-change wct.
+            if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) {
                 // This is the first bounds change since drag resize operation started.
                 wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
-                mPendingDragResizeHint = false;
             }
             mTaskOrganizer.applyTransaction(wct);
+            mHasMoved = true;
         }
     }
 
     @Override
-    public void onDragResizeEnd(float x, float y) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-        wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
-        changeBounds(wct, x, y);
-        mTaskOrganizer.applyTransaction(wct);
+    public void onDragPositioningEnd(float x, float y) {
+        // |mHasMoved| being false means there is no real change to the task bounds in WM core, so
+        // we don't need a WCT to finish it.
+        if (mHasMoved) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
+            changeBounds(wct, x, y);
+            mTaskOrganizer.applyTransaction(wct);
+        }
 
-        mCtrlType = 0;
+        mCtrlType = CTRL_TYPE_UNDEFINED;
         mTaskBoundsAtDragStart.setEmpty();
-        mResizeStartPoint.set(0, 0);
-        mPendingDragResizeHint = false;
+        mRepositionStartPoint.set(0, 0);
+        mHasMoved = false;
     }
 
     private boolean changeBounds(WindowContainerTransaction wct, float x, float y) {
-        float deltaX = x - mResizeStartPoint.x;
-        mResizeTaskBounds.set(mTaskBoundsAtDragStart);
+        // |mRepositionTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not
+        // true, we can compare it against |mTaskBoundsAtDragStart|.
+        final int oldLeft = mHasMoved ? mRepositionTaskBounds.left : mTaskBoundsAtDragStart.left;
+        final int oldTop = mHasMoved ? mRepositionTaskBounds.top : mTaskBoundsAtDragStart.top;
+        final int oldRight = mHasMoved ? mRepositionTaskBounds.right : mTaskBoundsAtDragStart.right;
+        final int oldBottom =
+                mHasMoved ? mRepositionTaskBounds.bottom : mTaskBoundsAtDragStart.bottom;
+
+        final float deltaX = x - mRepositionStartPoint.x;
+        final float deltaY = y - mRepositionStartPoint.y;
+        mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
         if ((mCtrlType & CTRL_TYPE_LEFT) != 0) {
-            mResizeTaskBounds.left += deltaX;
+            mRepositionTaskBounds.left += deltaX;
         }
         if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) {
-            mResizeTaskBounds.right += deltaX;
+            mRepositionTaskBounds.right += deltaX;
         }
-        float deltaY = y - mResizeStartPoint.y;
         if ((mCtrlType & CTRL_TYPE_TOP) != 0) {
-            mResizeTaskBounds.top += deltaY;
+            mRepositionTaskBounds.top += deltaY;
         }
         if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) {
-            mResizeTaskBounds.bottom += deltaY;
+            mRepositionTaskBounds.bottom += deltaY;
         }
-        if (mCtrlType == 0) {
-            mResizeTaskBounds.offset((int) deltaX, (int) deltaY);
+        if (mCtrlType == CTRL_TYPE_UNDEFINED) {
+            mRepositionTaskBounds.offset((int) deltaX, (int) deltaY);
         }
 
-        if (!mResizeTaskBounds.isEmpty()) {
-            wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds);
-            return true;
+        // If width or height are negative or less than the minimum width or height, revert the
+        // respective bounds to use previous bound dimensions.
+        if (mRepositionTaskBounds.width() < getMinWidth()) {
+            mRepositionTaskBounds.right = oldRight;
+            mRepositionTaskBounds.left = oldLeft;
         }
-        return false;
+        if (mRepositionTaskBounds.height() < getMinHeight()) {
+            mRepositionTaskBounds.top = oldTop;
+            mRepositionTaskBounds.bottom = oldBottom;
+        }
+        // If there are no changes to the bounds after checking new bounds against minimum width
+        // and height, do not set bounds and return false
+        if (oldLeft == mRepositionTaskBounds.left && oldTop == mRepositionTaskBounds.top
+                && oldRight == mRepositionTaskBounds.right
+                && oldBottom == mRepositionTaskBounds.bottom) {
+            return false;
+        }
+
+        wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+        return true;
+    }
+
+    private float getMinWidth() {
+        return mWindowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize()
+                : mWindowDecoration.mTaskInfo.minWidth;
+    }
+
+    private float getMinHeight() {
+        return mWindowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize()
+                : mWindowDecoration.mTaskInfo.minHeight;
+    }
+
+    private float getDefaultMinSize() {
+        float density =  mDisplayController.getDisplayLayout(mWindowDecoration.mTaskInfo.displayId)
+                .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+        return mWindowDecoration.mTaskInfo.defaultMinSize * density;
     }
 
     interface DragStartListener {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 907977c..3734487 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -29,7 +29,8 @@
  */
 public interface WindowDecorViewModel {
     /**
-     * Sets the transition starter that starts freeform task transitions.
+     * Sets the transition starter that starts freeform task transitions. Only called when
+     * {@link com.android.wm.shell.transition.Transitions#ENABLE_SHELL_TRANSITIONS} is {@code true}.
      *
      * @param transitionStarter the transition starter that starts freeform task transitions
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 7f85988..ae685ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -252,9 +252,7 @@
         }
 
         final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
-        final int captionWidth = params.mCaptionWidthId == Resources.ID_NULL
-                ? taskBounds.width()
-                : loadDimensionPixelSize(resources, params.mCaptionWidthId);
+        final int captionWidth = taskBounds.width();
 
         startT.setPosition(
                         mCaptionContainerSurface,
@@ -393,10 +391,11 @@
      * @param yPos y position of new window
      * @param width width of new window
      * @param height height of new window
+     * @param cropPadding padding to add to window crop to ensure shadows display properly
      * @return
      */
-    AdditionalWindow addWindow(int layoutId, String namePrefix,
-            SurfaceControl.Transaction t, int xPos, int yPos, int width, int height) {
+    AdditionalWindow addWindow(int layoutId, String namePrefix, SurfaceControl.Transaction t,
+            int xPos, int yPos, int width, int height, int cropPadding) {
         final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
         SurfaceControl windowSurfaceControl = builder
                 .setName(namePrefix + " of Task=" + mTaskInfo.taskId)
@@ -407,7 +406,7 @@
 
         t.setPosition(
                 windowSurfaceControl, xPos, yPos)
-                .setWindowCrop(windowSurfaceControl, width, height)
+                .setWindowCrop(windowSurfaceControl, width + cropPadding, height + cropPadding)
                 .show(windowSurfaceControl);
         final WindowManager.LayoutParams lp =
                 new WindowManager.LayoutParams(width, height,
diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
index 8949a75..27d40b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
@@ -17,7 +17,7 @@
 <resources>
     <!-- Resources used in WindowDecorationTests -->
     <dimen name="test_freeform_decor_caption_height">32dp</dimen>
-    <dimen name="test_freeform_decor_caption_width">216dp</dimen>
+    <dimen name="test_freeform_decor_caption_menu_width">216dp</dimen>
     <dimen name="test_window_decor_left_outset">10dp</dimen>
     <dimen name="test_window_decor_top_outset">20dp</dimen>
     <dimen name="test_window_decor_right_outset">30dp</dimen>
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
index ff1d2990..d5bb901 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
@@ -28,9 +28,11 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -82,13 +84,14 @@
     @Mock
     SyncTransactionQueue mSyncQueue;
     @Mock
-    TaskViewTransitions mTaskViewTransitions;
+    Transitions mTransitions;
 
     SurfaceSession mSession;
     SurfaceControl mLeash;
 
     Context mContext;
     TaskView mTaskView;
+    TaskViewTransitions mTaskViewTransitions;
 
     @Before
     public void setUp() {
@@ -118,6 +121,10 @@
             return null;
         }).when(mSyncQueue).runInSync(any());
 
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            doReturn(true).when(mTransitions).isRegistered();
+        }
+        mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions));
         mTaskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue);
         mTaskView.setListener(mExecutor, mViewListener);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 2e328b0..2754496 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -53,6 +53,7 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackNavigationInfo;
 import android.window.IBackNaviAnimationController;
 import android.window.IOnBackInvokedCallback;
@@ -246,10 +247,11 @@
         // Check that back start and progress is dispatched when first move.
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
-        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+        ArgumentCaptor<BackMotionEvent> backEventCaptor =
+                ArgumentCaptor.forClass(BackMotionEvent.class);
         verify(mIOnBackInvokedCallback).onBackStarted(backEventCaptor.capture());
         assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget());
-        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
+        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackMotionEvent.class));
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
@@ -271,17 +273,18 @@
 
         RemoteAnimationTarget animationTarget = createAnimationTarget();
         IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
-        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+        ArgumentCaptor<BackMotionEvent> backEventCaptor =
+                ArgumentCaptor.forClass(BackMotionEvent.class);
         createNavigationInfo(animationTarget, null, null,
                 BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false);
 
         triggerBackGesture();
 
-        verify(appCallback, never()).onBackStarted(any(BackEvent.class));
+        verify(appCallback, never()).onBackStarted(any(BackMotionEvent.class));
         verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
         verify(appCallback, times(1)).onBackInvoked();
 
-        verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
+        verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackMotionEvent.class));
         verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
         verify(mIOnBackInvokedCallback, never()).onBackInvoked();
     }
@@ -314,7 +317,7 @@
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
     }
 
     @Test
@@ -333,7 +336,7 @@
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
     }
 
 
@@ -349,7 +352,7 @@
         // Check that back start and progress is dispatched when first move.
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
index 3aefc3f..ba9c159 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -38,7 +39,7 @@
     @Test
     public void generatesProgress_onStart() {
         mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
-        BackEvent event = mTouchTracker.createStartEvent(null);
+        BackMotionEvent event = mTouchTracker.createStartEvent(null);
         assertEquals(event.getProgress(), 0f, 0f);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index e6711ac..8b025cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.bubbles;
 
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -32,6 +34,7 @@
 
 import android.app.Notification;
 import android.app.PendingIntent;
+import android.content.Intent;
 import android.content.LocusId;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
@@ -94,6 +97,7 @@
     private Bubble mBubbleInterruptive;
     private Bubble mBubbleDismissed;
     private Bubble mBubbleLocusId;
+    private Bubble mAppBubble;
 
     private BubbleData mBubbleData;
     private TestableBubblePositioner mPositioner;
@@ -178,6 +182,11 @@
                 mBubbleMetadataFlagListener,
                 mPendingIntentCanceledListener,
                 mMainExecutor);
+
+        Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
+        appBubbleIntent.setPackage(mContext.getPackageName());
+        mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mMainExecutor);
+
         mPositioner = new TestableBubblePositioner(mContext,
                 mock(WindowManager.class));
         mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner,
@@ -1089,6 +1098,18 @@
         assertOverflowChangedTo(ImmutableList.of());
     }
 
+    @Test
+    public void test_removeAppBubble_skipsOverflow() {
+        mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/,
+                false /* showInShade */);
+        assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble);
+
+        mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE);
+
+        assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull();
+        assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull();
+    }
+
     private void verifyUpdateReceived() {
         verify(mListener).applyUpdate(mUpdateCaptor.capture());
         reset(mListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
index 1636c5f..0a31338 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -21,7 +21,6 @@
 import android.util.SparseArray
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.bubbles.storage.BubbleXmlHelperTest.Companion.sparseArraysEqual
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertNotNull
 import junit.framework.Assert.assertTrue
@@ -36,7 +35,8 @@
 
     // user, package, shortcut, notification key, height, res-height, title, taskId, locusId
     private val user0Bubbles = listOf(
-            BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null),
+            BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null,
+                    true),
             BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
                     null),
             BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
@@ -44,7 +44,8 @@
     )
 
     private val user1Bubbles = listOf(
-            BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null),
+            BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null,
+                    true),
             BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
                     null),
             BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
@@ -76,6 +77,6 @@
         assertEquals(actual.size(), 0)
 
         repository.persistsToDisk(bubbles)
-        assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk()))
+        assertTrue(bubbles.contentEquals(repository.readFromDisk()))
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
index 4ab9f87..3bfbcd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
@@ -34,7 +34,8 @@
 class BubbleXmlHelperTest : ShellTestCase() {
 
     private val user0Bubbles = listOf(
-            BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1),
+            BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1,
+                    isDismissable = true),
             BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
                     null),
             BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
@@ -42,7 +43,8 @@
     )
 
     private val user1Bubbles = listOf(
-            BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3),
+            BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3,
+                    isDismissable = true),
             BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
                     null),
             BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
@@ -51,28 +53,6 @@
 
     private val bubbles = SparseArray<List<BubbleEntity>>()
 
-    // Checks that the contents of the two sparse arrays are the same.
-    companion object {
-        fun sparseArraysEqual(
-            one: SparseArray<List<BubbleEntity>>?,
-            two: SparseArray<List<BubbleEntity>>?
-        ): Boolean {
-            if (one == null && two == null) return true
-            if ((one == null) != (two == null)) return false
-            if (one!!.size() != two!!.size()) return false
-            for (i in 0 until one.size()) {
-                val k1 = one.keyAt(i)
-                val v1 = one.valueAt(i)
-                val k2 = two.keyAt(i)
-                val v2 = two.valueAt(i)
-                if (k1 != k2 && v1 != v2) {
-                    return false
-                }
-            }
-            return true
-        }
-    }
-
     @Before
     fun setup() {
         bubbles.put(0, user0Bubbles)
@@ -83,14 +63,14 @@
     fun testWriteXml() {
         val expectedEntries = """
 <bs uid="0">
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" />
-<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" />
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" d="true" />
+<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" d="false" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" d="false" />
 </bs>
 <bs uid="1">
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" />
-<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" />
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" d="true" />
+<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" d="false" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" d="false" />
 </bs>
         """.trimIndent()
         ByteArrayOutputStream().use {
@@ -107,19 +87,19 @@
 <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
 <bs v="2">
 <bs uid="0">
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" />
-<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" />
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" d="true" />
+<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" d="false" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" d="false" />
 </bs>
 <bs uid="1">
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" />
-<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" />
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" d="true" />
+<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" d="false" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" d="false" />
 </bs>
 </bs>
         """.trimIndent()
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
-        assertTrue("failed parsing bubbles from xml\n$src", sparseArraysEqual(bubbles, actual))
+        assertTrue("failed parsing bubbles from xml\n$src", bubbles.contentEquals(actual))
     }
 
     // V0 -> V1 happened prior to release / during dogfood so nothing is saved
@@ -161,8 +141,7 @@
 </bs>
         """.trimIndent()
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
-        assertTrue("failed parsing bubbles from xml\n$src",
-                sparseArraysEqual(expectedBubbles, actual))
+        assertTrue("failed parsing bubbles from xml\n$src", expectedBubbles.contentEquals(actual))
     }
 
     /**
@@ -187,7 +166,7 @@
         """.trimIndent()
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
         assertTrue("failed parsing bubbles from xml\n$src",
-                sparseArraysEqual(expectedBubbles, actual))
+                expectedBubbles.contentEquals(actual))
     }
 
     @Test
@@ -210,6 +189,6 @@
         )
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
         assertTrue("failed parsing bubbles from xml\n$src",
-                sparseArraysEqual(expectedBubbles, actual))
+                expectedBubbles.contentEquals(actual))
     }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java
new file mode 100644
index 0000000..f8ee300
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.wm.shell.common;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DevicePostureControllerTest {
+    @Mock
+    private Context mContext;
+
+    @Mock
+    private ShellInit mShellInit;
+
+    @Mock
+    private ShellExecutor mMainExecutor;
+
+    @Captor
+    private ArgumentCaptor<Integer> mDevicePostureCaptor;
+
+    @Mock
+    private DevicePostureController.OnDevicePostureChangedListener mOnDevicePostureChangedListener;
+
+    private DevicePostureController mDevicePostureController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mDevicePostureController = new DevicePostureController(mContext, mShellInit, mMainExecutor);
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), eq(mDevicePostureController));
+    }
+
+    @Test
+    public void registerOnDevicePostureChangedListener_callbackCurrentPosture() {
+        mDevicePostureController.registerOnDevicePostureChangedListener(
+                mOnDevicePostureChangedListener);
+        verify(mOnDevicePostureChangedListener, times(1))
+                .onDevicePostureChanged(anyInt());
+    }
+
+    @Test
+    public void onDevicePostureChanged_differentPosture_callbackListener() {
+        mDevicePostureController.registerOnDevicePostureChangedListener(
+                mOnDevicePostureChangedListener);
+        verify(mOnDevicePostureChangedListener).onDevicePostureChanged(
+                mDevicePostureCaptor.capture());
+        clearInvocations(mOnDevicePostureChangedListener);
+
+        int differentDevicePosture = mDevicePostureCaptor.getValue() + 1;
+        mDevicePostureController.onDevicePostureChanged(differentDevicePosture);
+
+        verify(mOnDevicePostureChangedListener, times(1))
+                .onDevicePostureChanged(differentDevicePosture);
+    }
+
+    @Test
+    public void onDevicePostureChanged_samePosture_doesNotCallbackListener() {
+        mDevicePostureController.registerOnDevicePostureChangedListener(
+                mOnDevicePostureChangedListener);
+        verify(mOnDevicePostureChangedListener).onDevicePostureChanged(
+                mDevicePostureCaptor.capture());
+        clearInvocations(mOnDevicePostureChangedListener);
+
+        int sameDevicePosture = mDevicePostureCaptor.getValue();
+        mDevicePostureController.onDevicePostureChanged(sameDevicePosture);
+
+        verifyZeroInteractions(mOnDevicePostureChangedListener);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 2fc0914..98de584 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -94,7 +94,10 @@
     private @Mock Lazy<Transitions> mMockTransitionsLazy;
     private @Mock CompatUIWindowManager mMockCompatLayout;
     private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
+    private @Mock RestartDialogWindowManager mMockRestartDialogLayout;
     private @Mock DockStateReader mDockStateReader;
+    private @Mock CompatUIConfiguration mCompatUIConfiguration;
+    private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler;
 
     @Captor
     ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -112,10 +115,17 @@
         doReturn(TASK_ID).when(mMockLetterboxEduLayout).getTaskId();
         doReturn(true).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
         doReturn(true).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
+
+        doReturn(DISPLAY_ID).when(mMockRestartDialogLayout).getDisplayId();
+        doReturn(TASK_ID).when(mMockRestartDialogLayout).getTaskId();
+        doReturn(true).when(mMockRestartDialogLayout).createLayout(anyBoolean());
+        doReturn(true).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean());
+
         mShellInit = spy(new ShellInit(mMockExecutor));
         mController = new CompatUIController(mContext, mShellInit, mMockShellController,
                 mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
-                mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader) {
+                mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader,
+                mCompatUIConfiguration, mCompatUIShellCommandHandler) {
             @Override
             CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
                     ShellTaskOrganizer.TaskListener taskListener) {
@@ -127,6 +137,12 @@
                     TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
                 return mMockLetterboxEduLayout;
             }
+
+            @Override
+            RestartDialogWindowManager createRestartDialogWindowManager(Context context,
+                    TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
+                return mMockRestartDialogLayout;
+            }
         };
         mShellInit.init();
         spyOn(mController);
@@ -159,6 +175,8 @@
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
+        verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+                eq(mMockTaskListener));
 
         // Verify that the compat controls and letterbox education are updated with new size compat
         // info.
@@ -167,10 +185,12 @@
                 CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
         mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
 
-        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                true);
-        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                true);
+        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ true);
+        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ true);
+        verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ true);
 
         // Verify that compat controls and letterbox education are removed with null task listener.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
@@ -180,12 +200,14 @@
 
         verify(mMockCompatLayout).release();
         verify(mMockLetterboxEduLayout).release();
+        verify(mMockRestartDialogLayout).release();
     }
 
     @Test
     public void testOnCompatInfoChanged_createLayoutReturnsFalse() {
         doReturn(false).when(mMockCompatLayout).createLayout(anyBoolean());
         doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
+        doReturn(false).when(mMockRestartDialogLayout).createLayout(anyBoolean());
 
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
@@ -194,6 +216,8 @@
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
+        verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+                eq(mMockTaskListener));
 
         // Verify that the layout is created again.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
@@ -201,15 +225,19 @@
 
         verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
         verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
+        verify(mMockRestartDialogLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
+        verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+                eq(mMockTaskListener));
     }
 
     @Test
     public void testOnCompatInfoChanged_updateCompatInfoReturnsFalse() {
         doReturn(false).when(mMockCompatLayout).updateCompatInfo(any(), any(), anyBoolean());
         doReturn(false).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
+        doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean());
 
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
@@ -218,24 +246,33 @@
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
+        verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+                eq(mMockTaskListener));
 
-        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
+        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout,
+                mController);
         mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
 
-        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                true);
-        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                true);
+        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ true);
+        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ true);
+        verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ true);
 
         // Verify that the layout is created again.
-        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
+        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout,
+                mController);
         mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
 
         verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
         verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
+        verify(mMockRestartDialogLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
+        verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+                eq(mMockTaskListener));
     }
 
 
@@ -259,6 +296,7 @@
 
         verify(mMockCompatLayout, never()).release();
         verify(mMockLetterboxEduLayout, never()).release();
+        verify(mMockRestartDialogLayout, never()).release();
         verify(mMockDisplayInsetsController, never()).removeInsetsChangedListener(eq(DISPLAY_ID),
                 any());
 
@@ -267,6 +305,7 @@
         verify(mMockDisplayInsetsController).removeInsetsChangedListener(eq(DISPLAY_ID), any());
         verify(mMockCompatLayout).release();
         verify(mMockLetterboxEduLayout).release();
+        verify(mMockRestartDialogLayout).release();
     }
 
     @Test
@@ -278,11 +317,13 @@
 
         verify(mMockCompatLayout, never()).updateDisplayLayout(any());
         verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(any());
+        verify(mMockRestartDialogLayout, never()).updateDisplayLayout(any());
 
         mController.onDisplayConfigurationChanged(DISPLAY_ID, new Configuration());
 
         verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout);
         verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout);
+        verify(mMockRestartDialogLayout).updateDisplayLayout(mMockDisplayLayout);
     }
 
     @Test
@@ -301,12 +342,14 @@
 
         verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout);
         verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout);
+        verify(mMockRestartDialogLayout).updateDisplayLayout(mMockDisplayLayout);
 
         // No update if the insets state is the same.
-        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
+        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout);
         mOnInsetsChangedListenerCaptor.getValue().insetsChanged(new InsetsState(insetsState));
         verify(mMockCompatLayout, never()).updateDisplayLayout(mMockDisplayLayout);
         verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(mMockDisplayLayout);
+        verify(mMockRestartDialogLayout, never()).updateDisplayLayout(mMockDisplayLayout);
     }
 
     @Test
@@ -319,22 +362,26 @@
 
         verify(mMockCompatLayout).updateVisibility(false);
         verify(mMockLetterboxEduLayout).updateVisibility(false);
+        verify(mMockRestartDialogLayout).updateVisibility(false);
 
         // Verify button remains hidden while IME is showing.
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
         mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
 
-        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                false);
-        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                false);
+        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ false);
+        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ false);
+        verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ false);
 
         // Verify button is shown after IME is hidden.
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false);
 
         verify(mMockCompatLayout).updateVisibility(true);
         verify(mMockLetterboxEduLayout).updateVisibility(true);
+        verify(mMockRestartDialogLayout).updateVisibility(true);
     }
 
     @Test
@@ -347,22 +394,26 @@
 
         verify(mMockCompatLayout).updateVisibility(false);
         verify(mMockLetterboxEduLayout).updateVisibility(false);
+        verify(mMockRestartDialogLayout).updateVisibility(false);
 
         // Verify button remains hidden while keyguard is showing.
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
         mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
 
-        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                false);
-        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
-                false);
+        verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ false);
+        verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ false);
+        verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+                /* canShow= */ false);
 
         // Verify button is shown after keyguard becomes not showing.
         mController.onKeyguardVisibilityChanged(false, false, false);
 
         verify(mMockCompatLayout).updateVisibility(true);
         verify(mMockLetterboxEduLayout).updateVisibility(true);
+        verify(mMockRestartDialogLayout).updateVisibility(true);
     }
 
     @Test
@@ -375,20 +426,23 @@
 
         verify(mMockCompatLayout, times(2)).updateVisibility(false);
         verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false);
+        verify(mMockRestartDialogLayout, times(2)).updateVisibility(false);
 
-        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
+        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout);
 
         // Verify button remains hidden after keyguard becomes not showing since IME is showing.
         mController.onKeyguardVisibilityChanged(false, false, false);
 
         verify(mMockCompatLayout).updateVisibility(false);
         verify(mMockLetterboxEduLayout).updateVisibility(false);
+        verify(mMockRestartDialogLayout).updateVisibility(false);
 
         // Verify button is shown after IME is not showing.
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false);
 
         verify(mMockCompatLayout).updateVisibility(true);
         verify(mMockLetterboxEduLayout).updateVisibility(true);
+        verify(mMockRestartDialogLayout).updateVisibility(true);
     }
 
     @Test
@@ -401,20 +455,53 @@
 
         verify(mMockCompatLayout, times(2)).updateVisibility(false);
         verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false);
+        verify(mMockRestartDialogLayout, times(2)).updateVisibility(false);
 
-        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
+        clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout);
 
         // Verify button remains hidden after IME is hidden since keyguard is showing.
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false);
 
         verify(mMockCompatLayout).updateVisibility(false);
         verify(mMockLetterboxEduLayout).updateVisibility(false);
+        verify(mMockRestartDialogLayout).updateVisibility(false);
 
         // Verify button is shown after keyguard becomes not showing.
         mController.onKeyguardVisibilityChanged(false, false, false);
 
         verify(mMockCompatLayout).updateVisibility(true);
         verify(mMockLetterboxEduLayout).updateVisibility(true);
+        verify(mMockRestartDialogLayout).updateVisibility(true);
+    }
+
+    @Test
+    public void testRestartLayoutRecreatedIfNeeded() {
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        doReturn(true).when(mMockRestartDialogLayout)
+                .needsToBeRecreated(any(TaskInfo.class),
+                        any(ShellTaskOrganizer.TaskListener.class));
+
+        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+
+        verify(mMockRestartDialogLayout, times(2))
+                .createLayout(anyBoolean());
+    }
+
+    @Test
+    public void testRestartLayoutNotRecreatedIfNotNeeded() {
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        doReturn(false).when(mMockRestartDialogLayout)
+                .needsToBeRecreated(any(TaskInfo.class),
+                        any(ShellTaskOrganizer.TaskListener.class));
+
+        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+
+        verify(mMockRestartDialogLayout, times(1))
+                .createLayout(anyBoolean());
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 7d3e718..5f294d5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -31,6 +31,7 @@
 import android.app.TaskInfo;
 import android.app.TaskInfo.CameraCompatControlState;
 import android.testing.AndroidTestingRunner;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.SurfaceControlViewHost;
 import android.widget.ImageButton;
@@ -45,12 +46,17 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
 
+import junit.framework.Assert;
+
 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.function.Consumer;
+
 /**
  * Tests for {@link CompatUILayout}.
  *
@@ -65,20 +71,22 @@
 
     @Mock private SyncTransactionQueue mSyncTransactionQueue;
     @Mock private CompatUIController.CompatUICallback mCallback;
+    @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
     @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
     @Mock private SurfaceControlViewHost mViewHost;
+    @Mock private CompatUIConfiguration mCompatUIConfiguration;
 
     private CompatUIWindowManager mWindowManager;
     private CompatUILayout mLayout;
+    private TaskInfo mTaskInfo;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
-        mWindowManager = new CompatUIWindowManager(mContext,
-                createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN),
-                mSyncTransactionQueue, mCallback, mTaskListener,
-                new DisplayLayout(), new CompatUIHintsState());
+        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
+                mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+                mCompatUIConfiguration, mOnRestartButtonClicked);
 
         mLayout = (CompatUILayout)
                 LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, null);
@@ -95,8 +103,15 @@
         final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button);
         button.performClick();
 
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> restartCaptor =
+                ArgumentCaptor.forClass(Pair.class);
+
         verify(mWindowManager).onRestartButtonClicked();
-        verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
+        verify(mOnRestartButtonClicked).accept(restartCaptor.capture());
+        final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = restartCaptor.getValue();
+        Assert.assertEquals(mTaskInfo, result.first);
+        Assert.assertEquals(mTaskListener, result.second);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index e79b803..0c5edc3f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -28,6 +28,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -38,6 +39,7 @@
 import android.app.TaskInfo;
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
+import android.util.Pair;
 import android.view.DisplayInfo;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -53,12 +55,17 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
 
+import junit.framework.Assert;
+
 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.function.Consumer;
+
 /**
  * Tests for {@link CompatUIWindowManager}.
  *
@@ -73,20 +80,22 @@
 
     @Mock private SyncTransactionQueue mSyncTransactionQueue;
     @Mock private CompatUIController.CompatUICallback mCallback;
+    @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
     @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
     @Mock private CompatUILayout mLayout;
     @Mock private SurfaceControlViewHost mViewHost;
+    @Mock private CompatUIConfiguration mCompatUIConfiguration;
 
     private CompatUIWindowManager mWindowManager;
+    private TaskInfo mTaskInfo;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
-        mWindowManager = new CompatUIWindowManager(mContext,
-                createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN),
-                mSyncTransactionQueue, mCallback, mTaskListener,
-                new DisplayLayout(), new CompatUIHintsState());
+        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
+                mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+                mCompatUIConfiguration, mOnRestartButtonClicked);
 
         spyOn(mWindowManager);
         doReturn(mLayout).when(mWindowManager).inflateLayout();
@@ -351,14 +360,14 @@
         mWindowManager.updateVisibility(/* canShow= */ false);
 
         verify(mWindowManager, never()).createLayout(anyBoolean());
-        verify(mLayout).setVisibility(View.GONE);
+        verify(mLayout, atLeastOnce()).setVisibility(View.GONE);
 
         // Show button.
         doReturn(View.GONE).when(mLayout).getVisibility();
         mWindowManager.updateVisibility(/* canShow= */ true);
 
         verify(mWindowManager, never()).createLayout(anyBoolean());
-        verify(mLayout).setVisibility(View.VISIBLE);
+        verify(mLayout, atLeastOnce()).setVisibility(View.VISIBLE);
     }
 
     @Test
@@ -404,7 +413,14 @@
     public void testOnRestartButtonClicked() {
         mWindowManager.onRestartButtonClicked();
 
-        verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> restartCaptor =
+                ArgumentCaptor.forClass(Pair.class);
+
+        verify(mOnRestartButtonClicked).accept(restartCaptor.capture());
+        final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = restartCaptor.getValue();
+        Assert.assertEquals(mTaskInfo, result.first);
+        Assert.assertEquals(mTaskListener, result.second);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
new file mode 100644
index 0000000..e2dcdb0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link RestartDialogLayout}.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:RestartDialogLayoutTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class RestartDialogLayoutTest extends ShellTestCase  {
+
+    @Mock private Runnable mDismissCallback;
+    @Mock private Consumer<Boolean> mRestartCallback;
+
+    private RestartDialogLayout mLayout;
+    private View mDismissButton;
+    private View mRestartButton;
+    private View mDialogContainer;
+    private CheckBox mDontRepeatCheckBox;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mLayout = (RestartDialogLayout)
+                LayoutInflater.from(mContext).inflate(R.layout.letterbox_restart_dialog_layout,
+                        null);
+        mDismissButton = mLayout.findViewById(R.id.letterbox_restart_dialog_dismiss_button);
+        mRestartButton = mLayout.findViewById(R.id.letterbox_restart_dialog_restart_button);
+        mDialogContainer = mLayout.findViewById(R.id.letterbox_restart_dialog_container);
+        mDontRepeatCheckBox = mLayout.findViewById(R.id.letterbox_restart_dialog_checkbox);
+        mLayout.setDismissOnClickListener(mDismissCallback);
+        mLayout.setRestartOnClickListener(mRestartCallback);
+    }
+
+    @Test
+    public void testOnFinishInflate() {
+        assertEquals(mLayout.getDialogContainerView(),
+                mLayout.findViewById(R.id.letterbox_restart_dialog_container));
+        assertEquals(mLayout.getDialogTitle(),
+                mLayout.findViewById(R.id.letterbox_restart_dialog_title));
+        assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground());
+        assertEquals(mLayout.getBackground().getAlpha(), 0);
+    }
+
+    @Test
+    public void testOnDismissButtonClicked() {
+        assertTrue(mDismissButton.performClick());
+
+        verify(mDismissCallback).run();
+    }
+
+    @Test
+    public void testOnRestartButtonClickedWithoutCheckbox() {
+        mDontRepeatCheckBox.setChecked(false);
+        assertTrue(mRestartButton.performClick());
+
+        verify(mRestartCallback).accept(false);
+    }
+
+    @Test
+    public void testOnRestartButtonClickedWithCheckbox() {
+        mDontRepeatCheckBox.setChecked(true);
+        assertTrue(mRestartButton.performClick());
+
+        verify(mRestartCallback).accept(true);
+    }
+
+    @Test
+    public void testOnBackgroundClickedDoesntDismiss() {
+        assertFalse(mLayout.performClick());
+
+        verify(mDismissCallback, never()).run();
+    }
+
+    @Test
+    public void testOnDialogContainerClicked() {
+        assertTrue(mDialogContainer.performClick());
+
+        verify(mDismissCallback, never()).run();
+        verify(mRestartCallback, never()).accept(anyBoolean());
+    }
+
+    @Test
+    public void testSetDismissOnClickListenerNull() {
+        mLayout.setDismissOnClickListener(null);
+
+        assertFalse(mDismissButton.performClick());
+        assertFalse(mLayout.performClick());
+        assertTrue(mDialogContainer.performClick());
+
+        verify(mDismissCallback, never()).run();
+    }
+
+    @Test
+    public void testSetRestartOnClickListenerNull() {
+        mLayout.setRestartOnClickListener(null);
+
+        assertFalse(mRestartButton.performClick());
+        assertFalse(mLayout.performClick());
+        assertTrue(mDialogContainer.performClick());
+
+        verify(mRestartCallback, never()).accept(anyBoolean());
+    }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index 08af3d3..43f8f7b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
@@ -279,7 +280,7 @@
     }
 
     @Test
-    public void testShowDesktopApps_appsAlreadyVisible_doesNothing() {
+    public void testShowDesktopApps_appsAlreadyVisible_bringsToFront() {
         final RunningTaskInfo task1 = createFreeformTask();
         mDesktopModeTaskRepository.addActiveTask(task1.taskId);
         mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
@@ -294,8 +295,17 @@
         mController.showDesktopApps();
 
         final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
-        // No reordering needed.
-        assertThat(wct.getHierarchyOps()).isEmpty();
+        // Check wct has reorder calls
+        assertThat(wct.getHierarchyOps()).hasSize(2);
+        // Task 1 appeared first, must be first reorder to top.
+        HierarchyOp op1 = wct.getHierarchyOps().get(0);
+        assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder());
+
+        // Task 2 appeared last, must be last reorder to top.
+        HierarchyOp op2 = wct.getHierarchyOps().get(1);
+        assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder());
     }
 
     @Test
@@ -325,6 +335,41 @@
     }
 
     @Test
+    public void testGetVisibleTaskCount_noTasks_returnsZero() {
+        assertThat(mController.getVisibleTaskCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+        RunningTaskInfo task1 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+
+        RunningTaskInfo task2 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+
+        assertThat(mController.getVisibleTaskCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void testGetVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
+        RunningTaskInfo task1 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+
+        RunningTaskInfo task2 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, false /* visible */);
+
+        assertThat(mController.getVisibleTaskCount()).isEqualTo(1);
+    }
+
+    @Test
     public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() {
         when(DesktopModeStatus.isActive(any())).thenReturn(false);
         WindowContainerTransaction wct = mController.handleRequest(
@@ -402,7 +447,7 @@
         final ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
                 WindowContainerTransaction.class);
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), arg.capture(), any());
+            verify(mTransitions).startTransition(eq(TRANSIT_NONE), arg.capture(), any());
         } else {
             verify(mShellTaskOrganizer).applyTransaction(arg.capture());
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 1e43a59..45cb3a0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -141,6 +141,36 @@
     }
 
     @Test
+    fun getVisibleTaskCount() {
+        // No tasks, count is 0
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+
+        // New task increments count to 1
+        repo.updateVisibleFreeformTasks(taskId = 1, visible = true)
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+
+        // Visibility update to same task does not increase count
+        repo.updateVisibleFreeformTasks(taskId = 1, visible = true)
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+
+        // Second task visible increments count
+        repo.updateVisibleFreeformTasks(taskId = 2, visible = true)
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(2)
+
+        // Hiding a task decrements count
+        repo.updateVisibleFreeformTasks(taskId = 1, visible = false)
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+
+        // Hiding all tasks leaves count at 0
+        repo.updateVisibleFreeformTasks(taskId = 2, visible = false)
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+
+        // Hiding a not existing task, count remains at 0
+        repo.updateVisibleFreeformTasks(taskId = 999, visible = false)
+        assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+    }
+
+    @Test
     fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() {
         repo.addOrMoveFreeformTaskToTop(5)
         repo.addOrMoveFreeformTaskToTop(6)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 9a92879..95e78a8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -26,6 +26,8 @@
 import android.os.Binder
 import android.testing.AndroidTestingRunner
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionRequestInfo
@@ -55,6 +57,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mock
 import org.mockito.Mockito
@@ -141,7 +144,7 @@
 
         controller.showDesktopApps()
 
-        val wct = getLatestWct()
+        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
         assertThat(wct.hierarchyOps).hasSize(3)
         // Expect order to be from bottom: home, task1, task2
         wct.assertReorderAt(index = 0, homeTask)
@@ -150,8 +153,8 @@
     }
 
     @Test
-    fun showDesktopApps_appsAlreadyVisible_doesNothing() {
-        setUpHomeTask()
+    fun showDesktopApps_appsAlreadyVisible_bringsToFront() {
+        val homeTask = setUpHomeTask()
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
         markTaskVisible(task1)
@@ -159,7 +162,12 @@
 
         controller.showDesktopApps()
 
-        verifyWCTNotExecuted()
+        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: home, task1, task2
+        wct.assertReorderAt(index = 0, homeTask)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
     }
 
     @Test
@@ -172,7 +180,7 @@
 
         controller.showDesktopApps()
 
-        val wct = getLatestWct()
+        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
         assertThat(wct.hierarchyOps).hasSize(3)
         // Expect order to be from bottom: home, task1, task2
         wct.assertReorderAt(index = 0, homeTask)
@@ -186,16 +194,37 @@
 
         controller.showDesktopApps()
 
-        val wct = getLatestWct()
+        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
         assertThat(wct.hierarchyOps).hasSize(1)
         wct.assertReorderAt(index = 0, homeTask)
     }
 
     @Test
+    fun getVisibleTaskCount_noTasks_returnsZero() {
+        assertThat(controller.getVisibleTaskCount()).isEqualTo(0)
+    }
+
+    @Test
+    fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+        setUpHomeTask()
+        setUpFreeformTask().also(::markTaskVisible)
+        setUpFreeformTask().also(::markTaskVisible)
+        assertThat(controller.getVisibleTaskCount()).isEqualTo(2)
+    }
+
+    @Test
+    fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
+        setUpHomeTask()
+        setUpFreeformTask().also(::markTaskVisible)
+        setUpFreeformTask().also(::markTaskHidden)
+        assertThat(controller.getVisibleTaskCount()).isEqualTo(1)
+    }
+
+    @Test
     fun moveToDesktop() {
         val task = setUpFullscreenTask()
         controller.moveToDesktop(task)
-        val wct = getLatestWct()
+        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
             .isEqualTo(WINDOWING_MODE_FREEFORM)
     }
@@ -207,10 +236,27 @@
     }
 
     @Test
+    fun moveToDesktop_otherFreeformTasksBroughtToFront() {
+        val homeTask = setUpHomeTask()
+        val freeformTask = setUpFreeformTask()
+        val fullscreenTask = setUpFullscreenTask()
+        markTaskHidden(freeformTask)
+
+        controller.moveToDesktop(fullscreenTask)
+
+        with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+            assertThat(hierarchyOps).hasSize(3)
+            assertReorderSequence(homeTask, freeformTask, fullscreenTask)
+            assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+                .isEqualTo(WINDOWING_MODE_FREEFORM)
+        }
+    }
+
+    @Test
     fun moveToFullscreen() {
         val task = setUpFreeformTask()
         controller.moveToFullscreen(task)
-        val wct = getLatestWct()
+        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
             .isEqualTo(WINDOWING_MODE_FULLSCREEN)
     }
@@ -372,10 +418,12 @@
         desktopModeTaskRepository.updateVisibleFreeformTasks(task.taskId, visible = false)
     }
 
-    private fun getLatestWct(): WindowContainerTransaction {
+    private fun getLatestWct(
+        @WindowManager.TransitionType expectTransition: Int = TRANSIT_OPEN
+    ): WindowContainerTransaction {
         val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
         if (ENABLE_SHELL_TRANSITIONS) {
-            verify(transitions).startTransition(anyInt(), arg.capture(), isNull())
+            verify(transitions).startTransition(eq(expectTransition), arg.capture(), isNull())
         } else {
             verify(shellTaskOrganizer).applyTransaction(arg.capture())
         }
@@ -406,3 +454,9 @@
     assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
     assertThat(op.container).isEqualTo(task.token.asBinder())
 }
+
+private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
+    for (i in tasks.indices) {
+        assertReorderAt(i, tasks[i])
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index b6dbcf2..523cb66 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -48,7 +48,6 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -82,7 +81,7 @@
     @Mock
     private ShellExecutor mMainExecutor;
     @Mock
-    private SplitScreenController mSplitScreenController;
+    private WindowManager mWindowManager;
 
     private DragAndDropController mController;
 
@@ -100,11 +99,6 @@
     }
 
     @Test
-    public void instantiateController_registerConfigChangeListener() {
-        verify(mShellController, times(1)).addConfigurationChangeListener(any());
-    }
-
-    @Test
     public void testIgnoreNonDefaultDisplays() {
         final int nonDefaultDisplayId = 12345;
         final View dragLayout = mock(View.class);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 262e429..ec264a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -32,6 +32,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -57,17 +58,22 @@
     private PipBoundsAlgorithm mPipBoundsAlgorithm;
     private DisplayInfo mDefaultDisplayInfo;
     private PipBoundsState mPipBoundsState;
+    private PipSizeSpecHandler mPipSizeSpecHandler;
 
 
     @Before
     public void setUp() throws Exception {
         initializeMockResources();
-        mPipBoundsState = new PipBoundsState(mContext);
+        mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+        mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
-                new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+                new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
+                mPipSizeSpecHandler);
 
-        mPipBoundsState.setDisplayLayout(
-                new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
+        DisplayLayout layout =
+                new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true);
+        mPipBoundsState.setDisplayLayout(layout);
+        mPipSizeSpecHandler.setDisplayLayout(layout);
     }
 
     private void initializeMockResources() {
@@ -120,9 +126,7 @@
 
     @Test
     public void getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio() {
-        final Size defaultSize = mPipBoundsAlgorithm.getSizeForAspectRatio(DEFAULT_ASPECT_RATIO,
-                DEFAULT_MIN_EDGE_SIZE, mDefaultDisplayInfo.logicalWidth,
-                mDefaultDisplayInfo.logicalHeight);
+        final Size defaultSize = mPipSizeSpecHandler.getDefaultSize(DEFAULT_ASPECT_RATIO);
 
         mPipBoundsState.setOverrideMinSize(null);
         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
@@ -296,9 +300,9 @@
                 (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
         };
         final Size[] minimalSizes = new Size[] {
-                new Size((int) (100 * aspectRatios[0]), 100),
-                new Size((int) (100 * aspectRatios[1]), 100),
-                new Size((int) (100 * aspectRatios[2]), 100)
+                new Size((int) (200 * aspectRatios[0]), 200),
+                new Size((int) (200 * aspectRatios[1]), 200),
+                new Size((int) (200 * aspectRatios[2]), 200)
         };
         for (int i = 0; i < aspectRatios.length; i++) {
             final float aspectRatio = aspectRatios[i];
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
index 8e30f65..341a451 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
@@ -33,6 +33,7 @@
 
 import com.android.internal.util.function.TriConsumer;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -57,7 +58,7 @@
 
     @Before
     public void setUp() {
-        mPipBoundsState = new PipBoundsState(mContext);
+        mPipBoundsState = new PipBoundsState(mContext, new PipSizeSpecHandler(mContext));
         mTestComponentName1 = new ComponentName(mContext, "component1");
         mTestComponentName2 = new ComponentName(mContext, "component2");
     }
@@ -161,10 +162,10 @@
     @Test
     public void testSetOverrideMinSize_notChanged_callbackNotInvoked() {
         final Runnable callback = mock(Runnable.class);
-        mPipBoundsState.setOverrideMinSize(new Size(5, 5));
+        mPipBoundsState.setOverrideMinSize(new Size(100, 150));
         mPipBoundsState.setOnMinimalSizeChangeCallback(callback);
 
-        mPipBoundsState.setOverrideMinSize(new Size(5, 5));
+        mPipBoundsState.setOverrideMinSize(new Size(100, 150));
 
         verify(callback, never()).run();
     }
@@ -174,11 +175,11 @@
         mPipBoundsState.setOverrideMinSize(null);
         assertEquals(0, mPipBoundsState.getOverrideMinEdgeSize());
 
-        mPipBoundsState.setOverrideMinSize(new Size(5, 10));
-        assertEquals(5, mPipBoundsState.getOverrideMinEdgeSize());
+        mPipBoundsState.setOverrideMinSize(new Size(100, 110));
+        assertEquals(100, mPipBoundsState.getOverrideMinEdgeSize());
 
-        mPipBoundsState.setOverrideMinSize(new Size(15, 10));
-        assertEquals(10, mPipBoundsState.getOverrideMinEdgeSize());
+        mPipBoundsState.setOverrideMinSize(new Size(150, 200));
+        assertEquals(150, mPipBoundsState.getOverrideMinEdgeSize());
     }
 
     @Test
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 9088077..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
@@ -53,6 +53,7 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.pip.phone.PhonePipMenuController;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import org.junit.Before;
@@ -86,6 +87,7 @@
     private PipBoundsState mPipBoundsState;
     private PipTransitionState mPipTransitionState;
     private PipBoundsAlgorithm mPipBoundsAlgorithm;
+    private PipSizeSpecHandler mPipSizeSpecHandler;
 
     private ComponentName mComponent1;
     private ComponentName mComponent2;
@@ -95,13 +97,15 @@
         MockitoAnnotations.initMocks(this);
         mComponent1 = new ComponentName(mContext, "component1");
         mComponent2 = new ComponentName(mContext, "component2");
-        mPipBoundsState = new PipBoundsState(mContext);
+        mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+        mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
         mPipTransitionState = new PipTransitionState();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
-                new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+                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,
@@ -253,8 +257,10 @@
 
     private void preparePipTaskOrg() {
         final DisplayInfo info = new DisplayInfo();
-        mPipBoundsState.setDisplayLayout(new DisplayLayout(info,
-                mContext.getResources(), true, true));
+        DisplayLayout layout = new DisplayLayout(info,
+                mContext.getResources(), true, true);
+        mPipBoundsState.setDisplayLayout(layout);
+        mPipSizeSpecHandler.setDisplayLayout(layout);
         mPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA);
         mPipTaskOrganizer.setSurfaceControlTransactionFactory(
                 MockSurfaceControlHelper::createMockSurfaceControlTransaction);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 35c09a1..4a68287 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -65,7 +65,6 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -108,6 +107,7 @@
     @Mock private PipMotionHelper mMockPipMotionHelper;
     @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper;
     @Mock private PipBoundsState mMockPipBoundsState;
+    @Mock private PipSizeSpecHandler mMockPipSizeSpecHandler;
     @Mock private TaskStackListenerImpl mMockTaskStackListener;
     @Mock private ShellExecutor mMockExecutor;
     @Mock private Optional<OneHandedController> mMockOneHandedController;
@@ -130,11 +130,12 @@
         mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
                 mShellController, mMockDisplayController, mMockPipAnimationController,
                 mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
-                mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
-                mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
-                mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
-                mMockTaskStackListener, mMockPipParamsChangedForwarder,
-                mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor);
+                mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper,
+                mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer,
+                mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController,
+                mMockWindowManagerShellWrapper, mMockTaskStackListener,
+                mMockPipParamsChangedForwarder, mMockDisplayInsetsController,
+                mMockOneHandedController, mMockExecutor);
         mShellInit.init();
         when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
         when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
@@ -220,11 +221,12 @@
         assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
                 mShellController, mMockDisplayController, mMockPipAnimationController,
                 mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
-                mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
-                mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
-                mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
-                mMockTaskStackListener, mMockPipParamsChangedForwarder,
-                mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor));
+                mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper,
+                mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer,
+                mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController,
+                mMockWindowManagerShellWrapper, mMockTaskStackListener,
+                mMockPipParamsChangedForwarder, mMockDisplayInsetsController,
+                mMockOneHandedController, mMockExecutor));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 3bd2ae7..c7b9eb3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -37,7 +37,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
@@ -85,15 +85,18 @@
 
     private PipBoundsState mPipBoundsState;
 
+    private PipSizeSpecHandler mPipSizeSpecHandler;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mPipBoundsState = new PipBoundsState(mContext);
+        mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+        mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
         final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
-        final PipKeepClearAlgorithm pipKeepClearAlgorithm =
-                new PipKeepClearAlgorithm() {};
+        final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm =
+                new PipKeepClearAlgorithmInterface() {};
         final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
-                mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm);
+                mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipSizeSpecHandler);
         final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
                 mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
new file mode 100644
index 0000000..d9ff7d1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.wm.shell.pip.phone;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.SystemProperties;
+import android.testing.AndroidTestingRunner;
+import android.util.Size;
+import android.view.DisplayInfo;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayLayout;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.exceptions.misusing.InvalidUseOfMatchersException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Unit test against {@link PipSizeSpecHandler} with feature flag on.
+ */
+@RunWith(AndroidTestingRunner.class)
+public class PipSizeSpecHandlerTest extends ShellTestCase {
+    /** A sample overridden min edge size. */
+    private static final int OVERRIDE_MIN_EDGE_SIZE = 40;
+    /** A sample default min edge size */
+    private static final int DEFAULT_MIN_EDGE_SIZE = 40;
+    /** Display edge size */
+    private static final int DISPLAY_EDGE_SIZE = 1000;
+    /** Default sizing percentage */
+    private static final float DEFAULT_PERCENT = 0.6f;
+    /** Minimum sizing percentage */
+    private static final float MIN_PERCENT = 0.5f;
+    /** Aspect ratio that the new PIP size spec logic optimizes for. */
+    private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16;
+
+    /** A map of aspect ratios to be tested to expected sizes */
+    private static Map<Float, Size> sExpectedMaxSizes;
+    private static Map<Float, Size> sExpectedDefaultSizes;
+    private static Map<Float, Size> sExpectedMinSizes;
+    /** A static mockito session object to mock {@link SystemProperties} */
+    private static StaticMockitoSession sStaticMockitoSession;
+
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+
+    private PipSizeSpecHandler mPipSizeSpecHandler;
+
+    /**
+     * Sets up static Mockito session for SystemProperties and mocks necessary static methods.
+     */
+    private static void setUpStaticSystemPropertiesSession() {
+        sStaticMockitoSession = mockitoSession()
+                .mockStatic(SystemProperties.class).startMocking();
+        // make sure the feature flag is on
+        when(SystemProperties.getBoolean(anyString(), anyBoolean())).thenReturn(true);
+        when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> {
+            String property = invocation.getArgument(0);
+            if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) {
+                return Float.toString(DEFAULT_PERCENT);
+            } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) {
+                return Float.toString(MIN_PERCENT);
+            }
+
+            // throw an exception if illegal arguments are used for these tests
+            throw new InvalidUseOfMatchersException(
+                String.format("Argument %s does not match", property)
+            );
+        });
+    }
+
+    /**
+     * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes.
+     */
+    private static void initExpectedSizes() {
+        sExpectedMaxSizes = new HashMap<>();
+        sExpectedDefaultSizes = new HashMap<>();
+        sExpectedMinSizes = new HashMap<>();
+
+        sExpectedMaxSizes.put(16f / 9, new Size(1000, 562));
+        sExpectedDefaultSizes.put(16f / 9, new Size(600, 337));
+        sExpectedMinSizes.put(16f / 9, new Size(499, 281));
+
+        sExpectedMaxSizes.put(4f / 3, new Size(892, 669));
+        sExpectedDefaultSizes.put(4f / 3, new Size(535, 401));
+        sExpectedMinSizes.put(4f / 3, new Size(445, 334));
+
+        sExpectedMaxSizes.put(3f / 4, new Size(669, 892));
+        sExpectedDefaultSizes.put(3f / 4, new Size(401, 535));
+        sExpectedMinSizes.put(3f / 4, new Size(334, 445));
+
+        sExpectedMaxSizes.put(9f / 16, new Size(562, 999));
+        sExpectedDefaultSizes.put(9f / 16, new Size(337, 599));
+        sExpectedMinSizes.put(9f / 16, new Size(281, 499));
+    }
+
+    private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes,
+            Function<Float, Size> callback) {
+        for (Map.Entry<Float, Size> expectedSizesEntry : expectedSizes.entrySet()) {
+            float aspectRatio = expectedSizesEntry.getKey();
+            Size expectedSize = expectedSizesEntry.getValue();
+
+            Assert.assertEquals(expectedSize, callback.apply(aspectRatio));
+        }
+    }
+
+    @Before
+    public void setUp() {
+        initExpectedSizes();
+        setUpStaticSystemPropertiesSession();
+
+        when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE);
+        when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO);
+        when(mResources.getString(anyInt())).thenReturn("0x0");
+        when(mResources.getDisplayMetrics())
+                .thenReturn(getContext().getResources().getDisplayMetrics());
+
+        // set up the mock context for spec handler specifically
+        when(mContext.getResources()).thenReturn(mResources);
+
+        mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+
+        // no overridden min edge size by default
+        mPipSizeSpecHandler.setOverrideMinSize(null);
+
+        DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = DISPLAY_EDGE_SIZE;
+        displayInfo.logicalHeight = DISPLAY_EDGE_SIZE;
+
+        // use the parent context (not the mocked one) to obtain the display layout
+        // this is done to avoid unnecessary mocking while allowing for custom display dimensions
+        DisplayLayout displayLayout = new DisplayLayout(displayInfo, getContext().getResources(),
+                false, false);
+        mPipSizeSpecHandler.setDisplayLayout(displayLayout);
+    }
+
+    @After
+    public void cleanUp() {
+        sStaticMockitoSession.finishMocking();
+    }
+
+    @Test
+    public void testGetMaxSize() {
+        forEveryTestCaseCheck(sExpectedMaxSizes,
+                (aspectRatio) -> mPipSizeSpecHandler.getMaxSize(aspectRatio));
+    }
+
+    @Test
+    public void testGetDefaultSize() {
+        forEveryTestCaseCheck(sExpectedDefaultSizes,
+                (aspectRatio) -> mPipSizeSpecHandler.getDefaultSize(aspectRatio));
+    }
+
+    @Test
+    public void testGetMinSize() {
+        forEveryTestCaseCheck(sExpectedMinSizes,
+                (aspectRatio) -> mPipSizeSpecHandler.getMinSize(aspectRatio));
+    }
+
+    @Test
+    public void testGetSizeForAspectRatio_noOverrideMinSize() {
+        // an initial size with 16:9 aspect ratio
+        Size initSize = new Size(600, 337);
+
+        Size expectedSize = new Size(337, 599);
+        Size actualSize = mPipSizeSpecHandler.getSizeForAspectRatio(initSize, 9f / 16);
+
+        Assert.assertEquals(expectedSize, actualSize);
+    }
+
+    @Test
+    public void testGetSizeForAspectRatio_withOverrideMinSize() {
+        // an initial size with a 1:1 aspect ratio
+        mPipSizeSpecHandler.setOverrideMinSize(new Size(OVERRIDE_MIN_EDGE_SIZE,
+                OVERRIDE_MIN_EDGE_SIZE));
+        // make sure initial size is same as override min size
+        Size initSize = mPipSizeSpecHandler.getOverrideMinSize();
+
+        Size expectedSize = new Size(40, 71);
+        Size actualSize = mPipSizeSpecHandler.getSizeForAspectRatio(initSize, 9f / 16);
+
+        Assert.assertEquals(expectedSize, actualSize);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 474d6aa..5c4863f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -25,6 +25,7 @@
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.util.Size;
 
 import androidx.test.filters.SmallTest;
 
@@ -34,7 +35,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
@@ -90,6 +91,7 @@
     private PipSnapAlgorithm mPipSnapAlgorithm;
     private PipMotionHelper mMotionHelper;
     private PipResizeGestureHandler mPipResizeGestureHandler;
+    private PipSizeSpecHandler mPipSizeSpecHandler;
 
     private DisplayLayout mDisplayLayout;
     private Rect mInsetBounds;
@@ -103,16 +105,17 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mPipBoundsState = new PipBoundsState(mContext);
+        mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+        mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
         mPipSnapAlgorithm = new PipSnapAlgorithm();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
-                new PipKeepClearAlgorithm() {});
+                new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler);
         PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
                 mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator);
         mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController,
-                mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, pipMotionHelper,
-                mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor);
+                mPipBoundsAlgorithm, mPipBoundsState, mPipSizeSpecHandler, mPipTaskOrganizer,
+                pipMotionHelper, mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor);
         // We aren't actually using ShellInit, so just call init directly
         mPipTouchHandler.onInit();
         mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper());
@@ -122,6 +125,7 @@
 
         mDisplayLayout = new DisplayLayout(mContext, mContext.getDisplay());
         mPipBoundsState.setDisplayLayout(mDisplayLayout);
+        mPipSizeSpecHandler.setDisplayLayout(mDisplayLayout);
         mInsetBounds = new Rect(mPipBoundsState.getDisplayBounds().left + INSET,
                 mPipBoundsState.getDisplayBounds().top + INSET,
                 mPipBoundsState.getDisplayBounds().right - INSET,
@@ -154,13 +158,17 @@
         mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mPipBounds, mCurBounds,
                 mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation);
 
+        // getting the expected min and max size
+        float aspectRatio = (float) mPipBounds.width() / mPipBounds.height();
+        Size expectedMinSize = mPipSizeSpecHandler.getMinSize(aspectRatio);
+        Size expectedMaxSize = mPipSizeSpecHandler.getMaxSize(aspectRatio);
+
         assertEquals(expectedMovementBounds, mPipBoundsState.getNormalMovementBounds());
         verify(mPipResizeGestureHandler, times(1))
-                .updateMinSize(mPipBounds.width(), mPipBounds.height());
+                .updateMinSize(expectedMinSize.getWidth(), expectedMinSize.getHeight());
 
         verify(mPipResizeGestureHandler, times(1))
-                .updateMaxSize(shorterLength - 2 * mInsetBounds.left,
-                        shorterLength - 2 * mInsetBounds.left);
+                .updateMaxSize(expectedMaxSize.getWidth(), expectedMaxSize.getHeight());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 82392ad9..b542fae 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -253,10 +253,10 @@
     }
 
     @Test
-    public void testGetRecentTasks_groupActiveFreeformTasks() {
+    public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() {
         StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
                 DesktopModeStatus.class).startMocking();
-        when(DesktopModeStatus.isActive(any())).thenReturn(true);
+        when(DesktopModeStatus.isProto2Enabled()).thenReturn(true);
 
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
@@ -293,6 +293,39 @@
     }
 
     @Test
+    public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() {
+        StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
+                DesktopModeStatus.class).startMocking();
+        when(DesktopModeStatus.isProto2Enabled()).thenReturn(false);
+
+        ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+        ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+        ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+        ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
+        setRawList(t1, t2, t3, t4);
+
+        when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
+        when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+
+        ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+                MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+
+        // Expect no grouping of tasks
+        assertEquals(4, recentTasks.size());
+        assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(0).getType());
+        assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(1).getType());
+        assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(2).getType());
+        assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(3).getType());
+
+        assertEquals(t1, recentTasks.get(0).getTaskInfo1());
+        assertEquals(t2, recentTasks.get(1).getTaskInfo1());
+        assertEquals(t3, recentTasks.get(2).getTaskInfo1());
+        assertEquals(t4, recentTasks.get(3).getTaskInfo1());
+
+        mockitoSession.finishMocking();
+    }
+
+    @Test
     public void testRemovedTaskRemovesSplit() {
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index ea3af9d..d0e2601 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -27,8 +27,6 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -201,7 +199,6 @@
         ActivityManager.RunningTaskInfo topRunningTask =
                 createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
         doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
-        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
 
         mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
 
@@ -222,7 +219,6 @@
         ActivityManager.RunningTaskInfo topRunningTask =
                 createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
         doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
-        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
         // Put the same component into a task in the background
         ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
         doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 65e1ea8..0bb809d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -29,11 +29,13 @@
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -45,6 +47,8 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -57,6 +61,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
@@ -65,6 +70,8 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -98,11 +105,7 @@
     @Mock
     private DisplayInsetsController mDisplayInsetsController;
     @Mock
-    private Transitions mTransitions;
-    @Mock
     private TransactionPool mTransactionPool;
-    @Mock
-    private ShellExecutor mMainExecutor;
 
     private final Rect mBounds1 = new Rect(10, 20, 30, 40);
     private final Rect mBounds2 = new Rect(5, 10, 15, 20);
@@ -112,11 +115,16 @@
     private SurfaceControl mRootLeash;
     private ActivityManager.RunningTaskInfo mRootTask;
     private StageCoordinator mStageCoordinator;
+    private Transitions mTransitions;
+    private final TestShellExecutor mMainExecutor = new TestShellExecutor();
+    private final ShellExecutor mAnimExecutor = new TestShellExecutor();
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
 
     @Before
     @UiThreadTest
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mTransitions = createTestTransitions();
         mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
@@ -329,7 +337,20 @@
 
         mStageCoordinator.onFoldedStateChanged(true);
 
-        verify(mStageCoordinator).onSplitScreenExit();
-        verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            verify(mTaskOrganizer).startNewTransition(eq(TRANSIT_SPLIT_DISMISS), notNull());
+        } else {
+            verify(mStageCoordinator).onSplitScreenExit();
+            verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+        }
+    }
+
+    private Transitions createTestTransitions() {
+        ShellInit shellInit = new ShellInit(mMainExecutor);
+        final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
+                mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor,
+                mMainHandler, mAnimExecutor);
+        shellInit.init();
+        return t;
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
index 3550721..1d1aa79 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
@@ -49,6 +49,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.DesktopModeController;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -73,6 +74,7 @@
     @Mock private Choreographer mMainChoreographer;
     @Mock private ShellTaskOrganizer mTaskOrganizer;
     @Mock private DisplayController mDisplayController;
+    @Mock private SplitScreenController mSplitScreenController;
     @Mock private SyncTransactionQueue mSyncQueue;
     @Mock private DesktopModeController mDesktopModeController;
     @Mock private DesktopTasksController mDesktopTasksController;
@@ -98,6 +100,7 @@
                 mSyncQueue,
                 Optional.of(mDesktopModeController),
                 Optional.of(mDesktopTasksController),
+                Optional.of(mSplitScreenController),
                 mDesktopModeWindowDecorFactory,
                 mMockInputMonitorFactory
             );
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
new file mode 100644
index 0000000..8f84008
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.wm.shell.windowdecor
+
+import android.os.SystemClock
+import android.testing.AndroidTestingRunner
+import android.view.MotionEvent
+import android.view.InputDevice
+import androidx.test.filters.SmallTest
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Tests for [DragDetector].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DragDetectorTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DragDetectorTest {
+    private val motionEvents = mutableListOf<MotionEvent>()
+
+    @Mock
+    private lateinit var eventHandler: DragDetector.MotionEventHandler
+
+    private lateinit var dragDetector: DragDetector
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(eventHandler.handleMotionEvent(any())).thenReturn(true)
+
+        dragDetector = DragDetector(eventHandler)
+        dragDetector.setTouchSlop(SLOP)
+    }
+
+    @After
+    fun tearDown() {
+        motionEvents.forEach {
+            it.recycle()
+        }
+        motionEvents.clear()
+    }
+
+    @Test
+    fun testNoMove_passesDownAndUp() {
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+    }
+
+    @Test
+    fun testMoveInSlop_touch_passesDownAndUp() {
+        `when`(eventHandler.handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_DOWN
+        })).thenReturn(false)
+
+        assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+
+        val newX = X + SLOP - 1
+        assertFalse(
+                dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
+        verify(eventHandler, never()).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_MOVE
+        })
+
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+    }
+
+    @Test
+    fun testMoveInSlop_mouse_passesDownMoveAndUp() {
+        `when`(eventHandler.handleMotionEvent(argThat {
+            it.action == MotionEvent.ACTION_DOWN
+        })).thenReturn(false)
+
+        assertFalse(dragDetector.onMotionEvent(
+                createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+                    it.source == InputDevice.SOURCE_MOUSE
+        })
+
+        val newX = X + SLOP - 1
+        assertTrue(dragDetector.onMotionEvent(
+                createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+                    it.source == InputDevice.SOURCE_MOUSE
+        })
+
+        assertTrue(dragDetector.onMotionEvent(
+                createMotionEvent(MotionEvent.ACTION_UP, newX, Y, isTouch = false)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
+                    it.source == InputDevice.SOURCE_MOUSE
+        })
+    }
+
+    @Test
+    fun testMoveBeyondSlop_passesDownMoveAndUp() {
+        `when`(eventHandler.handleMotionEvent(argThat {
+            it.action == MotionEvent.ACTION_DOWN
+        })).thenReturn(false)
+
+        assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+
+        val newX = X + SLOP + 1
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
+                    it.source == InputDevice.SOURCE_TOUCHSCREEN
+        })
+    }
+
+    @Test
+    fun testPassesHoverEnter() {
+        `when`(eventHandler.handleMotionEvent(argThat {
+            it.action == MotionEvent.ACTION_HOVER_ENTER
+        })).thenReturn(false)
+
+        assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_ENTER)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_HOVER_ENTER && it.x == X && it.y == Y
+        })
+    }
+
+    @Test
+    fun testPassesHoverMove() {
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y
+        })
+    }
+
+    @Test
+    fun testPassesHoverExit() {
+        assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT)))
+        verify(eventHandler).handleMotionEvent(argThat {
+            return@argThat it.action == MotionEvent.ACTION_HOVER_EXIT && it.x == X && it.y == Y
+        })
+    }
+
+    private fun createMotionEvent(action: Int, x: Float = X, y: Float = Y, isTouch: Boolean = true):
+            MotionEvent {
+        val time = SystemClock.uptimeMillis()
+        val ev = MotionEvent.obtain(time, time, action, x, y, 0)
+        ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE
+        motionEvents.add(ev)
+        return ev
+    }
+
+    companion object {
+        private const val SLOP = 10
+        private const val X = 123f
+        private const val Y = 234f
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
index ac10ddb..8f66f4e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
@@ -1,15 +1,19 @@
 package com.android.wm.shell.windowdecor
 
 import android.app.ActivityManager
+import android.app.WindowConfiguration
 import android.graphics.Rect
 import android.os.IBinder
 import android.testing.AndroidTestingRunner
 import android.window.WindowContainerToken
 import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
 import androidx.test.filters.SmallTest
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP
 import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED
 import org.junit.Before
 import org.junit.Test
@@ -43,6 +47,11 @@
     @Mock
     private lateinit var taskBinder: IBinder
 
+    @Mock
+    private lateinit var mockDisplayController: DisplayController
+    @Mock
+    private lateinit var mockDisplayLayout: DisplayLayout
+
     private lateinit var taskPositioner: TaskPositioner
 
     @Before
@@ -52,19 +61,112 @@
         taskPositioner = TaskPositioner(
                 mockShellTaskOrganizer,
                 mockWindowDecoration,
+                mockDisplayController,
                 mockDragStartListener
         )
+
         `when`(taskToken.asBinder()).thenReturn(taskBinder)
+        `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
+        `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
+
         mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
             taskId = TASK_ID
             token = taskToken
+            minWidth = MIN_WIDTH
+            minHeight = MIN_HEIGHT
+            defaultMinSize = DEFAULT_MIN
+            displayId = DISPLAY_ID
             configuration.windowConfiguration.bounds = STARTING_BOUNDS
         }
     }
 
     @Test
+    fun testDragResize_notMove_skipsTransactionOnEnd() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        taskPositioner.onDragPositioningEnd(
+                STARTING_BOUNDS.left.toFloat() + 10,
+                STARTING_BOUNDS.top.toFloat() + 10
+        )
+
+        verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_noEffectiveMove_skipsTransactionOnMoveAndEnd() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        taskPositioner.onDragPositioningMove(
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        taskPositioner.onDragPositioningEnd(
+                STARTING_BOUNDS.left.toFloat() + 10,
+                STARTING_BOUNDS.top.toFloat() + 10
+        )
+
+        verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_hasEffectiveMove_issuesTransactionOnMoveAndEnd() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        taskPositioner.onDragPositioningMove(
+                STARTING_BOUNDS.left.toFloat() + 10,
+                STARTING_BOUNDS.top.toFloat()
+        )
+        val rectAfterMove = Rect(STARTING_BOUNDS)
+        rectAfterMove.right += 10
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+                        change.configuration.windowConfiguration.bounds == rectAfterMove
+            }
+        })
+
+        taskPositioner.onDragPositioningEnd(
+                STARTING_BOUNDS.left.toFloat() + 10,
+                STARTING_BOUNDS.top.toFloat() + 10
+        )
+        val rectAfterEnd = Rect(rectAfterMove)
+        rectAfterEnd.top += 10
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+                        change.configuration.windowConfiguration.bounds == rectAfterEnd
+            }
+        })
+    }
+
+    @Test
     fun testDragResize_move_skipsDragResizingFlag() {
-        taskPositioner.onDragResizeStart(
+        taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // Move
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
@@ -73,12 +175,12 @@
         // Move the task 10px to the right.
         val newX = STARTING_BOUNDS.left.toFloat() + 10
         val newY = STARTING_BOUNDS.top.toFloat()
-        taskPositioner.onDragResizeMove(
+        taskPositioner.onDragPositioningMove(
                 newX,
                 newY
         )
 
-        taskPositioner.onDragResizeEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(newX, newY)
 
         verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -91,7 +193,7 @@
 
     @Test
     fun testDragResize_resize_setsDragResizingFlag() {
-        taskPositioner.onDragResizeStart(
+        taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
@@ -100,12 +202,12 @@
         // Resize the task by 10px to the right.
         val newX = STARTING_BOUNDS.right.toFloat() + 10
         val newY = STARTING_BOUNDS.top.toFloat()
-        taskPositioner.onDragResizeMove(
+        taskPositioner.onDragPositioningMove(
                 newX,
                 newY
         )
 
-        taskPositioner.onDragResizeEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -123,8 +225,239 @@
         })
     }
 
+    @Test
+    fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenLessThanMin() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Resize to width of 95px and height of 5px with min width of 10px
+        val newX = STARTING_BOUNDS.right.toFloat() - 5
+        val newY = STARTING_BOUNDS.top.toFloat() + 95
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS)
+                                != 0) && change.configuration.windowConfiguration.bounds.top ==
+                        STARTING_BOUNDS.top &&
+                        change.configuration.windowConfiguration.bounds.bottom ==
+                        STARTING_BOUNDS.bottom
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenLessThanMin() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Resize to height of 95px and width of 5px with min width of 10px
+        val newX = STARTING_BOUNDS.right.toFloat() - 95
+        val newY = STARTING_BOUNDS.top.toFloat() + 5
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS)
+                                != 0) && change.configuration.windowConfiguration.bounds.right ==
+                        STARTING_BOUNDS.right &&
+                        change.configuration.windowConfiguration.bounds.left ==
+                        STARTING_BOUNDS.left
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenNegative() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Resize to height of -5px and width of 95px
+        val newX = STARTING_BOUNDS.right.toFloat() - 5
+        val newY = STARTING_BOUNDS.top.toFloat() + 105
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS)
+                                != 0) && change.configuration.windowConfiguration.bounds.top ==
+                        STARTING_BOUNDS.top &&
+                        change.configuration.windowConfiguration.bounds.bottom ==
+                        STARTING_BOUNDS.bottom
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenNegative() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Resize to width of -5px and height of 95px
+        val newX = STARTING_BOUNDS.right.toFloat() - 105
+        val newY = STARTING_BOUNDS.top.toFloat() + 5
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS)
+                                != 0) && change.configuration.windowConfiguration.bounds.right ==
+                        STARTING_BOUNDS.right &&
+                        change.configuration.windowConfiguration.bounds.left ==
+                        STARTING_BOUNDS.left
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_setBoundsRunsWhenResizeBoundsValid() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Shrink to height 20px and width 20px with both min height/width equal to 10px
+        val newX = STARTING_BOUNDS.right.toFloat() - 80
+        val newY = STARTING_BOUNDS.top.toFloat() + 80
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_setBoundsDoesNotRunWithNegativeHeightAndWidth() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Shrink to height 5px and width 5px with both min height/width equal to 10px
+        val newX = STARTING_BOUNDS.right.toFloat() - 95
+        val newY = STARTING_BOUNDS.top.toFloat() + 95
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_useDefaultMinWhenMinWidthInvalid() {
+        mockWindowDecoration.mTaskInfo.minWidth = -1
+
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Shrink to width and height of 3px with invalid minWidth = -1 and defaultMinSize = 5px
+        val newX = STARTING_BOUNDS.right.toFloat() - 97
+        val newY = STARTING_BOUNDS.top.toFloat() + 97
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_useMinWidthWhenValid() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                STARTING_BOUNDS.right.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Shrink to width and height of 7px with valid minWidth = 10px and defaultMinSize = 5px
+        val newX = STARTING_BOUNDS.right.toFloat() - 93
+        val newY = STARTING_BOUNDS.top.toFloat() + 93
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragPositioningEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+            }
+        })
+    }
+
     companion object {
         private const val TASK_ID = 5
+        private const val MIN_WIDTH = 10
+        private const val MIN_HEIGHT = 10
+        private const val DENSITY_DPI = 20
+        private const val DEFAULT_MIN = 40
+        private const val DISPLAY_ID = 1
         private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index ec4f17f..b80edce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -107,6 +107,7 @@
     private SurfaceControl.Transaction mMockSurfaceControlFinishT;
     private SurfaceControl.Transaction mMockSurfaceControlAddWindowT;
     private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
+    private int mCaptionMenuWidthId;
 
     @Before
     public void setUp() {
@@ -116,8 +117,7 @@
 
         mRelayoutParams.mLayoutResId = 0;
         mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
-        // Caption should have fixed width except in testLayoutResultCalculation_fullWidthCaption()
-        mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width;
+        mCaptionMenuWidthId = R.dimen.test_freeform_decor_caption_menu_width;
         mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
 
         doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
@@ -240,7 +240,7 @@
         verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
         verify(captionContainerSurfaceBuilder).setContainerLayer();
         verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
-        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 432, 64);
+        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
         verify(mMockSurfaceControlStartT).show(captionContainerSurface);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
@@ -248,7 +248,7 @@
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
                         argThat(lp -> lp.height == 64
-                                && lp.width == 432
+                                && lp.width == 300
                                 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
         if (ViewRootImpl.CAPTION_ON_SHELL) {
             verify(mMockView).setTaskFocusState(true);
@@ -431,7 +431,7 @@
         verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface);
         verify(additionalWindowSurfaceBuilder).build();
         verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40);
-        verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, 432, 64);
+        verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, 442, 74);
         verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface);
         verify(mMockSurfaceControlViewHostFactory, Mockito.times(2))
                 .create(any(), eq(defaultDisplay), any());
@@ -484,7 +484,6 @@
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
 
-        mRelayoutParams.mCaptionWidthId = Resources.ID_NULL;
         windowDecor.relayout(taskInfo);
 
         verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
@@ -558,7 +557,7 @@
             final Resources resources = mDecorWindowContext.getResources();
             int x = mRelayoutParams.mCaptionX;
             int y = mRelayoutParams.mCaptionY;
-            int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+            int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId);
             int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
             String name = "Test Window";
             WindowDecoration.AdditionalWindow additionalWindow =
@@ -566,7 +565,7 @@
                             mMockSurfaceControlAddWindowT,
                             x - mRelayoutResult.mDecorContainerOffsetX,
                             y - mRelayoutResult.mDecorContainerOffsetY,
-                            width, height);
+                            width, height, 10);
             return additionalWindow;
         }
     }
diff --git a/libs/dream/OWNERS b/libs/dream/OWNERS
new file mode 100644
index 0000000..a4b0127
--- /dev/null
+++ b/libs/dream/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/service/dreams/OWNERS
\ No newline at end of file
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index 90c4440..2ab7a58 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -174,14 +174,13 @@
 
 void ShaderCache::saveToDiskLocked() {
     ATRACE_NAME("ShaderCache::saveToDiskLocked");
-    if (mInitialized && mBlobCache && mSavePending) {
+    if (mInitialized && mBlobCache) {
         if (mIDHash.size()) {
             auto key = sIDKey;
             set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size());
         }
         mBlobCache->writeToFile();
     }
-    mSavePending = false;
 }
 
 void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) {
@@ -224,10 +223,10 @@
     }
     set(bc, key.data(), keySize, value, valueSize);
 
-    if (!mSavePending && mDeferredSaveDelay > 0) {
+    if (!mSavePending && mDeferredSaveDelayMs > 0) {
         mSavePending = true;
         std::thread deferredSaveThread([this]() {
-            sleep(mDeferredSaveDelay);
+            usleep(mDeferredSaveDelayMs * 1000);  // milliseconds to microseconds
             std::lock_guard<std::mutex> lock(mMutex);
             // Store file on disk if there a new shader or Vulkan pipeline cache size changed.
             if (mCacheDirty || mNewPipelineCacheSize != mOldPipelineCacheSize) {
@@ -236,6 +235,7 @@
                 mTryToStorePipelineCache = false;
                 mCacheDirty = false;
             }
+            mSavePending = false;
         });
         deferredSaveThread.detach();
     }
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index 3e0fd51..4e3eb81 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -153,7 +153,8 @@
      * pending.  Each time a key/value pair is inserted into the cache via
      * load, a deferred save is initiated if one is not already pending.
      * This will wait some amount of time and then trigger a save of the cache
-     * contents to disk.
+     * contents to disk, unless mDeferredSaveDelayMs is 0 in which case saving
+     * is disabled.
      */
     bool mSavePending = false;
 
@@ -163,9 +164,11 @@
     size_t mObservedBlobValueSize = 20 * 1024;
 
     /**
-     *  The time in seconds to wait before saving newly inserted cache entries.
+     *  The time in milliseconds to wait before saving newly inserted cache entries.
+     *
+     *  WARNING: setting this to 0 will disable writing the cache to disk.
      */
-    unsigned int mDeferredSaveDelay = 4;
+    unsigned int mDeferredSaveDelayMs = 4 * 1000;
 
     /**
      * "mMutex" is the mutex used to prevent concurrent access to the member
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 75d3ff7..602554a 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -537,7 +537,7 @@
             const auto inputEventId =
                     static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId));
             native_window_set_frame_timeline_info(
-                    mNativeSurface->getNativeWindow(), vsyncId, inputEventId,
+                    mNativeSurface->getNativeWindow(), frameCompleteNr, vsyncId, inputEventId,
                     mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime));
         }
     }
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index 03f02de..3c75499 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -280,6 +280,11 @@
     mSignal.signal();
 }
 
+void DrawFrameTask::createHintSession(pid_t uiThreadId, pid_t renderThreadId) {
+    if (mHintSessionWrapper) return;
+    mHintSessionWrapper.emplace(uiThreadId, renderThreadId);
+}
+
 DrawFrameTask::HintSessionWrapper::HintSessionWrapper(int32_t uiThreadId, int32_t renderThreadId) {
     if (!Properties::useHintManager) return;
     if (uiThreadId < 0 || renderThreadId < 0) return;
diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h
index d6fc292..b135a21 100644
--- a/libs/hwui/renderthread/DrawFrameTask.h
+++ b/libs/hwui/renderthread/DrawFrameTask.h
@@ -90,6 +90,8 @@
 
     void forceDrawNextFrame() { mForceDrawFrame = true; }
 
+    void createHintSession(pid_t uiThreadId, pid_t renderThreadId);
+
 private:
     class HintSessionWrapper {
     public:
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index a44b498..d9b650c 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -38,11 +38,18 @@
 RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode,
                          IContextFactory* contextFactory)
         : mRenderThread(RenderThread::getInstance()), mContext(nullptr) {
-    mContext = mRenderThread.queue().runSync([&]() -> CanvasContext* {
-        return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
+    pid_t uiThreadId = pthread_gettid_np(pthread_self());
+    pid_t renderThreadId = getRenderThreadTid();
+    mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* {
+        CanvasContext* context =
+                CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
+        if (context != nullptr) {
+            mRenderThread.queue().post(
+                    [=] { mDrawFrameTask.createHintSession(uiThreadId, renderThreadId); });
+        }
+        return context;
     });
-    mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode,
-                              pthread_gettid_np(pthread_self()), getRenderThreadTid());
+    mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode, uiThreadId, renderThreadId);
 }
 
 RenderProxy::~RenderProxy() {
diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp
index 974d85a..7bcd45c 100644
--- a/libs/hwui/tests/unit/ShaderCacheTests.cpp
+++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
+#include <GrDirectContext.h>
+#include <Properties.h>
+#include <SkData.h>
+#include <SkRefCnt.h>
 #include <cutils/properties.h>
 #include <dirent.h>
 #include <errno.h>
@@ -22,9 +26,12 @@
 #include <stdlib.h>
 #include <sys/types.h>
 #include <utils/Log.h>
+
 #include <cstdint>
+
 #include "FileBlobCache.h"
 #include "pipeline/skia/ShaderCache.h"
+#include "tests/common/TestUtils.h"
 
 using namespace android::uirenderer::skiapipeline;
 
@@ -35,11 +42,38 @@
 class ShaderCacheTestUtils {
 public:
     /**
-     * "setSaveDelay" sets the time in seconds to wait before saving newly inserted cache entries.
-     * If set to 0, then deferred save is disabled.
+     * Hack to reset all member variables of the given cache to their default / initial values.
+     *
+     * WARNING: this must be kept up to date manually, since ShaderCache's parent disables just
+     * reassigning a new instance.
      */
-    static void setSaveDelay(ShaderCache& cache, unsigned int saveDelay) {
-        cache.mDeferredSaveDelay = saveDelay;
+    static void reinitializeAllFields(ShaderCache& cache) {
+        ShaderCache newCache = ShaderCache();
+        std::lock_guard<std::mutex> lock(cache.mMutex);
+        // By order of declaration
+        cache.mInitialized = newCache.mInitialized;
+        cache.mBlobCache.reset(nullptr);
+        cache.mFilename = newCache.mFilename;
+        cache.mIDHash.clear();
+        cache.mSavePending = newCache.mSavePending;
+        cache.mObservedBlobValueSize = newCache.mObservedBlobValueSize;
+        cache.mDeferredSaveDelayMs = newCache.mDeferredSaveDelayMs;
+        cache.mTryToStorePipelineCache = newCache.mTryToStorePipelineCache;
+        cache.mInStoreVkPipelineInProgress = newCache.mInStoreVkPipelineInProgress;
+        cache.mNewPipelineCacheSize = newCache.mNewPipelineCacheSize;
+        cache.mOldPipelineCacheSize = newCache.mOldPipelineCacheSize;
+        cache.mCacheDirty = newCache.mCacheDirty;
+        cache.mNumShadersCachedInRam = newCache.mNumShadersCachedInRam;
+    }
+
+    /**
+     * "setSaveDelayMs" sets the time in milliseconds to wait before saving newly inserted cache
+     * entries. If set to 0, then deferred save is disabled, and "saveToDiskLocked" must be called
+     * manually, as seen in the "terminate" testing helper function.
+     */
+    static void setSaveDelayMs(ShaderCache& cache, unsigned int saveDelayMs) {
+        std::lock_guard<std::mutex> lock(cache.mMutex);
+        cache.mDeferredSaveDelayMs = saveDelayMs;
     }
 
     /**
@@ -48,8 +82,9 @@
      */
     static void terminate(ShaderCache& cache, bool saveContent) {
         std::lock_guard<std::mutex> lock(cache.mMutex);
-        cache.mSavePending = saveContent;
-        cache.saveToDiskLocked();
+        if (saveContent) {
+            cache.saveToDiskLocked();
+        }
         cache.mBlobCache = NULL;
     }
 
@@ -60,6 +95,38 @@
     static bool validateCache(ShaderCache& cache, std::vector<T> hash) {
         return cache.validateCache(hash.data(), hash.size() * sizeof(T));
     }
+
+    /**
+     * Waits until cache::mSavePending is false, checking every 0.1 ms *while the mutex is free*.
+     *
+     * Fails if there was no save pending, or if the cache was already being written to disk, or if
+     * timeoutMs is exceeded.
+     *
+     * Note: timeoutMs only guards against mSavePending getting stuck like in b/268205519, and
+     * cannot protect against mutex-based deadlock. Reaching timeoutMs implies something is broken,
+     * so setting it to a sufficiently large value will not delay execution in the happy state.
+     */
+    static void waitForPendingSave(ShaderCache& cache, const int timeoutMs = 50) {
+        {
+            std::lock_guard<std::mutex> lock(cache.mMutex);
+            ASSERT_TRUE(cache.mSavePending);
+        }
+        bool saving = true;
+        float elapsedMilliseconds = 0;
+        while (saving) {
+            if (elapsedMilliseconds >= timeoutMs) {
+                FAIL() << "Timed out after waiting " << timeoutMs << " ms for a pending save";
+            }
+            // This small (0.1 ms) delay is to avoid working too much while waiting for
+            // deferredSaveThread to take the mutex and start the disk write.
+            const int delayMicroseconds = 100;
+            usleep(delayMicroseconds);
+            elapsedMilliseconds += (float)delayMicroseconds / 1000;
+
+            std::lock_guard<std::mutex> lock(cache.mMutex);
+            saving = cache.mSavePending;
+        }
+    }
 };
 
 } /* namespace skiapipeline */
@@ -81,6 +148,18 @@
     return false;
 }
 
+/**
+ * Attempts to delete the given file, and asserts that either:
+ * 1. Deletion was successful, OR
+ * 2. The file did not exist.
+ *
+ * Tip: wrap calls to this in ASSERT_NO_FATAL_FAILURE(x) if a test should exit early if this fails.
+ */
+void deleteFileAssertSuccess(const std::string& filePath) {
+    int deleteResult = remove(filePath.c_str());
+    ASSERT_TRUE(0 == deleteResult || ENOENT == errno);
+}
+
 inline bool checkShader(const sk_sp<SkData>& shader1, const sk_sp<SkData>& shader2) {
     return nullptr != shader1 && nullptr != shader2 && shader1->size() == shader2->size() &&
            0 == memcmp(shader1->data(), shader2->data(), shader1->size());
@@ -91,6 +170,10 @@
     return checkShader(shader, shader2);
 }
 
+inline bool checkShader(const sk_sp<SkData>& shader, const std::string& program) {
+    return checkShader(shader, program.c_str());
+}
+
 template <typename T>
 bool checkShader(const sk_sp<SkData>& shader, std::vector<T>& program) {
     sk_sp<SkData> shader2 = SkData::MakeWithCopy(program.data(), program.size() * sizeof(T));
@@ -101,6 +184,10 @@
     shader = SkData::MakeWithCString(program);
 }
 
+void setShader(sk_sp<SkData>& shader, const std::string& program) {
+    setShader(shader, program.c_str());
+}
+
 template <typename T>
 void setShader(sk_sp<SkData>& shader, std::vector<T>& buffer) {
     shader = SkData::MakeWithCopy(buffer.data(), buffer.size() * sizeof(T));
@@ -124,13 +211,13 @@
     std::string cacheFile2 = getExternalStorageFolder() + "/shaderCacheTest2";
 
     // remove any test files from previous test run
-    int deleteFile = remove(cacheFile1.c_str());
-    ASSERT_TRUE(0 == deleteFile || ENOENT == errno);
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1));
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2));
     std::srand(0);
 
     // read the cache from a file that does not exist
     ShaderCache::get().setFilename(cacheFile1.c_str());
-    ShaderCacheTestUtils::setSaveDelay(ShaderCache::get(), 0);  // disable deferred save
+    ShaderCacheTestUtils::setSaveDelayMs(ShaderCache::get(), 0);  // disable deferred save
     ShaderCache::get().initShaderDiskCache();
 
     // read a key - should not be found since the cache is empty
@@ -184,7 +271,8 @@
     ASSERT_TRUE(checkShader(outVS2, dataBuffer));
 
     ShaderCacheTestUtils::terminate(ShaderCache::get(), false);
-    remove(cacheFile1.c_str());
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1));
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2));
 }
 
 TEST(ShaderCacheTest, testCacheValidation) {
@@ -196,13 +284,13 @@
     std::string cacheFile2 = getExternalStorageFolder() + "/shaderCacheTest2";
 
     // remove any test files from previous test run
-    int deleteFile = remove(cacheFile1.c_str());
-    ASSERT_TRUE(0 == deleteFile || ENOENT == errno);
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1));
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2));
     std::srand(0);
 
     // generate identity and read the cache from a file that does not exist
     ShaderCache::get().setFilename(cacheFile1.c_str());
-    ShaderCacheTestUtils::setSaveDelay(ShaderCache::get(), 0);  // disable deferred save
+    ShaderCacheTestUtils::setSaveDelayMs(ShaderCache::get(), 0);  // disable deferred save
     std::vector<uint8_t> identity(1024);
     genRandomData(identity);
     ShaderCache::get().initShaderDiskCache(
@@ -276,7 +364,81 @@
     }
 
     ShaderCacheTestUtils::terminate(ShaderCache::get(), false);
-    remove(cacheFile1.c_str());
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1));
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2));
+}
+
+using namespace android::uirenderer;
+RENDERTHREAD_SKIA_PIPELINE_TEST(ShaderCacheTest, testOnVkFrameFlushed) {
+    if (Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan) {
+        // RENDERTHREAD_SKIA_PIPELINE_TEST declares both SkiaVK and SkiaGL variants.
+        GTEST_SKIP() << "This test is only applicable to RenderPipelineType::SkiaVulkan";
+    }
+    if (!folderExist(getExternalStorageFolder())) {
+        // Don't run the test if external storage folder is not available
+        return;
+    }
+    std::string cacheFile = getExternalStorageFolder() + "/shaderCacheTest";
+    GrDirectContext* grContext = renderThread.getGrContext();
+
+    // Remove any test files from previous test run
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile));
+
+    // The first iteration of this loop is to save an initial VkPipelineCache data blob to disk,
+    // which sets up the second iteration for a common scenario of comparing a "new" VkPipelineCache
+    // blob passed to "store" against the same blob that's already in the persistent cache from a
+    // previous launch. "reinitializeAllFields" is critical to emulate each iteration being as close
+    // to the state of a freshly launched app as possible, as the initial values of member variables
+    // like mInStoreVkPipelineInProgress and mOldPipelineCacheSize are critical to catch issues
+    // such as b/268205519
+    for (int flushIteration = 1; flushIteration <= 2; flushIteration++) {
+        SCOPED_TRACE("Frame flush iteration " + std::to_string(flushIteration));
+        // Reset *all* in-memory data and reload the cache from disk.
+        ShaderCacheTestUtils::reinitializeAllFields(ShaderCache::get());
+        ShaderCacheTestUtils::setSaveDelayMs(ShaderCache::get(), 10);  // Delay must be > 0 to save.
+        ShaderCache::get().setFilename(cacheFile.c_str());
+        ShaderCache::get().initShaderDiskCache();
+
+        // 1st iteration: store pipeline data to be read back on a subsequent "boot" of the "app".
+        // 2nd iteration: ensure that an initial frame flush (without storing any shaders) given the
+        // same pipeline data that's already on disk doesn't break the cache.
+        ShaderCache::get().onVkFrameFlushed(grContext);
+        ASSERT_NO_FATAL_FAILURE(ShaderCacheTestUtils::waitForPendingSave(ShaderCache::get()));
+    }
+
+    constexpr char shader1[] = "sassas";
+    constexpr char shader2[] = "someVS";
+    constexpr int numIterations = 3;
+    // Also do n iterations of separate "store some shaders then flush the frame" pairs to just
+    // double-check the cache also doesn't get stuck from that use case.
+    for (int saveIteration = 1; saveIteration <= numIterations; saveIteration++) {
+        SCOPED_TRACE("Shader save iteration " + std::to_string(saveIteration));
+        // Write twice to the in-memory cache, which should start a deferred save with both queued.
+        sk_sp<SkData> inVS;
+        setShader(inVS, shader1 + std::to_string(saveIteration));
+        ShaderCache::get().store(GrProgramDescTest(100), *inVS.get(), SkString());
+        setShader(inVS, shader2 + std::to_string(saveIteration));
+        ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString());
+
+        // Simulate flush to also save latest pipeline info.
+        ShaderCache::get().onVkFrameFlushed(grContext);
+        ASSERT_NO_FATAL_FAILURE(ShaderCacheTestUtils::waitForPendingSave(ShaderCache::get()));
+    }
+
+    // Reload from disk to ensure saving succeeded.
+    ShaderCacheTestUtils::terminate(ShaderCache::get(), false);
+    ShaderCache::get().initShaderDiskCache();
+
+    // Read twice, ensure equal to last store.
+    sk_sp<SkData> outVS;
+    ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(100))), sk_sp<SkData>());
+    ASSERT_TRUE(checkShader(outVS, shader1 + std::to_string(numIterations)));
+    ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>());
+    ASSERT_TRUE(checkShader(outVS, shader2 + std::to_string(numIterations)));
+
+    // Clean up.
+    ShaderCacheTestUtils::terminate(ShaderCache::get(), false);
+    ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile));
 }
 
 }  // namespace
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 546f0c6..5ebe5bb 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/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4bc1999..1583a1b 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1337,12 +1337,8 @@
     public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags) {
         Preconditions.checkNotNull(attr, "attr must not be null");
         final IAudioService service = getService();
-        try {
-            service.setVolumeIndexForAttributes(attr, index, flags,
-                    getContext().getOpPackageName(), getContext().getAttributionTag());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        int groupId = getVolumeGroupIdForAttributes(attr);
+        setVolumeGroupVolumeIndex(groupId, index, flags);
     }
 
     /**
@@ -1361,11 +1357,8 @@
     public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
         Preconditions.checkNotNull(attr, "attr must not be null");
         final IAudioService service = getService();
-        try {
-            return service.getVolumeIndexForAttributes(attr);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        int groupId = getVolumeGroupIdForAttributes(attr);
+        return getVolumeGroupVolumeIndex(groupId);
     }
 
     /**
@@ -1382,11 +1375,8 @@
     public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
         Preconditions.checkNotNull(attr, "attr must not be null");
         final IAudioService service = getService();
-        try {
-            return service.getMaxVolumeIndexForAttributes(attr);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        int groupId = getVolumeGroupIdForAttributes(attr);
+        return getVolumeGroupMaxVolumeIndex(groupId);
     }
 
     /**
@@ -1403,8 +1393,168 @@
     public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
         Preconditions.checkNotNull(attr, "attr must not be null");
         final IAudioService service = getService();
+        int groupId = getVolumeGroupIdForAttributes(attr);
+        return getVolumeGroupMinVolumeIndex(groupId);
+    }
+
+    /**
+     * Returns the volume group id associated to the given {@link AudioAttributes}.
+     *
+     * @param attributes The {@link AudioAttributes} to consider.
+     * @return {@link android.media.audiopolicy.AudioVolumeGroup} id supporting the given
+     * {@link AudioAttributes} if found,
+     * {@code android.media.audiopolicy.AudioVolumeGroup.DEFAULT_VOLUME_GROUP} otherwise.
+     * @hide
+     */
+    public int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) {
+        Preconditions.checkNotNull(attributes, "Audio Attributes must not be null");
+        return AudioProductStrategy.getVolumeGroupIdForAudioAttributes(attributes,
+                /* fallbackOnDefault= */ true);
+    }
+
+    /**
+     * Sets the volume index for a particular group associated to given id.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @param index The volume index to set. See
+     *          {@link #getVolumeGroupMaxVolumeIndex(id)} for the largest valid value
+     *          {@link #getVolumeGroupMinVolumeIndex(id)} for the lowest valid value.
+     * @param flags One or more flags.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void setVolumeGroupVolumeIndex(int groupId, int index, int flags) {
+        final IAudioService service = getService();
         try {
-            return service.getMinVolumeIndexForAttributes(attr);
+            service.setVolumeGroupVolumeIndex(groupId, index, flags,
+                    getContext().getOpPackageName(), getContext().getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the current volume index for a particular group associated to given id.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @return The current volume index for the stream.
+     * @hide
+     */
+    @IntRange(from = 0)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public int getVolumeGroupVolumeIndex(int groupId) {
+        final IAudioService service = getService();
+        try {
+            return service.getVolumeGroupVolumeIndex(groupId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the maximum volume index for a particular group associated to given id.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @return The maximum valid volume index for the {@link AudioAttributes}.
+     * @hide
+     */
+    @IntRange(from = 0)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public int getVolumeGroupMaxVolumeIndex(int groupId) {
+        final IAudioService service = getService();
+        try {
+            return service.getVolumeGroupMaxVolumeIndex(groupId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the minimum volume index for a particular group associated to given id.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @return The minimum valid volume index for the {@link AudioAttributes}.
+     * @hide
+     */
+    @IntRange(from = 0)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public int getVolumeGroupMinVolumeIndex(int groupId) {
+        final IAudioService service = getService();
+        try {
+            return service.getVolumeGroupMinVolumeIndex(groupId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adjusts the volume of a particular group associated to given id by one step in a direction.
+     * <p> If the volume group is associated to a stream type, it fallbacks on
+     * {@link AudioManager#adjustStreamVolume()} for compatibility reason.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @param direction The direction to adjust the volume. One of
+     *            {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or
+     *            {@link #ADJUST_SAME}.
+     * @param flags One or more flags.
+     * @throws SecurityException if the adjustment triggers a Do Not Disturb change and the caller
+     * is not granted notification policy access.
+     * @hide
+     */
+    public void adjustVolumeGroupVolume(int groupId, int direction, int flags) {
+        IAudioService service = getService();
+        try {
+            service.adjustVolumeGroupVolume(groupId, direction, flags,
+                    getContext().getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get last audible volume of the group associated to given id before it was muted.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @return current volume if not muted, volume before muted otherwise.
+     * @hide
+     */
+    @RequiresPermission("android.permission.QUERY_AUDIO_STATE")
+    @IntRange(from = 0)
+    public int getLastAudibleVolumeGroupVolume(int groupId) {
+        IAudioService service = getService();
+        try {
+            return service.getLastAudibleVolumeGroupVolume(groupId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the current mute state for a particular volume group associated to the given id.
+     * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+     * id supporting the given {@link AudioAttributes}.
+     *
+     * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+     * @return The mute state for the given {@link android.media.audiopolicy.AudioVolumeGroup} id.
+     * @see #adjustAttributesVolume(AudioAttributes, int, int)
+     * @hide
+     */
+    public boolean isVolumeGroupMuted(int groupId) {
+        IAudioService service = getService();
+        try {
+            return service.isVolumeGroupMuted(groupId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -7720,7 +7870,8 @@
         Objects.requireNonNull(device);
         try {
             if (device.getId() == 0) {
-                throw new IllegalArgumentException("In valid device: " + device);
+                Log.w(TAG, "setCommunicationDevice: device not found: " + device);
+                return false;
             }
             return getService().setCommunicationDevice(mICallBack, device.getId());
         } catch (RemoteException e) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index aa2e0ba..73dbfa8 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -127,14 +127,20 @@
 
     List<AudioVolumeGroup> getAudioVolumeGroups();
 
-    void setVolumeIndexForAttributes(in AudioAttributes aa, int index, int flags,
-            String callingPackage, in String attributionTag);
+    void setVolumeGroupVolumeIndex(int groupId, int index, int flags, String callingPackage,
+            in String attributionTag);
 
-    int getVolumeIndexForAttributes(in AudioAttributes aa);
+    int getVolumeGroupVolumeIndex(int groupId);
 
-    int getMaxVolumeIndexForAttributes(in AudioAttributes aa);
+    int getVolumeGroupMaxVolumeIndex(int groupId);
 
-    int getMinVolumeIndexForAttributes(in AudioAttributes aa);
+    int getVolumeGroupMinVolumeIndex(int groupId);
+
+    int getLastAudibleVolumeGroupVolume(int groupId);
+
+    boolean isVolumeGroupMuted(int groupId);
+
+    void adjustVolumeGroupVolume(int groupId, int direction, int flags, String callingPackage);
 
     int getLastAudibleStreamVolume(int streamType);
 
diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java
index f957498..4b46d1a 100644
--- a/media/java/android/media/audiopolicy/AudioProductStrategy.java
+++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java
@@ -29,11 +29,11 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * @hide
@@ -143,7 +143,7 @@
      */
     public static int getLegacyStreamTypeForStrategyWithAudioAttributes(
             @NonNull AudioAttributes audioAttributes) {
-        Preconditions.checkNotNull(audioAttributes, "AudioAttributes must not be null");
+        Objects.requireNonNull(audioAttributes, "AudioAttributes must not be null");
         for (final AudioProductStrategy productStrategy :
                 AudioProductStrategy.getAudioProductStrategies()) {
             if (productStrategy.supportsAudioAttributes(audioAttributes)) {
@@ -163,6 +163,30 @@
         return AudioSystem.STREAM_MUSIC;
     }
 
+    /**
+     * @hide
+     * @param attributes the {@link AudioAttributes} to identify VolumeGroupId with
+     * @param fallbackOnDefault if set, allows to fallback on the default group (e.g. the group
+     *                          associated to {@link AudioManager#STREAM_MUSIC}).
+     * @return volume group id associated with the given {@link AudioAttributes} if found,
+     *     default volume group id if fallbackOnDefault is set
+     * <p>By convention, the product strategy with default attributes will be associated to the
+     * default volume group (e.g. associated to {@link AudioManager#STREAM_MUSIC})
+     * or {@link AudioVolumeGroup#DEFAULT_VOLUME_GROUP} if not found.
+     */
+    public static int getVolumeGroupIdForAudioAttributes(
+            @NonNull AudioAttributes attributes, boolean fallbackOnDefault) {
+        Objects.requireNonNull(attributes, "attributes must not be null");
+        int volumeGroupId = getVolumeGroupIdForAudioAttributesInt(attributes);
+        if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
+            return volumeGroupId;
+        }
+        if (fallbackOnDefault) {
+            return getVolumeGroupIdForAudioAttributesInt(getDefaultAttributes());
+        }
+        return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+    }
+
     private static List<AudioProductStrategy> initializeAudioProductStrategies() {
         ArrayList<AudioProductStrategy> apsList = new ArrayList<AudioProductStrategy>();
         int status = native_list_audio_product_strategies(apsList);
@@ -193,8 +217,8 @@
      */
     private AudioProductStrategy(@NonNull String name, int id,
             @NonNull AudioAttributesGroup[] aag) {
-        Preconditions.checkNotNull(name, "name must not be null");
-        Preconditions.checkNotNull(aag, "AudioAttributesGroups must not be null");
+        Objects.requireNonNull(name, "name must not be null");
+        Objects.requireNonNull(aag, "AudioAttributesGroups must not be null");
         mName = name;
         mId = id;
         mAudioAttributesGroups = aag;
@@ -212,6 +236,15 @@
 
     /**
      * @hide
+     * @return the product strategy ID (which is the generalisation of Car Audio Usage / legacy
+     *         routing_strategy linked to {@link AudioAttributes#getUsage()}).
+     */
+    @NonNull public String getName() {
+        return mName;
+    }
+
+    /**
+     * @hide
      * @return first {@link AudioAttributes} associated to this product strategy.
      */
     @SystemApi
@@ -244,7 +277,7 @@
      */
     @TestApi
     public int getLegacyStreamTypeForAudioAttributes(@NonNull AudioAttributes aa) {
-        Preconditions.checkNotNull(aa, "AudioAttributes must not be null");
+        Objects.requireNonNull(aa, "AudioAttributes must not be null");
         for (final AudioAttributesGroup aag : mAudioAttributesGroups) {
             if (aag.supportsAttributes(aa)) {
                 return aag.getStreamType();
@@ -261,7 +294,7 @@
      */
     @SystemApi
     public boolean supportsAudioAttributes(@NonNull AudioAttributes aa) {
-        Preconditions.checkNotNull(aa, "AudioAttributes must not be null");
+        Objects.requireNonNull(aa, "AudioAttributes must not be null");
         for (final AudioAttributesGroup aag : mAudioAttributesGroups) {
             if (aag.supportsAttributes(aa)) {
                 return true;
@@ -294,7 +327,7 @@
      */
     @TestApi
     public int getVolumeGroupIdForAudioAttributes(@NonNull AudioAttributes aa) {
-        Preconditions.checkNotNull(aa, "AudioAttributes must not be null");
+        Objects.requireNonNull(aa, "AudioAttributes must not be null");
         for (final AudioAttributesGroup aag : mAudioAttributesGroups) {
             if (aag.supportsAttributes(aa)) {
                 return aag.getVolumeGroupId();
@@ -303,6 +336,17 @@
         return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
     }
 
+    private static int getVolumeGroupIdForAudioAttributesInt(@NonNull AudioAttributes attributes) {
+        Objects.requireNonNull(attributes, "attributes must not be null");
+        for (AudioProductStrategy productStrategy : getAudioProductStrategies()) {
+            int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes);
+            if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
+                return volumeGroupId;
+            }
+        }
+        return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -378,8 +422,8 @@
      */
     private static boolean attributesMatches(@NonNull AudioAttributes refAttr,
             @NonNull AudioAttributes attr) {
-        Preconditions.checkNotNull(refAttr, "refAttr must not be null");
-        Preconditions.checkNotNull(attr, "attr must not be null");
+        Objects.requireNonNull(refAttr, "reference AudioAttributes must not be null");
+        Objects.requireNonNull(attr, "requester's AudioAttributes must not be null");
         String refFormattedTags = TextUtils.join(";", refAttr.getTags());
         String cliFormattedTags = TextUtils.join(";", attr.getTags());
         if (refAttr.equals(DEFAULT_ATTRIBUTES)) {
diff --git a/media/tests/MediaFrameworkTest/AndroidManifest.xml b/media/tests/MediaFrameworkTest/AndroidManifest.xml
index 33872ed..0da5de7 100644
--- a/media/tests/MediaFrameworkTest/AndroidManifest.xml
+++ b/media/tests/MediaFrameworkTest/AndroidManifest.xml
@@ -37,7 +37,6 @@
         </activity>
         <activity android:label="Camera2CtsActivity"
              android:name="Camera2SurfaceViewActivity"
-             android:screenOrientation="landscape"
              android:configChanges="keyboardHidden|orientation|screenSize">
         </activity>
     </application>
diff --git a/packages/AppPredictionLib/Android.bp b/packages/AppPredictionLib/Android.bp
index 5a68fdc..31c1936 100644
--- a/packages/AppPredictionLib/Android.bp
+++ b/packages/AppPredictionLib/Android.bp
@@ -25,7 +25,7 @@
     name: "app_prediction",
 
     sdk_version: "system_current",
-    min_sdk_version: "system_current",
+    min_sdk_version: "current",
 
     srcs: [
         "src/**/*.java",
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 87e61b5..d253dda 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -65,7 +65,7 @@
         "src/**/*.kt",
     ],
 
-    min_sdk_version: "29",
+    min_sdk_version: "30",
 
 }
 
diff --git a/packages/SettingsLib/DeviceStateRotationLock/OWNERS b/packages/SettingsLib/DeviceStateRotationLock/OWNERS
new file mode 100644
index 0000000..091df26
--- /dev/null
+++ b/packages/SettingsLib/DeviceStateRotationLock/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/packages/SettingsLib/src/com/android/settingslib/devicestate/OWNERS
\ No newline at end of file
diff --git a/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml b/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml
new file mode 100644
index 0000000..141886c
--- /dev/null
+++ b/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+
+<resources>
+    <declare-styleable name="IllustrationPreference">
+        <attr name="dynamicColor" format="boolean" />
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 468a976..37ae2d4 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -55,10 +55,25 @@
 
     private int mMaxHeight = SIZE_UNSPECIFIED;
     private int mImageResId;
+    private boolean mCacheComposition = true;
     private boolean mIsAutoScale;
     private Uri mImageUri;
     private Drawable mImageDrawable;
     private View mMiddleGroundView;
+    private OnBindListener mOnBindListener;
+
+    private boolean mLottieDynamicColor;
+
+    /**
+     * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+     */
+    public interface OnBindListener {
+        /**
+         * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+         * @param animationView the animation view for this preference.
+         */
+        void onBind(LottieAnimationView animationView);
+    }
 
     private final Animatable2.AnimationCallback mAnimationCallback =
             new Animatable2.AnimationCallback() {
@@ -119,6 +134,7 @@
         lp.width = screenWidth < screenHeight ? screenWidth : screenHeight;
         illustrationFrame.setLayoutParams(lp);
 
+        illustrationView.setCacheComposition(mCacheComposition);
         handleImageWithAnimation(illustrationView);
         handleImageFrameMaxHeight(backgroundView, illustrationView);
 
@@ -133,6 +149,21 @@
         if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) {
             ColorUtils.applyDynamicColors(getContext(), illustrationView);
         }
+
+        if (mLottieDynamicColor) {
+            LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
+        }
+
+        if (mOnBindListener != null) {
+            mOnBindListener.onBind(illustrationView);
+        }
+    }
+
+    /**
+     * Sets a listener to be notified when the views are binded.
+     */
+    public void setOnBindListener(OnBindListener listener) {
+        mOnBindListener = listener;
     }
 
     /**
@@ -239,6 +270,21 @@
         }
     }
 
+    /**
+     * Sets the lottie illustration apply dynamic color.
+     */
+    public void applyDynamicColor() {
+        mLottieDynamicColor = true;
+        notifyChanged();
+    }
+
+    /**
+     * Return if the lottie illustration apply dynamic color or not.
+     */
+    public boolean isApplyDynamicColor() {
+        return mLottieDynamicColor;
+    }
+
     private void resetImageResourceCache() {
         mImageDrawable = null;
         mImageUri = null;
@@ -380,9 +426,17 @@
 
         mIsAutoScale = false;
         if (attrs != null) {
-            final TypedArray a = context.obtainStyledAttributes(attrs,
+            TypedArray a = context.obtainStyledAttributes(attrs,
                     R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
             mImageResId = a.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0);
+            mCacheComposition = a.getBoolean(
+                    R.styleable.LottieAnimationView_lottie_cacheComposition, true);
+
+            a = context.obtainStyledAttributes(attrs,
+                    R.styleable.IllustrationPreference, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+            mLottieDynamicColor = a.getBoolean(R.styleable.IllustrationPreference_dynamicColor,
+                    false);
+
             a.recycle();
         }
     }
diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml
index 4de23ba..6058190 100644
--- a/packages/SettingsLib/res/values/arrays.xml
+++ b/packages/SettingsLib/res/values/arrays.xml
@@ -690,4 +690,18 @@
     <!-- Content descriptions for each of the images in the avatar_images array. -->
     <string-array name="avatar_image_descriptions"/>
 
+    <string-array name="entries_font_size">
+        <item msgid="6490061470416867723">Small</item>
+        <item msgid="3579015730662088893">Default</item>
+        <item msgid="1678068858001018666">Large</item>
+        <item msgid="490158884605093126">Largest</item>
+    </string-array>
+
+    <string-array name="entryvalues_font_size" translatable="false">
+        <item>0.85</item>
+        <item>1.0</item>
+        <item>1.15</item>
+        <item>1.30</item>
+    </string-array>
+
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
index 44a37f4..d4d2b48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.hardware.display.DisplayManager;
 import android.os.AsyncTask;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -32,6 +33,9 @@
 import com.android.settingslib.R;
 
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
 
 /**
  * Utility methods for working with display density.
@@ -70,120 +74,169 @@
      */
     private static final int MIN_DIMENSION_DP = 320;
 
-    private final String[] mEntries;
-    private final int[] mValues;
+    private static final Predicate<DisplayInfo> INTERNAL_ONLY =
+            (info) -> info.type == Display.TYPE_INTERNAL;
 
-    private final int mDefaultDensity;
-    private final int mCurrentIndex;
+    private final Predicate<DisplayInfo> mPredicate;
+
+    private final DisplayManager mDisplayManager;
+
+    /**
+     * The text description of the density values of the default display.
+     */
+    private String[] mDefaultDisplayDensityEntries;
+
+    /**
+     * The density values of the default display.
+     */
+    private int[] mDefaultDisplayDensityValues;
+
+    /**
+     * The density values, indexed by display unique ID.
+     */
+    private final Map<String, int[]> mValuesPerDisplay = new HashMap();
+
+    private int mDefaultDensityForDefaultDisplay;
+    private int mCurrentIndex = -1;
 
     public DisplayDensityUtils(Context context) {
-        final int defaultDensity = DisplayDensityUtils.getDefaultDisplayDensity(
-                Display.DEFAULT_DISPLAY);
-        if (defaultDensity <= 0) {
-            mEntries = null;
-            mValues = null;
-            mDefaultDensity = 0;
-            mCurrentIndex = -1;
-            return;
-        }
+        this(context, INTERNAL_ONLY);
+    }
 
-        final Resources res = context.getResources();
-        DisplayInfo info = new DisplayInfo();
-        context.getDisplayNoVerify().getDisplayInfo(info);
+    /**
+     * Creates an instance that stores the density values for the displays that satisfy
+     * the predicate.
+     * @param context The context
+     * @param predicate Determines what displays the density should be set for. The default display
+     *                  must satisfy this predicate.
+     */
+    public DisplayDensityUtils(Context context, Predicate predicate) {
+        mPredicate = predicate;
+        mDisplayManager = context.getSystemService(DisplayManager.class);
 
-        final int currentDensity = info.logicalDensityDpi;
-        int currentDensityIndex = -1;
-
-        // Compute number of "larger" and "smaller" scales for this display.
-        final int minDimensionPx = Math.min(info.logicalWidth, info.logicalHeight);
-        final int maxDensity = DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
-        final float maxScaleDimen = context.getResources().getFraction(
-                R.fraction.display_density_max_scale, 1, 1);
-        final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity);
-        final float minScale = context.getResources().getFraction(
-                R.fraction.display_density_min_scale, 1, 1);
-        final float minScaleInterval = context.getResources().getFraction(
-                R.fraction.display_density_min_scale_interval, 1, 1);
-        final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval,
-                0, SUMMARIES_LARGER.length);
-        final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval,
-                0, SUMMARIES_SMALLER.length);
-
-        String[] entries = new String[1 + numSmaller + numLarger];
-        int[] values = new int[entries.length];
-        int curIndex = 0;
-
-        if (numSmaller > 0) {
-            final float interval = (1 - minScale) / numSmaller;
-            for (int i = numSmaller - 1; i >= 0; i--) {
-                // Round down to a multiple of 2 by truncating the low bit.
-                final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
-                if (currentDensity == density) {
-                    currentDensityIndex = curIndex;
-                }
-                entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
-                values[curIndex] = density;
-                curIndex++;
+        for (Display display : mDisplayManager.getDisplays(
+                DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+            DisplayInfo info = new DisplayInfo();
+            if (!display.getDisplayInfo(info)) {
+                Log.w(LOG_TAG, "Cannot fetch display info for display " + display.getDisplayId());
+                continue;
             }
-        }
-
-        if (currentDensity == defaultDensity) {
-            currentDensityIndex = curIndex;
-        }
-        values[curIndex] = defaultDensity;
-        entries[curIndex] = res.getString(SUMMARY_DEFAULT);
-        curIndex++;
-
-        if (numLarger > 0) {
-            final float interval = (maxScale - 1) / numLarger;
-            for (int i = 0; i < numLarger; i++) {
-                // Round down to a multiple of 2 by truncating the low bit.
-                final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
-                if (currentDensity == density) {
-                    currentDensityIndex = curIndex;
+            if (!mPredicate.test(info)) {
+                if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+                    throw new IllegalArgumentException("Predicate must not filter out the default "
+                            + "display.");
                 }
-                values[curIndex] = density;
-                entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
-                curIndex++;
+                continue;
             }
+
+            final int defaultDensity = DisplayDensityUtils.getDefaultDensityForDisplay(
+                    display.getDisplayId());
+            if (defaultDensity <= 0) {
+                Log.w(LOG_TAG, "Cannot fetch default density for display "
+                        + display.getDisplayId());
+                continue;
+            }
+
+            final Resources res = context.getResources();
+
+            final int currentDensity = info.logicalDensityDpi;
+            int currentDensityIndex = -1;
+
+            // Compute number of "larger" and "smaller" scales for this display.
+            final int minDimensionPx = Math.min(info.logicalWidth, info.logicalHeight);
+            final int maxDensity =
+                    DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
+            final float maxScaleDimen = context.getResources().getFraction(
+                    R.fraction.display_density_max_scale, 1, 1);
+            final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity);
+            final float minScale = context.getResources().getFraction(
+                    R.fraction.display_density_min_scale, 1, 1);
+            final float minScaleInterval = context.getResources().getFraction(
+                    R.fraction.display_density_min_scale_interval, 1, 1);
+            final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval,
+                    0, SUMMARIES_LARGER.length);
+            final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval,
+                    0, SUMMARIES_SMALLER.length);
+
+            String[] entries = new String[1 + numSmaller + numLarger];
+            int[] values = new int[entries.length];
+            int curIndex = 0;
+
+            if (numSmaller > 0) {
+                final float interval = (1 - minScale) / numSmaller;
+                for (int i = numSmaller - 1; i >= 0; i--) {
+                    // Round down to a multiple of 2 by truncating the low bit.
+                    final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
+                    if (currentDensity == density) {
+                        currentDensityIndex = curIndex;
+                    }
+                    entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
+                    values[curIndex] = density;
+                    curIndex++;
+                }
+            }
+
+            if (currentDensity == defaultDensity) {
+                currentDensityIndex = curIndex;
+            }
+            values[curIndex] = defaultDensity;
+            entries[curIndex] = res.getString(SUMMARY_DEFAULT);
+            curIndex++;
+
+            if (numLarger > 0) {
+                final float interval = (maxScale - 1) / numLarger;
+                for (int i = 0; i < numLarger; i++) {
+                    // Round down to a multiple of 2 by truncating the low bit.
+                    final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
+                    if (currentDensity == density) {
+                        currentDensityIndex = curIndex;
+                    }
+                    values[curIndex] = density;
+                    entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
+                    curIndex++;
+                }
+            }
+
+            final int displayIndex;
+            if (currentDensityIndex >= 0) {
+                displayIndex = currentDensityIndex;
+            } else {
+                // We don't understand the current density. Must have been set by
+                // someone else. Make room for another entry...
+                int newLength = values.length + 1;
+                values = Arrays.copyOf(values, newLength);
+                values[curIndex] = currentDensity;
+
+                entries = Arrays.copyOf(entries, newLength);
+                entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
+
+                displayIndex = curIndex;
+            }
+
+            if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+                mDefaultDensityForDefaultDisplay = defaultDensity;
+                mCurrentIndex = displayIndex;
+                mDefaultDisplayDensityEntries = entries;
+                mDefaultDisplayDensityValues = values;
+            }
+            mValuesPerDisplay.put(info.uniqueId, values);
         }
-
-        final int displayIndex;
-        if (currentDensityIndex >= 0) {
-            displayIndex = currentDensityIndex;
-        } else {
-            // We don't understand the current density. Must have been set by
-            // someone else. Make room for another entry...
-            int newLength = values.length + 1;
-            values = Arrays.copyOf(values, newLength);
-            values[curIndex] = currentDensity;
-
-            entries = Arrays.copyOf(entries, newLength);
-            entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
-
-            displayIndex = curIndex;
-        }
-
-        mDefaultDensity = defaultDensity;
-        mCurrentIndex = displayIndex;
-        mEntries = entries;
-        mValues = values;
     }
 
-    public String[] getEntries() {
-        return mEntries;
+    public String[] getDefaultDisplayDensityEntries() {
+        return mDefaultDisplayDensityEntries;
     }
 
-    public int[] getValues() {
-        return mValues;
+    public int[] getDefaultDisplayDensityValues() {
+        return mDefaultDisplayDensityValues;
     }
 
-    public int getCurrentIndex() {
+    public int getCurrentIndexForDefaultDisplay() {
         return mCurrentIndex;
     }
 
-    public int getDefaultDensity() {
-        return mDefaultDensity;
+    public int getDefaultDensityForDefaultDisplay() {
+        return mDefaultDensityForDefaultDisplay;
     }
 
     /**
@@ -193,7 +246,7 @@
      * @return the default density of the specified display, or {@code -1} if
      *         the display does not exist or the density could not be obtained
      */
-    private static int getDefaultDisplayDensity(int displayId) {
+    private static int getDefaultDensityForDisplay(int displayId) {
        try {
            final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
            return wm.getInitialDisplayDensity(displayId);
@@ -203,19 +256,31 @@
     }
 
     /**
-     * Asynchronously applies display density changes to the specified display.
+     * Asynchronously applies display density changes to the displays that satisfy the predicate.
      * <p>
      * The change will be applied to the user specified by the value of
      * {@link UserHandle#myUserId()} at the time the method is called.
-     *
-     * @param displayId the identifier of the display to modify
      */
-    public static void clearForcedDisplayDensity(final int displayId) {
+    public void clearForcedDisplayDensity() {
         final int userId = UserHandle.myUserId();
         AsyncTask.execute(() -> {
             try {
-                final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-                wm.clearForcedDisplayDensityForUser(displayId, userId);
+                for (Display display : mDisplayManager.getDisplays(
+                        DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+                    int displayId = display.getDisplayId();
+                    DisplayInfo info = new DisplayInfo();
+                    if (!display.getDisplayInfo(info)) {
+                        Log.w(LOG_TAG, "Unable to clear forced display density setting "
+                                + "for display " + displayId);
+                        continue;
+                    }
+                    if (!mPredicate.test(info)) {
+                        continue;
+                    }
+
+                    final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    wm.clearForcedDisplayDensityForUser(displayId, userId);
+                }
             } catch (RemoteException exc) {
                 Log.w(LOG_TAG, "Unable to clear forced display density setting");
             }
@@ -223,20 +288,39 @@
     }
 
     /**
-     * Asynchronously applies display density changes to the specified display.
+     * Asynchronously applies display density changes to the displays that satisfy the predicate.
      * <p>
      * The change will be applied to the user specified by the value of
      * {@link UserHandle#myUserId()} at the time the method is called.
      *
-     * @param displayId the identifier of the display to modify
-     * @param density the density to force for the specified display
+     * @param index The index of the density value
      */
-    public static void setForcedDisplayDensity(final int displayId, final int density) {
+    public void setForcedDisplayDensity(final int index) {
         final int userId = UserHandle.myUserId();
         AsyncTask.execute(() -> {
             try {
-                final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-                wm.setForcedDisplayDensityForUser(displayId, density, userId);
+                for (Display display : mDisplayManager.getDisplays(
+                        DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+                    int displayId = display.getDisplayId();
+                    DisplayInfo info = new DisplayInfo();
+                    if (!display.getDisplayInfo(info)) {
+                        Log.w(LOG_TAG, "Unable to save forced display density setting "
+                                + "for display " + displayId);
+                        continue;
+                    }
+                    if (!mPredicate.test(info)) {
+                        continue;
+                    }
+                    if (!mValuesPerDisplay.containsKey(info.uniqueId)) {
+                        Log.w(LOG_TAG, "Unable to save forced display density setting "
+                                + "for display " + info.uniqueId);
+                        continue;
+                    }
+
+                    final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    wm.setForcedDisplayDensityForUser(displayId,
+                            mValuesPerDisplay.get(info.uniqueId)[index], userId);
+                }
             } catch (RemoteException exc) {
                 Log.w(LOG_TAG, "Unable to save forced display density setting");
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 2614644..688fc72 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -55,6 +55,7 @@
         public ComponentName settingsComponentName;
         public CharSequence description;
         public Drawable previewImage;
+        public boolean supportsComplications = false;
 
         @Override
         public String toString() {
@@ -175,6 +176,7 @@
             if (dreamMetadata != null) {
                 dreamInfo.settingsComponentName = dreamMetadata.settingsActivity;
                 dreamInfo.previewImage = dreamMetadata.previewImage;
+                dreamInfo.supportsComplications = dreamMetadata.showComplications;
             }
             dreamInfos.add(dreamInfo);
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 7ec0fcd..d2e615a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -190,6 +190,10 @@
         return !isGroup;
     }
 
+    boolean preferRouteListingOrdering() {
+        return false;
+    }
+
     /**
      * Remove a {@code device} from current media.
      *
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index f4355c3..7458f010 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -213,6 +213,15 @@
     }
 
     /**
+     * Returns if media app establishes a preferred route listing order.
+     *
+     * @return True if route list ordering exist and not using system ordering, false otherwise.
+     */
+    public boolean isPreferenceRouteListingExist() {
+        return mInfoMediaManager.preferRouteListingOrdering();
+    }
+
+    /**
      * Start scan connected MediaDevice
      */
     public void startScan() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index c829bc3..d9d7cc9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -184,6 +184,33 @@
      */
     public abstract String getId();
 
+    /**
+     * Get disabled reason of device
+     *
+     * @return disabled reason of device
+     */
+    public int getDisableReason() {
+        return -1;
+    }
+
+    /**
+     * Checks if device is has disabled reason
+     *
+     * @return true if device has disabled reason
+     */
+    public boolean hasDisabledReason() {
+        return false;
+    }
+
+    /**
+     * Checks if device is suggested device from application
+     *
+     * @return true if device is suggested device
+     */
+    public boolean isSuggestedDevice() {
+        return false;
+    }
+
     void setConnectedRecord() {
         mConnectedRecord++;
         ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(),
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
index 6a1cee3..562d20d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
@@ -224,6 +224,9 @@
                     mMetricsLogger.logOnConditionSelected();
                     updateAlarmWarningText(tag.condition);
                 }
+                tag.line1.setStateDescription(
+                        isChecked ? buttonView.getContext().getString(
+                                com.android.internal.R.string.selected) : null);
             }
         });
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java
index 87e97b1..abbdaa7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java
@@ -196,6 +196,9 @@
                 if (isChecked) {
                     tag.rb.setChecked(true);
                 }
+                tag.line1.setStateDescription(
+                        isChecked ? buttonView.getContext().getString(
+                                com.android.internal.R.string.selected) : null);
             }
         });
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/EnableZenModeDialogTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/EnableZenModeDialogTest.java
index 59d5674..6b81c1a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/EnableZenModeDialogTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/EnableZenModeDialogTest.java
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.notification;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -78,6 +80,8 @@
         mController.mForeverId =  Condition.newId(mContext).appendPath("forever").build();
         when(mContext.getString(com.android.internal.R.string.zen_mode_forever))
                 .thenReturn("testSummary");
+        when(mContext.getString(com.android.internal.R.string.selected))
+                .thenReturn("selected");
         NotificationManager.Policy alarmsEnabledPolicy = new NotificationManager.Policy(
                 NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS, 0, 0, 0);
         doReturn(alarmsEnabledPolicy).when(mNotificationManager).getNotificationPolicy();
@@ -190,4 +194,25 @@
         // alarm warning should NOT be null
         assertNotNull(mController.computeAlarmWarningText(null));
     }
+
+    @Test
+    public void testAccessibility() {
+        mController.bindConditions(null);
+        EnableZenModeDialog.ConditionTag forever = mController.getConditionTagAt(
+                ZenDurationDialog.FOREVER_CONDITION_INDEX);
+        EnableZenModeDialog.ConditionTag countdown = mController.getConditionTagAt(
+                ZenDurationDialog.COUNTDOWN_CONDITION_INDEX);
+        EnableZenModeDialog.ConditionTag alwaysAsk = mController.getConditionTagAt(
+                ZenDurationDialog.ALWAYS_ASK_CONDITION_INDEX);
+
+        forever.rb.setChecked(true);
+        assertThat(forever.line1.getStateDescription().toString()).isEqualTo("selected");
+        assertThat(countdown.line1.getStateDescription()).isNull();
+        assertThat(alwaysAsk.line1.getStateDescription()).isNull();
+
+        alwaysAsk.rb.setChecked(true);
+        assertThat(forever.line1.getStateDescription()).isNull();
+        assertThat(countdown.line1.getStateDescription()).isNull();
+        assertThat(alwaysAsk.line1.getStateDescription().toString()).isEqualTo("selected");
+    }
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/ZenDurationDialogTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/ZenDurationDialogTest.java
index 437c0d4..fc45e89 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/ZenDurationDialogTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/ZenDurationDialogTest.java
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.notification;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -205,4 +207,27 @@
                 ZenDurationDialog.COUNTDOWN_CONDITION_INDEX);
         assertEquals(120, tag.countdownZenDuration);
     }
+
+    @Test
+    public void testAccessibility() {
+        Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION,
+                Settings.Secure.ZEN_DURATION_FOREVER);
+        mController.setupDialog(mBuilder);
+        ZenDurationDialog.ConditionTag forever = mController.getConditionTagAt(
+                ZenDurationDialog.FOREVER_CONDITION_INDEX);
+        ZenDurationDialog.ConditionTag countdown = mController.getConditionTagAt(
+                ZenDurationDialog.COUNTDOWN_CONDITION_INDEX);
+        ZenDurationDialog.ConditionTag alwaysAsk = mController.getConditionTagAt(
+                ZenDurationDialog.ALWAYS_ASK_CONDITION_INDEX);
+
+        forever.rb.setChecked(true);
+        assertThat(forever.line1.getStateDescription().toString()).isEqualTo("selected");
+        assertThat(countdown.line1.getStateDescription()).isNull();
+        assertThat(alwaysAsk.line1.getStateDescription()).isNull();
+
+        alwaysAsk.rb.setChecked(true);
+        assertThat(forever.line1.getStateDescription()).isNull();
+        assertThat(countdown.line1.getStateDescription()).isNull();
+        assertThat(alwaysAsk.line1.getStateDescription().toString()).isEqualTo("selected");
+    }
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 29549d9..21e119a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -61,6 +61,8 @@
     private PreferenceViewHolder mViewHolder;
     private FrameLayout mMiddleGroundLayout;
     private final Context mContext = ApplicationProvider.getApplicationContext();
+    private IllustrationPreference.OnBindListener mOnBindListener;
+    private LottieAnimationView mOnBindListenerAnimationView;
 
     @Before
     public void setUp() {
@@ -82,6 +84,12 @@
 
         final AttributeSet attributeSet = Robolectric.buildAttributeSet().build();
         mPreference = new IllustrationPreference(mContext, attributeSet);
+        mOnBindListener = new IllustrationPreference.OnBindListener() {
+            @Override
+            public void onBind(LottieAnimationView animationView) {
+                mOnBindListenerAnimationView = animationView;
+            }
+        };
     }
 
     @Test
@@ -186,4 +194,41 @@
         assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight);
         assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight);
     }
+
+    @Test
+    public void setOnBindListener_isNotified() {
+        mOnBindListenerAnimationView = null;
+        mPreference.setOnBindListener(mOnBindListener);
+
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mOnBindListenerAnimationView).isNotNull();
+        assertThat(mOnBindListenerAnimationView).isEqualTo(mAnimationView);
+    }
+
+    @Test
+    public void setOnBindListener_notNotified() {
+        mOnBindListenerAnimationView = null;
+        mPreference.setOnBindListener(null);
+
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mOnBindListenerAnimationView).isNull();
+    }
+
+    @Test
+    public void onBindViewHolder_default_shouldNotApplyDynamicColor() {
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mPreference.isApplyDynamicColor()).isFalse();
+    }
+
+    @Test
+    public void onBindViewHolder_applyDynamicColor_shouldReturnTrue() {
+        mPreference.applyDynamicColor();
+
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mPreference.isApplyDynamicColor()).isTrue();
+    }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
new file mode 100644
index 0000000..bdd4869
--- /dev/null
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.provider.settings.backup;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+/** Settings that should not be restored when target device is a large screen
+ *  i.e. tablets and foldables in unfolded state
+ */
+public class LargeScreenSettings {
+    private static final float LARGE_SCREEN_MIN_DPS = 600;
+    private static final String LARGE_SCREEN_DO_NOT_RESTORE = "accelerometer_rotation";
+
+   /**
+    * Autorotation setting should not be restored when the target device is a large screen.
+    * (b/243489549)
+    */
+    public static boolean doNotRestoreIfLargeScreenSetting(String key, Context context) {
+        return isLargeScreen(context) && LARGE_SCREEN_DO_NOT_RESTORE.equals(key);
+    }
+
+    // copied from systemui/shared/...Utilities.java
+    // since we don't want to add compile time dependency on sys ui package
+    private static boolean isLargeScreen(Context context) {
+        final WindowManager windowManager = context.getSystemService(WindowManager.class);
+        final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
+        float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
+                context.getResources().getConfiguration().densityDpi);
+        return smallestWidth >= LARGE_SCREEN_MIN_DPS;
+    }
+
+    private static float dpiFromPx(float size, int densityDpi) {
+        float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+        return (size / densityRatio);
+    }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 1b0b6b4..b5eaa4b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -123,13 +123,15 @@
         Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW,
         Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
         Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME,
-        Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
+        Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
         Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
         Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
         Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
         Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS,
         Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
         Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
+        Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+        Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD,
         Settings.Secure.VR_DISPLAY_MODE,
         Settings.Secure.NOTIFICATION_BADGING,
         Settings.Secure.NOTIFICATION_DISMISS_RTL,
@@ -218,6 +220,7 @@
         Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED,
         Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
         Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE,
-        Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME
+        Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME,
+        Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 4fa490f..534e31a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -178,7 +178,7 @@
         VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
                 NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR);
-        VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.SFPS_PERFORMANT_AUTH_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR);
@@ -189,6 +189,10 @@
         VALIDATORS.put(Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO, ANY_STRING_VALIDATOR);
         VALIDATORS.put(Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
                 ANY_STRING_VALIDATOR);
+        VALIDATORS.put(Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+                ANY_STRING_VALIDATOR);
+        VALIDATORS.put(Secure.ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD,
+                ANY_STRING_VALIDATOR);
         VALIDATORS.put(Secure.ASSIST_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ASSIST_GESTURE_SILENCE_ALERTS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ASSIST_GESTURE_WAKE_ENABLED, BOOLEAN_VALIDATOR);
@@ -350,5 +354,6 @@
         VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, ANY_STRING_VALIDATOR);
         VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_CODE, ANY_STRING_VALIDATOR);
         VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, ANY_STRING_VALIDATOR);
+        VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 6aa08f2..574fd5a 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -37,6 +37,7 @@
 import android.provider.Settings;
 import android.provider.settings.backup.DeviceSpecificSettings;
 import android.provider.settings.backup.GlobalSettings;
+import android.provider.settings.backup.LargeScreenSettings;
 import android.provider.settings.backup.SecureSettings;
 import android.provider.settings.backup.SystemSettings;
 import android.provider.settings.validators.GlobalSettingsValidators;
@@ -812,6 +813,12 @@
                 continue;
             }
 
+            if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
+                Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
+                        + "is a large screen (i.e tablet or foldable in unfolded state)");
+                continue;
+            }
+
             String value = null;
             boolean hasValueToRestore = false;
             if (cachedEntries.indexOfKey(key) >= 0) {
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index f13d961..afcd345 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -356,6 +356,9 @@
     <!-- Permission needed to test wallpaper dimming -->
     <uses-permission android:name="android.permission.SET_WALLPAPER_DIM_AMOUNT" />
 
+    <!-- Permission needed to test wallpaper read methods -->
+    <uses-permission android:name="android.permission.READ_WALLPAPER_INTERNAL" />
+
     <!-- Permission required to test ContentResolver caching. -->
     <uses-permission android:name="android.permission.CACHE_CONTENT" />
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cd0fbea..eb5a15f 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: [
@@ -271,6 +310,7 @@
         "LowLightDreamLib",
         "motion_tool_lib",
         "androidx.core_core-animation-testing-nodeps",
+        "androidx.compose.ui_ui",
     ],
 }
 
@@ -317,8 +357,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",
     ],
@@ -332,6 +380,9 @@
     certificate: "platform",
     privileged: true,
     resource_dirs: [],
+    kotlincflags: ["-Xjvm-default=all"],
+
+    plugins: ["dagger2-compiler"],
 }
 
 android_robolectric_test {
@@ -339,6 +390,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",
@@ -346,7 +404,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 810dd33..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" />
@@ -292,7 +293,7 @@
 
     <queries>
         <intent>
-            <action android:name="android.intent.action.NOTES" />
+            <action android:name="android.intent.action.CREATE_NOTE" />
         </intent>
     </queries>
 
@@ -411,7 +412,6 @@
 
         <service android:name=".screenshot.ScreenshotCrossProfileService"
                  android:permission="com.android.systemui.permission.SELF"
-                 android:process=":screenshot_cross_profile"
                  android:exported="false" />
 
         <service android:name=".screenrecord.RecordingService" />
@@ -664,6 +664,21 @@
             android:excludeFromRecents="true"
             android:exported="true" />
 
+        <!-- started from Telecomm(CallsManager) -->
+        <activity
+            android:name=".telephony.ui.activity.SwitchToManagedProfileForCallActivity"
+            android:excludeFromRecents="true"
+            android:exported="true"
+            android:finishOnCloseSystemDialogs="true"
+            android:permission="android.permission.MODIFY_PHONE_STATE"
+            android:theme="@style/Theme.SystemUI.Dialog.Alert">
+            <intent-filter>
+                <action android:name="android.telecom.action.SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="tel" />
+            </intent-filter>
+        </activity>
+
         <!-- platform logo easter egg activity -->
         <activity
             android:name=".DessertCase"
@@ -885,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>
@@ -908,7 +923,7 @@
             android:excludeFromRecents="true"
             android:theme="@android:style/Theme.NoDisplay"
             android:label="@string/note_task_button_label"
-            android:icon="@drawable/ic_note_task_button">
+            android:icon="@drawable/ic_note_task_shortcut_widget">
 
             <intent-filter>
                 <action android:name="android.intent.action.CREATE_SHORTCUT" />
diff --git a/packages/SystemUI/README.md b/packages/SystemUI/README.md
index ee8d023..2910bba 100644
--- a/packages/SystemUI/README.md
+++ b/packages/SystemUI/README.md
@@ -5,46 +5,72 @@
 SystemUI is a persistent process that provides UI for the system but outside
 of the system_server process.
 
-The starting point for most of sysui code is a list of services that extend
-SystemUI that are started up by SystemUIApplication. These services then depend
-on some custom dependency injection provided by Dependency.
-
 Inputs directed at sysui (as opposed to general listeners) generally come in
 through IStatusBar. Outputs from sysui are through a variety of private APIs to
 the android platform all over.
 
 ## SystemUIApplication
 
-When SystemUIApplication starts up, it will start up the services listed in
-config_systemUIServiceComponents or config_systemUIServiceComponentsPerUser.
+When SystemUIApplication starts up, it instantiates a Dagger graph from which
+various pieces of the application are built.
 
-Each of these services extend SystemUI. SystemUI provides them with a Context
-and gives them callbacks for onConfigurationChanged (this historically was
-the main path for onConfigurationChanged, now also happens through
-ConfigurationController). They also receive a callback for onBootCompleted
+To support customization, SystemUIApplication relies on the AndroidManifest.xml
+having an `android.app.AppComponentFactory` specified. Specifically, it relies
+on an `AppComponentFactory` that subclases `SystemUIAppComponentFactoryBase`.
+Implementations of this abstract base class must override
+`#createSystemUIInitializer(Context)` which returns a `SystemUIInitializer`.
+`SystemUIInitializer` primary job in turn is to intialize and return the Dagger
+root component back to the `SystemUIApplication`.
+
+Writing a custom `SystemUIAppComponentFactoryBase` and `SystemUIInitializer`,
+should be enough for most implementations to stand up a customized Dagger
+graph, and launch a custom version of SystemUI.
+
+## Dagger / Dependency Injection
+
+See [dagger.md](docs/dagger.md) and https://dagger.dev/.
+
+## CoreStartable
+
+The starting point for most of SystemUI code is a list of classes that
+implement `CoreStartable` that are started up by SystemUIApplication.
+CoreStartables are like miniature services. They have their `#start` method
+called after being instantiated, and a reference to them is stored inside
+SystemUIApplication. They are in charge of their own behavior beyond this,
+registering and unregistering with the rest of the system as needed.
+
+`CoreStartable` also receives a callback for `#onBootCompleted`
 since these objects may be started before the device has finished booting.
 
-Each SystemUI service is expected to be a major part of system ui and the
-goal is to minimize communication between them. So in general they should be
-relatively silo'd.
+`CoreStartable` is an ideal place to add new features and functionality
+that does not belong directly under the umbrella of an existing feature.
+It is better to define a new `CoreStartable` than to stick unrelated
+initialization code together in catch-all methods.
 
-## Dependencies
+CoreStartables are tied to application startup via Dagger:
 
-The first SystemUI service that is started should always be Dependency.
-Dependency provides a static method for getting a hold of dependencies that
-have a lifecycle that spans sysui. Dependency has code for how to create all
-dependencies manually added. SystemUIFactory is also capable of
-adding/replacing these dependencies.
+```kotlin
+class FeatureStartable
+@Inject
+constructor(
+    /* ... */
+) : CoreStartable {
+    override fun start() {
+        // ...
+    }
+}
 
-Dependencies are lazily initialized, so if a Dependency is never referenced at
-runtime, it will never be created.
+@Module
+abstract class FeatureModule {
+    @Binds
+    @IntoMap
+    @ClassKey(FeatureStartable::class)
+    abstract fun bind(impl: FeatureStartable): CoreStartable
+}
+```
 
-If an instantiated dependency implements Dumpable it will be included in dumps
-of sysui (and bug reports), allowing it to include current state information.
-This is how \*Controllers dump state to bug reports.
-
-If an instantiated dependency implements ConfigurationChangeReceiver it will
-receive onConfigurationChange callbacks when the configuration changes.
+Including `FeatureModule` in the Dagger graph such as this will ensure that
+`FeatureStartable` gets constructed and that its `#start` method is called.
 
 ## IStatusBar
 
@@ -64,12 +90,6 @@
 This is generally used a shortcut to directly trigger CommandQueue rather than
 calling StatusManager and waiting for the call to come back to IStatusBar.
 
-## Default SystemUI services list
-
-### [com.android.systemui.Dependency](/packages/SystemUI/src/com/android/systemui/Dependency.java)
-
-Provides custom dependency injection.
-
 ### [com.android.systemui.util.NotificationChannels](/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java)
 
 Creates/initializes the channels sysui uses when posting notifications.
@@ -88,11 +108,11 @@
 Registers all the callbacks/listeners required to show the Volume dialog when
 it should be shown.
 
-### [com.android.systemui.status.phone.StatusBar](/packages/SystemUI/src/com/android/systemui/status/phone/StatusBar.java)
+### [com.android.systemui.status.phone.CentralSurfaces](/packages/SystemUI/src/com/android/systemui/status/phone/CentralSurfaces.java)
 
 This shows the UI for the status bar and the notification shade it contains.
 It also contains a significant amount of other UI that interacts with these
-surfaces (keyguard, AOD, etc.). StatusBar also contains a notification listener
+surfaces (keyguard, AOD, etc.). CentralSurfaces also contains a notification listener
 to receive notification callbacks.
 
 ### [com.android.systemui.usb.StorageNotification](/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java)
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index cd45b8ea..60bfdb2 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -16,6 +16,9 @@
         },
         {
             "exclude-annotation": "android.platform.test.annotations.Postsubmit"
+        },
+        {
+            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
         }
       ]
     }
@@ -72,16 +75,16 @@
   // Curious where your @Scenario tests will run?
   //
   // @Ignore: nowhere
-  // @Staging or @FlakyTest: in staged-postsubmit, but not postsubmit or
-  // 	presubmit
+  // @FlakyTest: in staged-postsubmit, but not blocking postsubmit or
+  // presubmit
   // @Postsubmit: in postsubmit and staged-postsubmit, but not presubmit
   // none of the above: in presubmit, postsubmit, and staged-postsubmit
   //
-  // Therefore, please annotate new tests with @Staging, then with @Postsubmit
-  // once they're ready for postsubmit, then with neither once they're ready
-  // for presubmit.
+  // Ideally, please annotate new tests with @FlakyTest, then with @Postsubmit
+  // once they're ready for postsubmit as they will immediately block go/android-platinum,
+  // then with neither once they're ready for presubmit.
   //
-  // If you don't use @Staging or @Postsubmit, your new test will immediately
+  // If you don't use @Postsubmit, your new test will immediately
   // block presubmit, which is probably not what you want!
   "sysui-platinum-postsubmit": [
     {
@@ -98,6 +101,9 @@
         },
         {
             "exclude-annotation": "androidx.test.filters.FlakyTest"
+        },
+        {
+            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
         }
       ]
     }
@@ -130,6 +136,9 @@
         },
         {
             "include-filter": "android.platform.test.scenario.sysui"
+        },
+        {
+            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
         }
       ]
     }
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 8acc2f8..978ab5d 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -35,7 +35,6 @@
     ],
 
     static_libs: [
-        "PluginCoreLib",
         "androidx.core_core-animation-nodeps",
         "androidx.core_core-ktx",
         "androidx.annotation_annotation",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index fe349f2..17a94b86 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -37,8 +37,11 @@
 import android.view.WindowManager
 import android.view.animation.Interpolator
 import android.view.animation.PathInterpolator
+import androidx.annotation.BinderThread
+import androidx.annotation.UiThread
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
+import java.lang.IllegalArgumentException
 import kotlin.math.roundToInt
 
 private const val TAG = "ActivityLaunchAnimator"
@@ -226,7 +229,7 @@
         // If we expect an animation, post a timeout to cancel it in case the remote animation is
         // never started.
         if (willAnimate) {
-            runner.postTimeout()
+            runner.delegate.postTimeout()
 
             // Hide the keyguard using the launch animation instead of the default unlock animation.
             if (hideKeyguardWithAnimation) {
@@ -299,10 +302,13 @@
 
     interface Callback {
         /** Whether we are currently on the keyguard or not. */
-        fun isOnKeyguard(): Boolean
+        @JvmDefault fun isOnKeyguard(): Boolean = false
 
         /** Hide the keyguard and animate using [runner]. */
-        fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner)
+        @JvmDefault
+        fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) {
+            throw UnsupportedOperationException()
+        }
 
         /* Get the background color of [task]. */
         fun getBackgroundColor(task: TaskInfo): Int
@@ -333,13 +339,24 @@
              * Return a [Controller] that will animate and expand [view] into the opening window.
              *
              * Important: The view must be attached to a [ViewGroup] when calling this function and
-             * during the animation. For safety, this method will return null when it is not.
+             * during the animation. For safety, this method will return null when it is not. The
+             * view must also implement [LaunchableView], otherwise this method will throw.
              *
              * Note: The background of [view] should be a (rounded) rectangle so that it can be
              * properly animated.
              */
             @JvmStatic
             fun fromView(view: View, cujType: Int? = null): Controller? {
+                // Make sure the View we launch from implements LaunchableView to avoid visibility
+                // issues.
+                if (view !is LaunchableView) {
+                    throw IllegalArgumentException(
+                        "An ActivityLaunchAnimator.Controller was created from a View that does " +
+                            "not implement LaunchableView. This can lead to subtle bugs where the" +
+                            " visibility of the View we are launching from is not what we expected."
+                    )
+                }
+
                 if (view.parent !is ViewGroup) {
                     Log.e(
                         TAG,
@@ -389,14 +406,51 @@
         fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {}
     }
 
-    class Runner(
+    @VisibleForTesting
+    inner class Runner(
+        controller: Controller,
+        callback: Callback,
+        /** The animator to use to animate the window launch. */
+        launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+        /** Listener for animation lifecycle events. */
+        listener: Listener? = null
+    ) : IRemoteAnimationRunner.Stub() {
+        private val context = controller.launchContainer.context
+        internal val delegate: AnimationDelegate
+
+        init {
+            delegate = AnimationDelegate(controller, callback, launchAnimator, listener)
+        }
+
+        @BinderThread
+        override fun onAnimationStart(
+            transit: Int,
+            apps: Array<out RemoteAnimationTarget>?,
+            wallpapers: Array<out RemoteAnimationTarget>?,
+            nonApps: Array<out RemoteAnimationTarget>?,
+            finishedCallback: IRemoteAnimationFinishedCallback?
+        ) {
+            context.mainExecutor.execute {
+                delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback)
+            }
+        }
+
+        @BinderThread
+        override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {
+            context.mainExecutor.execute { delegate.onAnimationCancelled(isKeyguardOccluded) }
+        }
+    }
+
+    class AnimationDelegate
+    @JvmOverloads
+    constructor(
         private val controller: Controller,
         private val callback: Callback,
         /** The animator to use to animate the window launch. */
         private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
         /** Listener for animation lifecycle events. */
         private val listener: Listener? = null
-    ) : IRemoteAnimationRunner.Stub() {
+    ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
         private val launchContainer = controller.launchContainer
         private val context = launchContainer.context
         private val transactionApplierView =
@@ -419,6 +473,7 @@
         // posting it.
         private var onTimeout = Runnable { onAnimationTimedOut() }
 
+        @UiThread
         internal fun postTimeout() {
             launchContainer.postDelayed(onTimeout, LAUNCH_TIMEOUT)
         }
@@ -427,19 +482,20 @@
             launchContainer.removeCallbacks(onTimeout)
         }
 
+        @UiThread
         override fun onAnimationStart(
             @WindowManager.TransitionOldType transit: Int,
             apps: Array<out RemoteAnimationTarget>?,
             wallpapers: Array<out RemoteAnimationTarget>?,
             nonApps: Array<out RemoteAnimationTarget>?,
-            iCallback: IRemoteAnimationFinishedCallback?
+            callback: IRemoteAnimationFinishedCallback?
         ) {
             removeTimeout()
 
             // The animation was started too late and we already notified the controller that it
             // timed out.
             if (timedOut) {
-                iCallback?.invoke()
+                callback?.invoke()
                 return
             }
 
@@ -449,7 +505,7 @@
                 return
             }
 
-            context.mainExecutor.execute { startAnimation(apps, nonApps, iCallback) }
+            startAnimation(apps, nonApps, callback)
         }
 
         private fun startAnimation(
@@ -687,6 +743,7 @@
             controller.onLaunchAnimationCancelled()
         }
 
+        @UiThread
         override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {
             if (timedOut) {
                 return
@@ -695,10 +752,9 @@
             Log.i(TAG, "Remote animation was cancelled")
             cancelled = true
             removeTimeout()
-            context.mainExecutor.execute {
-                animation?.cancel()
-                controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded)
-            }
+
+            animation?.cancel()
+            controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded)
         }
 
         private fun IRemoteAnimationFinishedCallback.invoke() {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
new file mode 100644
index 0000000..1c9dabb
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
@@ -0,0 +1,6 @@
+package com.android.systemui.animation
+
+interface AnimationFeatureFlags {
+    val isPredictiveBackQsDialogAnim: Boolean
+        get() = false
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 0f81b0b..42a8636 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -35,6 +35,8 @@
 import android.widget.FrameLayout
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CujType
+import com.android.systemui.util.registerAnimationOnBackInvoked
+import java.lang.IllegalArgumentException
 import kotlin.math.roundToInt
 
 private const val TAG = "DialogLaunchAnimator"
@@ -55,8 +57,9 @@
 constructor(
     private val callback: Callback,
     private val interactionJankMonitor: InteractionJankMonitor,
+    private val featureFlags: AnimationFeatureFlags,
     private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
-    private val isForTesting: Boolean = false
+    private val isForTesting: Boolean = false,
 ) {
     private companion object {
         private val TIMINGS = ActivityLaunchAnimator.TIMINGS
@@ -151,12 +154,23 @@
              * Create a [Controller] that can animate [source] to and from a dialog.
              *
              * Important: The view must be attached to a [ViewGroup] when calling this function and
-             * during the animation. For safety, this method will return null when it is not.
+             * during the animation. For safety, this method will return null when it is not. The
+             * view must also implement [LaunchableView], otherwise this method will throw.
              *
              * Note: The background of [view] should be a (rounded) rectangle so that it can be
              * properly animated.
              */
             fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
+                // Make sure the View we launch from implements LaunchableView to avoid visibility
+                // issues.
+                if (source !is LaunchableView) {
+                    throw IllegalArgumentException(
+                        "A DialogLaunchAnimator.Controller was created from a View that does not " +
+                            "implement LaunchableView. This can lead to subtle bugs where the " +
+                            "visibility of the View we are launching from is not what we expected."
+                    )
+                }
+
                 if (source.parent !is ViewGroup) {
                     Log.e(
                         TAG,
@@ -237,25 +251,12 @@
             openedDialogs.firstOrNull {
                 it.dialog.window.decorView.viewRootImpl == controller.viewRoot
             }
-        val animateFrom =
+        val controller =
             animatedParent?.dialogContentWithBackground?.let {
                 Controller.fromView(it, controller.cuj)
             }
                 ?: controller
 
-        if (animatedParent == null && animateFrom !is LaunchableView) {
-            // Make sure the View we launch from implements LaunchableView to avoid visibility
-            // issues. Given that we don't own dialog decorViews so we can't enforce it for launches
-            // from a dialog.
-            // TODO(b/243636422): Throw instead of logging to enforce this.
-            Log.w(
-                TAG,
-                "A dialog was launched from a View that does not implement LaunchableView. This " +
-                    "can lead to subtle bugs where the visibility of the View we are " +
-                    "launching from is not what we expected."
-            )
-        }
-
         // Make sure we don't run the launch animation from the same source twice at the same time.
         if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
             Log.e(
@@ -269,15 +270,16 @@
 
         val animatedDialog =
             AnimatedDialog(
-                launchAnimator,
-                callback,
-                interactionJankMonitor,
-                animateFrom,
+                launchAnimator = launchAnimator,
+                callback = callback,
+                interactionJankMonitor = interactionJankMonitor,
+                controller = controller,
                 onDialogDismissed = { openedDialogs.remove(it) },
                 dialog = dialog,
-                animateBackgroundBoundsChange,
-                animatedParent,
-                isForTesting,
+                animateBackgroundBoundsChange = animateBackgroundBoundsChange,
+                parentAnimatedDialog = animatedParent,
+                forceDisableSynchronization = isForTesting,
+                featureFlags = featureFlags,
             )
 
         openedDialogs.add(animatedDialog)
@@ -298,10 +300,16 @@
     ) {
         val view =
             openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
-                ?: throw IllegalStateException(
-                    "The animateFrom dialog was not animated using " +
-                        "DialogLaunchAnimator.showFrom(View|Dialog)"
-                )
+        if (view == null) {
+            Log.w(
+                TAG,
+                "Showing dialog $dialog normally as the dialog it is shown from was not shown " +
+                    "using DialogLaunchAnimator"
+            )
+            dialog.show()
+            return
+        }
+
         showFromView(
             dialog,
             view,
@@ -366,7 +374,7 @@
         val dialog = animatedDialog.dialog
 
         // Don't animate if the dialog is not showing or if we are locked and going to show the
-        // bouncer.
+        // primary bouncer.
         if (
             !dialog.isShowing ||
                 (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
@@ -513,6 +521,7 @@
      * Whether synchronization should be disabled, which can be useful if we are running in a test.
      */
     private val forceDisableSynchronization: Boolean,
+    private val featureFlags: AnimationFeatureFlags,
 ) {
     /**
      * The DecorView of this dialog window.
@@ -601,10 +610,16 @@
                 }
 
                 // Animate that view with the background. Throw if we didn't find one, because
-                // otherwise
-                // it's not clear what we should animate.
+                // otherwise it's not clear what we should animate.
+                if (viewGroupWithBackground == null) {
+                    error("Unable to find ViewGroup with background")
+                }
+
+                if (viewGroupWithBackground !is LaunchableView) {
+                    error("The animated ViewGroup with background must implement LaunchableView")
+                }
+
                 viewGroupWithBackground
-                    ?: throw IllegalStateException("Unable to find ViewGroup with background")
             } else {
                 // We will make the dialog window (and therefore its DecorView) fullscreen to make
                 // it possible to animate outside its bounds.
@@ -627,7 +642,7 @@
                     FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                 )
 
-                val dialogContentWithBackground = FrameLayout(dialog.context)
+                val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
                 dialogContentWithBackground.background = decorView.background
 
                 // Make the window background transparent. Note that setting the window (or
@@ -708,7 +723,10 @@
 
         // Make the background view invisible until we start the animation. We use the transition
         // visibility like GhostView does so that we don't mess up with the accessibility tree (see
-        // b/204944038#comment17).
+        // b/204944038#comment17). Given that this background implements LaunchableView, we call
+        // setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is
+        // restored at the end of the animation.
+        dialogContentWithBackground.setShouldBlockVisibilityChanges(true)
         dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
 
         // Make sure the dialog is visible instantly and does not do any window animation.
@@ -774,9 +792,13 @@
         // the dialog.
         dialog.setDismissOverride(this::onDialogDismissed)
 
+        if (featureFlags.isPredictiveBackQsDialogAnim) {
+            // TODO(b/265923095) Improve animations for QS dialogs on configuration change
+            dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
+        }
+
         // Show the dialog.
         dialog.show()
-
         moveSourceDrawingToDialog()
     }
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
index 3d341af..2903288 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
@@ -33,9 +33,7 @@
 private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
 private const val FONT_ITALIC_DEFAULT_VALUE = 0f
 
-/**
- * Provide interpolation of two fonts by adjusting font variation settings.
- */
+/** Provide interpolation of two fonts by adjusting font variation settings. */
 class FontInterpolator {
 
     /**
@@ -61,11 +59,14 @@
         var index: Int,
         val sortedAxes: MutableList<FontVariationAxis>
     ) {
-        constructor(font: Font, axes: List<FontVariationAxis>) :
-                this(font.sourceIdentifier,
-                        font.ttcIndex,
-                        axes.toMutableList().apply { sortBy { it.tag } }
-                )
+        constructor(
+            font: Font,
+            axes: List<FontVariationAxis>
+        ) : this(
+            font.sourceIdentifier,
+            font.ttcIndex,
+            axes.toMutableList().apply { sortBy { it.tag } }
+        )
 
         fun set(font: Font, axes: List<FontVariationAxis>) {
             sourceId = font.sourceIdentifier
@@ -86,9 +87,7 @@
     private val tmpInterpKey = InterpKey(null, null, 0f)
     private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf())
 
-    /**
-     * Linear interpolate the font variation settings.
-     */
+    /** Linear interpolate the font variation settings. */
     fun lerp(start: Font, end: Font, progress: Float): Font {
         if (progress == 0f) {
             return start
@@ -115,27 +114,34 @@
         // this doesn't take much time since the variation axes is usually up to 5. If we need to
         // support more number of axes, we may want to preprocess the font and store the sorted axes
         // and also pre-fill the missing axes value with default value from 'fvar' table.
-        val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue ->
-            when (tag) {
-                // TODO: Good to parse 'fvar' table for retrieving default value.
-                TAG_WGHT -> adjustWeight(
-                        MathUtils.lerp(
+        val newAxes =
+            lerp(startAxes, endAxes) { tag, startValue, endValue ->
+                when (tag) {
+                    // TODO: Good to parse 'fvar' table for retrieving default value.
+                    TAG_WGHT ->
+                        adjustWeight(
+                            MathUtils.lerp(
                                 startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
                                 endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
-                                progress))
-                TAG_ITAL -> adjustItalic(
-                        MathUtils.lerp(
+                                progress
+                            )
+                        )
+                    TAG_ITAL ->
+                        adjustItalic(
+                            MathUtils.lerp(
                                 startValue ?: FONT_ITALIC_DEFAULT_VALUE,
                                 endValue ?: FONT_ITALIC_DEFAULT_VALUE,
-                                progress))
-                else -> {
-                    require(startValue != null && endValue != null) {
-                        "Unable to interpolate due to unknown default axes value : $tag"
+                                progress
+                            )
+                        )
+                    else -> {
+                        require(startValue != null && endValue != null) {
+                            "Unable to interpolate due to unknown default axes value : $tag"
+                        }
+                        MathUtils.lerp(startValue, endValue, progress)
                     }
-                    MathUtils.lerp(startValue, endValue, progress)
                 }
             }
-        }
 
         // Check if we already make font for this axes. This is typically happens if the animation
         // happens backward.
@@ -149,9 +155,7 @@
         // This is the first time to make the font for the axes. Build and store it to the cache.
         // Font.Builder#build won't throw IOException since creating fonts from existing fonts will
         // not do any IO work.
-        val newFont = Font.Builder(start)
-                .setFontVariationSettings(newAxes.toTypedArray())
-                .build()
+        val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
         interpCache[InterpKey(start, end, progress)] = newFont
         verFontCache[VarFontKey(start, newAxes)] = newFont
         return newFont
@@ -173,26 +177,28 @@
             val tagA = if (i < start.size) start[i].tag else null
             val tagB = if (j < end.size) end[j].tag else null
 
-            val comp = when {
-                tagA == null -> 1
-                tagB == null -> -1
-                else -> tagA.compareTo(tagB)
-            }
+            val comp =
+                when {
+                    tagA == null -> 1
+                    tagB == null -> -1
+                    else -> tagA.compareTo(tagB)
+                }
 
-            val axis = when {
-                comp == 0 -> {
-                    val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue)
-                    FontVariationAxis(tagA, v)
+            val axis =
+                when {
+                    comp == 0 -> {
+                        val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue)
+                        FontVariationAxis(tagA, v)
+                    }
+                    comp < 0 -> {
+                        val v = filter(tagA!!, start[i++].styleValue, null)
+                        FontVariationAxis(tagA, v)
+                    }
+                    else -> { // comp > 0
+                        val v = filter(tagB!!, null, end[j++].styleValue)
+                        FontVariationAxis(tagB, v)
+                    }
                 }
-                comp < 0 -> {
-                    val v = filter(tagA!!, start[i++].styleValue, null)
-                    FontVariationAxis(tagA, v)
-                }
-                else -> { // comp > 0
-                    val v = filter(tagB!!, null, end[j++].styleValue)
-                    FontVariationAxis(tagB, v)
-                }
-            }
 
             result.add(axis)
         }
@@ -202,21 +208,21 @@
     // For the performance reasons, we animate weight with FONT_WEIGHT_ANIMATION_STEP. This helps
     // Cache hit ratio in the Skia glyph cache.
     private fun adjustWeight(value: Float) =
-            coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP)
+        coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP)
 
     // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps
     // Cache hit ratio in the Skia glyph cache.
     private fun adjustItalic(value: Float) =
-            coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP)
+        coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP)
 
     private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) =
-            (v.coerceIn(min, max) / step).toInt() * step
+        (v.coerceIn(min, max) / step).toInt() * step
 
     companion object {
         private val EMPTY_AXES = arrayOf<FontVariationAxis>()
 
         // Returns true if given two font instance can be interpolated.
         fun canInterpolate(start: Font, end: Font) =
-                start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
+            start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index dfac02d..23e3a01 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -26,6 +26,7 @@
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.InsetDrawable
 import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.StateListDrawable
 import android.util.Log
 import android.view.GhostView
 import android.view.View
@@ -33,8 +34,10 @@
 import android.view.ViewGroupOverlay
 import android.widget.FrameLayout
 import com.android.internal.jank.InteractionJankMonitor
+import java.lang.IllegalArgumentException
 import java.util.LinkedList
 import kotlin.math.min
+import kotlin.math.roundToInt
 
 private const val TAG = "GhostedViewLaunchAnimatorController"
 
@@ -44,12 +47,15 @@
  * of the ghosted view.
  *
  * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
- * the animation.
+ * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
+ * during this controller instantiation.
  *
  * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
  * whenever possible instead.
  */
-open class GhostedViewLaunchAnimatorController @JvmOverloads constructor(
+open class GhostedViewLaunchAnimatorController
+@JvmOverloads
+constructor(
     /** The view that will be ghosted and from which the background will be extracted. */
     private val ghostedView: View,
 
@@ -97,6 +103,15 @@
     private val background: Drawable?
 
     init {
+        // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
+        if (ghostedView !is LaunchableView) {
+            throw IllegalArgumentException(
+                "A GhostedViewLaunchAnimatorController was created from a View that does not " +
+                    "implement LaunchableView. This can lead to subtle bugs where the visibility " +
+                    "of the View we are launching from is not what we expected."
+            )
+        }
+
         /** Find the first view with a background in [view] and its children. */
         fun findBackground(view: View): Drawable? {
             if (view.background != null) {
@@ -145,7 +160,8 @@
         val gradient = findGradientDrawable(drawable) ?: return 0f
 
         // TODO(b/184121838): Support more than symmetric top & bottom radius.
-        return gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
+        val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
+        return radius * ghostedView.scaleX
     }
 
     /** Return the current bottom corner radius of the background. */
@@ -154,7 +170,8 @@
         val gradient = findGradientDrawable(drawable) ?: return 0f
 
         // TODO(b/184121838): Support more than symmetric top & bottom radius.
-        return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
+        val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
+        return radius * ghostedView.scaleX
     }
 
     override fun createAnimatorState(): LaunchAnimator.State {
@@ -173,9 +190,13 @@
         ghostedView.getLocationOnScreen(ghostedViewLocation)
         val insets = backgroundInsets
         state.top = ghostedViewLocation[1] + insets.top
-        state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom
+        state.bottom =
+            ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() -
+                insets.bottom
         state.left = ghostedViewLocation[0] + insets.left
-        state.right = ghostedViewLocation[0] + ghostedView.width - insets.right
+        state.right =
+            ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() -
+                insets.right
     }
 
     override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -341,6 +362,10 @@
                 }
             }
 
+            if (drawable is StateListDrawable) {
+                return findGradientDrawable(drawable.current)
+            }
+
             return null
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
copy to packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
index 7bbfec7..2eb503b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
@@ -12,37 +12,35 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
-package com.android.systemui.common.ui.view
+package com.android.systemui.animation
 
 import android.content.Context
 import android.util.AttributeSet
-import android.widget.ImageView
-import com.android.systemui.animation.LaunchableView
-import com.android.systemui.animation.LaunchableViewDelegate
+import android.widget.FrameLayout
 
-class LaunchableImageView : ImageView, LaunchableView {
+/** A [FrameLayout] that also implements [LaunchableView]. */
+open class LaunchableFrameLayout : FrameLayout, LaunchableView {
     private val delegate =
         LaunchableViewDelegate(
             this,
             superSetVisibility = { super.setVisibility(it) },
         )
 
-    constructor(context: Context?) : super(context)
-    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+    constructor(context: Context) : super(context)
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
     constructor(
-        context: Context?,
+        context: Context,
         attrs: AttributeSet?,
-        defStyleAttr: Int,
+        defStyleAttr: Int
     ) : super(context, attrs, defStyleAttr)
 
     constructor(
-        context: Context?,
+        context: Context,
         attrs: AttributeSet?,
         defStyleAttr: Int,
-        defStyleRes: Int,
+        defStyleRes: Int
     ) : super(context, attrs, defStyleAttr, defStyleRes)
 
     override fun setShouldBlockVisibilityChanges(block: Boolean) {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
index 774255b..b98b9221 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
@@ -35,8 +35,7 @@
      *
      * Note that calls to [View.setTransitionVisibility] shouldn't be blocked.
      *
-     * @param block whether we should block/postpone all calls to `setVisibility` and
-     * `setTransitionVisibility`.
+     * @param block whether we should block/postpone all calls to `setVisibility`.
      */
     fun setShouldBlockVisibilityChanges(block: Boolean)
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
new file mode 100644
index 0000000..337408b
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
@@ -0,0 +1,30 @@
+package com.android.systemui.animation
+
+import android.annotation.UiThread
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.RemoteAnimationTarget
+import android.view.WindowManager
+
+/**
+ * A component capable of running remote animations.
+ *
+ * Expands the IRemoteAnimationRunner API by allowing for different types of more specialized
+ * callbacks.
+ */
+interface RemoteAnimationDelegate<in T : IRemoteAnimationFinishedCallback> {
+    /**
+     * Called on the UI thread when the animation targets are received. Sets up and kicks off the
+     * animation.
+     */
+    @UiThread
+    fun onAnimationStart(
+        @WindowManager.TransitionOldType transit: Int,
+        apps: Array<out RemoteAnimationTarget>?,
+        wallpapers: Array<out RemoteAnimationTarget>?,
+        nonApps: Array<out RemoteAnimationTarget>?,
+        callback: T?
+    )
+
+    /** Called on the UI thread when a signal is received to cancel the animation. */
+    @UiThread fun onAnimationCancelled(isKeyguardOccluded: Boolean)
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 5f1bb83..a08b598 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -23,6 +23,7 @@
 import android.graphics.Canvas
 import android.graphics.Typeface
 import android.graphics.fonts.Font
+import android.graphics.fonts.FontVariationAxis
 import android.text.Layout
 import android.util.SparseArray
 
@@ -36,8 +37,8 @@
  * Currently this class can provide text style animation for text weight and text size. For example
  * the simple view that draws text with animating text size is like as follows:
  *
- * <pre>
- * <code>
+ * <pre> <code>
+ * ```
  *     class SimpleTextAnimation : View {
  *         @JvmOverloads constructor(...)
  *
@@ -53,83 +54,63 @@
  *             animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate)
  *         }
  *     }
- * </code>
- * </pre>
+ * ```
+ * </code> </pre>
  */
-class TextAnimator(
-    layout: Layout,
-    private val invalidateCallback: () -> Unit
-) {
+class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
     // Following two members are for mutable for testing purposes.
     public var textInterpolator: TextInterpolator = TextInterpolator(layout)
-    public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply {
-        duration = DEFAULT_ANIMATION_DURATION
-        addUpdateListener {
-            textInterpolator.progress = it.animatedValue as Float
-            invalidateCallback()
-        }
-        addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) {
-                textInterpolator.rebase()
+    public var animator: ValueAnimator =
+        ValueAnimator.ofFloat(1f).apply {
+            duration = DEFAULT_ANIMATION_DURATION
+            addUpdateListener {
+                textInterpolator.progress = it.animatedValue as Float
+                invalidateCallback()
             }
-            override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
-        })
-    }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        textInterpolator.rebase()
+                    }
+                    override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
+                }
+            )
+        }
 
     sealed class PositionedGlyph {
 
-        /**
-         * Mutable X coordinate of the glyph position relative from drawing offset.
-         */
+        /** Mutable X coordinate of the glyph position relative from drawing offset. */
         var x: Float = 0f
 
-        /**
-         * Mutable Y coordinate of the glyph position relative from the baseline.
-         */
+        /** Mutable Y coordinate of the glyph position relative from the baseline. */
         var y: Float = 0f
 
-        /**
-         * The current line of text being drawn, in a multi-line TextView.
-         */
+        /** The current line of text being drawn, in a multi-line TextView. */
         var lineNo: Int = 0
 
-        /**
-         * Mutable text size of the glyph in pixels.
-         */
+        /** Mutable text size of the glyph in pixels. */
         var textSize: Float = 0f
 
-        /**
-         * Mutable color of the glyph.
-         */
+        /** Mutable color of the glyph. */
         var color: Int = 0
 
-        /**
-         * Immutable character offset in the text that the current font run start.
-         */
+        /** Immutable character offset in the text that the current font run start. */
         abstract var runStart: Int
             protected set
 
-        /**
-         * Immutable run length of the font run.
-         */
+        /** Immutable run length of the font run. */
         abstract var runLength: Int
             protected set
 
-        /**
-         * Immutable glyph index of the font run.
-         */
+        /** Immutable glyph index of the font run. */
         abstract var glyphIndex: Int
             protected set
 
-        /**
-         * Immutable font instance for this font run.
-         */
+        /** Immutable font instance for this font run. */
         abstract var font: Font
             protected set
 
-        /**
-         * Immutable glyph ID for this glyph.
-         */
+        /** Immutable glyph ID for this glyph. */
         abstract var glyphId: Int
             protected set
     }
@@ -147,20 +128,18 @@
     /**
      * GlyphFilter applied just before drawing to canvas for tweaking positions and text size.
      *
-     * This callback is called for each glyphs just before drawing the glyphs. This function will
-     * be called with the intrinsic position, size, color, glyph ID and font instance. You can
-     * mutate the position, size and color for tweaking animations.
-     * Do not keep the reference of passed glyph object. The interpolator reuses that object for
-     * avoiding object allocations.
+     * This callback is called for each glyphs just before drawing the glyphs. This function will be
+     * called with the intrinsic position, size, color, glyph ID and font instance. You can mutate
+     * the position, size and color for tweaking animations. Do not keep the reference of passed
+     * glyph object. The interpolator reuses that object for avoiding object allocations.
      *
-     * Details:
-     * The text is drawn with font run units. The font run is a text segment that draws with the
-     * same font. The {@code runStart} and {@code runLimit} is a range of the font run in the text
-     * that current glyph is in. Once the font run is determined, the system will convert characters
-     * into glyph IDs. The {@code glyphId} is the glyph identifier in the font and
-     * {@code glyphIndex} is the offset of the converted glyph array. Please note that the
-     * {@code glyphIndex} is not a character index, because the character will not be converted to
-     * glyph one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be
+     * Details: The text is drawn with font run units. The font run is a text segment that draws
+     * with the same font. The {@code runStart} and {@code runLimit} is a range of the font run in
+     * the text that current glyph is in. Once the font run is determined, the system will convert
+     * characters into glyph IDs. The {@code glyphId} is the glyph identifier in the font and {@code
+     * glyphIndex} is the offset of the converted glyph array. Please note that the {@code
+     * glyphIndex} is not a character index, because the character will not be converted to glyph
+     * one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be
      * composed from multiple characters.
      *
      * Here is an example of font runs: "fin. 終わり"
@@ -193,7 +172,9 @@
      */
     var glyphFilter: GlyphCallback?
         get() = textInterpolator.glyphFilter
-        set(value) { textInterpolator.glyphFilter = value }
+        set(value) {
+            textInterpolator.glyphFilter = value
+        }
 
     fun draw(c: Canvas) = textInterpolator.draw(c)
 
@@ -208,7 +189,7 @@
      * @param weight an optional text weight.
      * @param textSize an optional font size.
      * @param colors an optional colors array that must be the same size as numLines passed to
-     *  the TextInterpolator
+     *               the TextInterpolator
      * @param animate an optional boolean indicating true for showing style transition as animation,
      *                false for immediate style transition. True by default.
      * @param duration an optional animation duration in milliseconds. This is ignored if animate is
@@ -235,11 +216,39 @@
             textInterpolator.targetPaint.textSize = textSize
         }
         if (weight >= 0) {
-            // Paint#setFontVariationSettings creates Typeface instance from scratch. To reduce the
-            // memory impact, cache the typeface result.
-            textInterpolator.targetPaint.typeface = typefaceCache.getOrElse(weight) {
-                textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
-                textInterpolator.targetPaint.typeface
+            val fontVariationArray =
+                    FontVariationAxis.fromFontVariationSettings(
+                        textInterpolator.targetPaint.fontVariationSettings
+                    )
+            if (fontVariationArray.isNullOrEmpty()) {
+                textInterpolator.targetPaint.typeface =
+                    typefaceCache.getOrElse(weight) {
+                        textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
+                        textInterpolator.targetPaint.typeface
+                    }
+            } else {
+                val idx = fontVariationArray.indexOfFirst { it.tag == "$TAG_WGHT" }
+                if (idx == -1) {
+                    val updatedFontVariation =
+                        textInterpolator.targetPaint.fontVariationSettings + ",'$TAG_WGHT' $weight"
+                    textInterpolator.targetPaint.typeface =
+                        typefaceCache.getOrElse(weight) {
+                            textInterpolator.targetPaint.fontVariationSettings =
+                                    updatedFontVariation
+                            textInterpolator.targetPaint.typeface
+                        }
+                } else {
+                    fontVariationArray[idx] = FontVariationAxis(
+                            "$TAG_WGHT", weight.toFloat())
+                    val updatedFontVariation =
+                            FontVariationAxis.toFontVariationSettings(fontVariationArray)
+                    textInterpolator.targetPaint.typeface =
+                        typefaceCache.getOrElse(weight) {
+                            textInterpolator.targetPaint.fontVariationSettings =
+                                    updatedFontVariation
+                            textInterpolator.targetPaint.typeface
+                        }
+                }
             }
         }
         if (color != null) {
@@ -249,22 +258,24 @@
 
         if (animate) {
             animator.startDelay = delay
-            animator.duration = if (duration == -1L) {
-                DEFAULT_ANIMATION_DURATION
-            } else {
-                duration
-            }
+            animator.duration =
+                if (duration == -1L) {
+                    DEFAULT_ANIMATION_DURATION
+                } else {
+                    duration
+                }
             interpolator?.let { animator.interpolator = it }
             if (onAnimationEnd != null) {
-                val listener = object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
-                        onAnimationEnd.run()
-                        animator.removeListener(this)
+                val listener =
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator?) {
+                            onAnimationEnd.run()
+                            animator.removeListener(this)
+                        }
+                        override fun onAnimationCancel(animation: Animator?) {
+                            animator.removeListener(this)
+                        }
                     }
-                    override fun onAnimationCancel(animation: Animator?) {
-                        animator.removeListener(this)
-                    }
-                }
                 animator.addListener(listener)
             }
             animator.start()
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
index 0448c81..341784e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
@@ -26,12 +26,8 @@
 import com.android.internal.graphics.ColorUtils
 import java.lang.Math.max
 
-/**
- * Provide text style linear interpolation for plain text.
- */
-class TextInterpolator(
-    layout: Layout
-) {
+/** Provide text style linear interpolation for plain text. */
+class TextInterpolator(layout: Layout) {
 
     /**
      * Returns base paint used for interpolation.
@@ -64,12 +60,11 @@
         var baseFont: Font,
         var targetFont: Font
     ) {
-        val length: Int get() = end - start
+        val length: Int
+            get() = end - start
     }
 
-    /**
-     * A class represents text layout of a single run.
-     */
+    /** A class represents text layout of a single run. */
     private class Run(
         val glyphIds: IntArray,
         val baseX: FloatArray, // same length as glyphIds
@@ -79,12 +74,8 @@
         val fontRuns: List<FontRun>
     )
 
-    /**
-     * A class represents text layout of a single line.
-     */
-    private class Line(
-        val runs: List<Run>
-    )
+    /** A class represents text layout of a single line. */
+    private class Line(val runs: List<Run>)
 
     private var lines = listOf<Line>()
     private val fontInterpolator = FontInterpolator()
@@ -106,8 +97,8 @@
     /**
      * The layout used for drawing text.
      *
-     * Only non-styled text is supported. Even if the given layout is created from Spanned, the
-     * span information is not used.
+     * Only non-styled text is supported. Even if the given layout is created from Spanned, the span
+     * information is not used.
      *
      * The paint objects used for interpolation are not changed by this method call.
      *
@@ -122,6 +113,9 @@
             shapeText(value)
         }
 
+    var shapedText: String = ""
+        private set
+
     init {
         // shapeText needs to be called after all members are initialized.
         shapeText(layout)
@@ -130,8 +124,8 @@
     /**
      * Recalculate internal text layout for interpolation.
      *
-     * Whenever the target paint is modified, call this method to recalculate internal
-     * text layout used for interpolation.
+     * Whenever the target paint is modified, call this method to recalculate internal text layout
+     * used for interpolation.
      */
     fun onTargetPaintModified() {
         updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false)
@@ -140,8 +134,8 @@
     /**
      * Recalculate internal text layout for interpolation.
      *
-     * Whenever the base paint is modified, call this method to recalculate internal
-     * text layout used for interpolation.
+     * Whenever the base paint is modified, call this method to recalculate internal text layout
+     * used for interpolation.
      */
     fun onBasePaintModified() {
         updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true)
@@ -152,11 +146,11 @@
      *
      * The text interpolator does not calculate all the text position by text shaper due to
      * performance reasons. Instead, the text interpolator shape the start and end state and
-     * calculate text position of the middle state by linear interpolation. Due to this trick,
-     * the text positions of the middle state is likely different from the text shaper result.
-     * So, if you want to start animation from the middle state, you will see the glyph jumps due to
-     * this trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different
-     * from text shape result of weight 550.
+     * calculate text position of the middle state by linear interpolation. Due to this trick, the
+     * text positions of the middle state is likely different from the text shaper result. So, if
+     * you want to start animation from the middle state, you will see the glyph jumps due to this
+     * trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different from
+     * text shape result of weight 550.
      *
      * After calling this method, do not call onBasePaintModified() since it reshape the text and
      * update the base state. As in above notice, the text shaping result at current progress is
@@ -168,8 +162,8 @@
      * animate weight from 200 to 400, then if you want to move back to 200 at the half of the
      * animation, it will look like
      *
-     * <pre>
-     * <code>
+     * <pre> <code>
+     * ```
      *     val interp = TextInterpolator(layout)
      *
      *     // Interpolate between weight 200 to 400.
@@ -199,9 +193,8 @@
      *         // progress is 0.5
      *         animator.start()
      *     }
-     * </code>
-     * </pre>
-     *
+     * ```
+     * </code> </pre>
      */
     fun rebase() {
         if (progress == 0f) {
@@ -263,69 +256,73 @@
         }
 
         var maxRunLength = 0
-        lines = baseLayout.zip(targetLayout) { baseLine, targetLine ->
-            val runs = baseLine.zip(targetLine) { base, target ->
-
-                require(base.glyphCount() == target.glyphCount()) {
-                    "Inconsistent glyph count at line ${lines.size}"
-                }
-
-                val glyphCount = base.glyphCount()
-
-                // Good to recycle the array if the existing array can hold the new layout result.
-                val glyphIds = IntArray(glyphCount) {
-                    base.getGlyphId(it).also { baseGlyphId ->
-                        require(baseGlyphId == target.getGlyphId(it)) {
-                            "Inconsistent glyph ID at $it in line ${lines.size}"
+        lines =
+            baseLayout.zip(targetLayout) { baseLine, targetLine ->
+                val runs =
+                    baseLine.zip(targetLine) { base, target ->
+                        require(base.glyphCount() == target.glyphCount()) {
+                            "Inconsistent glyph count at line ${lines.size}"
                         }
-                    }
-                }
 
-                val baseX = FloatArray(glyphCount) { base.getGlyphX(it) }
-                val baseY = FloatArray(glyphCount) { base.getGlyphY(it) }
-                val targetX = FloatArray(glyphCount) { target.getGlyphX(it) }
-                val targetY = FloatArray(glyphCount) { target.getGlyphY(it) }
+                        val glyphCount = base.glyphCount()
 
-                // Calculate font runs
-                val fontRun = mutableListOf<FontRun>()
-                if (glyphCount != 0) {
-                    var start = 0
-                    var baseFont = base.getFont(start)
-                    var targetFont = target.getFont(start)
-                    require(FontInterpolator.canInterpolate(baseFont, targetFont)) {
-                        "Cannot interpolate font at $start ($baseFont vs $targetFont)"
-                    }
-
-                    for (i in 1 until glyphCount) {
-                        val nextBaseFont = base.getFont(i)
-                        val nextTargetFont = target.getFont(i)
-
-                        if (baseFont !== nextBaseFont) {
-                            require(targetFont !== nextTargetFont) {
-                                "Base font has changed at $i but target font has not changed."
+                        // Good to recycle the array if the existing array can hold the new layout
+                        // result.
+                        val glyphIds =
+                            IntArray(glyphCount) {
+                                base.getGlyphId(it).also { baseGlyphId ->
+                                    require(baseGlyphId == target.getGlyphId(it)) {
+                                        "Inconsistent glyph ID at $it in line ${lines.size}"
+                                    }
+                                }
                             }
-                            // Font transition point. push run and reset context.
-                            fontRun.add(FontRun(start, i, baseFont, targetFont))
-                            maxRunLength = max(maxRunLength, i - start)
-                            baseFont = nextBaseFont
-                            targetFont = nextTargetFont
-                            start = i
+
+                        val baseX = FloatArray(glyphCount) { base.getGlyphX(it) }
+                        val baseY = FloatArray(glyphCount) { base.getGlyphY(it) }
+                        val targetX = FloatArray(glyphCount) { target.getGlyphX(it) }
+                        val targetY = FloatArray(glyphCount) { target.getGlyphY(it) }
+
+                        // Calculate font runs
+                        val fontRun = mutableListOf<FontRun>()
+                        if (glyphCount != 0) {
+                            var start = 0
+                            var baseFont = base.getFont(start)
+                            var targetFont = target.getFont(start)
                             require(FontInterpolator.canInterpolate(baseFont, targetFont)) {
                                 "Cannot interpolate font at $start ($baseFont vs $targetFont)"
                             }
-                        } else { // baseFont === nextBaseFont
-                            require(targetFont === nextTargetFont) {
-                                "Base font has not changed at $i but target font has changed."
+
+                            for (i in 1 until glyphCount) {
+                                val nextBaseFont = base.getFont(i)
+                                val nextTargetFont = target.getFont(i)
+
+                                if (baseFont !== nextBaseFont) {
+                                    require(targetFont !== nextTargetFont) {
+                                        "Base font has changed at $i but target font is unchanged."
+                                    }
+                                    // Font transition point. push run and reset context.
+                                    fontRun.add(FontRun(start, i, baseFont, targetFont))
+                                    maxRunLength = max(maxRunLength, i - start)
+                                    baseFont = nextBaseFont
+                                    targetFont = nextTargetFont
+                                    start = i
+                                    require(FontInterpolator.canInterpolate(baseFont, targetFont)) {
+                                        "Cannot interpolate font at $start" +
+                                            " ($baseFont vs $targetFont)"
+                                    }
+                                } else { // baseFont === nextBaseFont
+                                    require(targetFont === nextTargetFont) {
+                                        "Base font is unchanged at $i but target font has changed."
+                                    }
+                                }
                             }
+                            fontRun.add(FontRun(start, glyphCount, baseFont, targetFont))
+                            maxRunLength = max(maxRunLength, glyphCount - start)
                         }
+                        Run(glyphIds, baseX, baseY, targetX, targetY, fontRun)
                     }
-                    fontRun.add(FontRun(start, glyphCount, baseFont, targetFont))
-                    maxRunLength = max(maxRunLength, glyphCount - start)
-                }
-                Run(glyphIds, baseX, baseY, targetX, targetY, fontRun)
+                Line(runs)
             }
-            Line(runs)
-        }
 
         // Update float array used for drawing.
         if (tmpPositionArray.size < maxRunLength * 2) {
@@ -357,9 +354,9 @@
         if (glyphFilter == null) {
             for (i in run.start until run.end) {
                 tmpPositionArray[arrayIndex++] =
-                        MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
+                    MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
                 tmpPositionArray[arrayIndex++] =
-                        MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
+                    MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
             }
             c.drawGlyphs(line.glyphIds, run.start, tmpPositionArray, 0, run.length, font, paint)
             return
@@ -388,13 +385,14 @@
                 tmpPaintForGlyph.color = tmpGlyph.color
 
                 c.drawGlyphs(
-                        line.glyphIds,
-                        prevStart,
-                        tmpPositionArray,
-                        0,
-                        i - prevStart,
-                        font,
-                        tmpPaintForGlyph)
+                    line.glyphIds,
+                    prevStart,
+                    tmpPositionArray,
+                    0,
+                    i - prevStart,
+                    font,
+                    tmpPaintForGlyph
+                )
                 prevStart = i
                 arrayIndex = 0
             }
@@ -404,13 +402,14 @@
         }
 
         c.drawGlyphs(
-                line.glyphIds,
-                prevStart,
-                tmpPositionArray,
-                0,
-                run.end - prevStart,
-                font,
-                tmpPaintForGlyph)
+            line.glyphIds,
+            prevStart,
+            tmpPositionArray,
+            0,
+            run.end - prevStart,
+            font,
+            tmpPaintForGlyph
+        )
     }
 
     private fun updatePositionsAndFonts(
@@ -418,9 +417,7 @@
         updateBase: Boolean
     ) {
         // Update target positions with newly calculated text layout.
-        check(layoutResult.size == lines.size) {
-            "The new layout result has different line count."
-        }
+        check(layoutResult.size == lines.size) { "The new layout result has different line count." }
 
         lines.zip(layoutResult) { line, runs ->
             line.runs.zip(runs) { lineRun, newGlyphs ->
@@ -436,7 +433,7 @@
                         }
                         require(newFont === newGlyphs.getFont(i)) {
                             "The new layout has different font run." +
-                                    " $newFont vs ${newGlyphs.getFont(i)} at $i"
+                                " $newFont vs ${newGlyphs.getFont(i)} at $i"
                         }
                     }
 
@@ -444,7 +441,7 @@
                     // check new font can be interpolatable with base font.
                     require(FontInterpolator.canInterpolate(newFont, run.baseFont)) {
                         "New font cannot be interpolated with existing font. $newFont," +
-                                " ${run.baseFont}"
+                            " ${run.baseFont}"
                     }
 
                     if (updateBase) {
@@ -480,14 +477,13 @@
     }
 
     // Shape the text and stores the result to out argument.
-    private fun shapeText(
-        layout: Layout,
-        paint: TextPaint
-    ): List<List<PositionedGlyphs>> {
+    private fun shapeText(layout: Layout, paint: TextPaint): List<List<PositionedGlyphs>> {
+        var text = StringBuilder()
         val out = mutableListOf<List<PositionedGlyphs>>()
         for (lineNo in 0 until layout.lineCount) { // Shape all lines.
             val lineStart = layout.getLineStart(lineNo)
-            var count = layout.getLineEnd(lineNo) - lineStart
+            val lineEnd = layout.getLineEnd(lineNo)
+            var count = lineEnd - lineStart
             // Do not render the last character in the line if it's a newline and unprintable
             val last = lineStart + count - 1
             if (last > lineStart && last < layout.text.length && layout.text[last] == '\n') {
@@ -495,19 +491,28 @@
             }
 
             val runs = mutableListOf<PositionedGlyphs>()
-            TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic,
-                    paint) { _, _, glyphs, _ ->
-                runs.add(glyphs)
-            }
+            TextShaper.shapeText(
+                layout.text,
+                lineStart,
+                count,
+                layout.textDirectionHeuristic,
+                paint
+            ) { _, _, glyphs, _ -> runs.add(glyphs) }
             out.add(runs)
+
+            if (lineNo > 0) {
+                text.append("\n")
+            }
+            text.append(layout.text.substring(lineStart, lineEnd))
         }
+        shapedText = text.toString()
         return out
     }
 }
 
 private fun Layout.getDrawOrigin(lineNo: Int) =
-        if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) {
-            getLineLeft(lineNo)
-        } else {
-            getLineRight(lineNo)
-        }
+    if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) {
+        getLineLeft(lineNo)
+    } else {
+        getLineRight(lineNo)
+    }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
new file mode 100644
index 0000000..f3d8b17
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.animation.back
+
+import android.util.DisplayMetrics
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.util.dpToPx
+
+/** Used to convert [BackEvent] into a [BackTransformation]. */
+fun interface BackAnimationSpec {
+
+    /** Computes transformation based on a [backEvent] and sets it to [result]. */
+    fun getBackTransformation(
+        backEvent: BackEvent,
+        progressY: Float, // TODO(b/265060720): Remove progressY. Could be retrieved from backEvent
+        result: BackTransformation,
+    )
+
+    companion object
+}
+
+/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */
+fun BackAnimationSpec.Companion.createFloatingSurfaceAnimationSpec(
+    displayMetrics: DisplayMetrics,
+    maxMarginXdp: Float,
+    maxMarginYdp: Float,
+    minScale: Float,
+    translateXEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
+    translateYEasing: Interpolator = Interpolators.LINEAR,
+    scaleEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
+): BackAnimationSpec {
+    val screenWidthPx = displayMetrics.widthPixels
+    val screenHeightPx = displayMetrics.heightPixels
+
+    val maxMarginXPx = maxMarginXdp.dpToPx(displayMetrics)
+    val maxMarginYPx = maxMarginYdp.dpToPx(displayMetrics)
+    val maxTranslationXByScale = (screenWidthPx - screenWidthPx * minScale) / 2
+    val maxTranslationX = maxTranslationXByScale - maxMarginXPx
+    val maxTranslationYByScale = (screenHeightPx - screenHeightPx * minScale) / 2
+    val maxTranslationY = maxTranslationYByScale - maxMarginYPx
+    val minScaleReversed = 1f - minScale
+
+    return BackAnimationSpec { backEvent, progressY, result ->
+        val direction = if (backEvent.swipeEdge == BackEvent.EDGE_LEFT) 1 else -1
+        val progressX = backEvent.progress
+
+        val ratioTranslateX = translateXEasing.getInterpolation(progressX)
+        val ratioTranslateY = translateYEasing.getInterpolation(progressY)
+        val ratioScale = scaleEasing.getInterpolation(progressX)
+
+        result.apply {
+            translateX = ratioTranslateX * direction * maxTranslationX
+            translateY = ratioTranslateY * maxTranslationY
+            scale = 1f - (ratioScale * minScaleReversed)
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
new file mode 100644
index 0000000..c6b7073
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.animation.back
+
+import android.util.DisplayMetrics
+
+/**
+ * SysUI transitions - Dismiss app (ST1) Return to launching surface or place of origin
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-1-dismiss-app
+ */
+fun BackAnimationSpec.Companion.dismissAppForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 8f,
+        maxMarginYdp = 8f,
+        minScale = 0.8f,
+    )
+
+/**
+ * SysUI transitions - Cross task (ST2) Return to previous task/app, keeping the current one open
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-2-cross-task
+ */
+fun BackAnimationSpec.Companion.crossTaskForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 8f,
+        maxMarginYdp = 8f,
+        minScale = 0.8f,
+    )
+
+/**
+ * SysUI transitions - Inner area dismiss (ST3) Dismiss non-detachable surface
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-3-inner-area-dismiss
+ */
+fun BackAnimationSpec.Companion.innerAreaDismissForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 0f,
+        maxMarginYdp = 0f,
+        minScale = 0.9f,
+    )
+
+/**
+ * SysUI transitions - Floating system surfaces (ST4)
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-4-floating-system-surfaces
+ */
+fun BackAnimationSpec.Companion.floatingSystemSurfacesForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 8f,
+        maxMarginYdp = 8f,
+        minScale = 0.8f,
+    )
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
new file mode 100644
index 0000000..49d1fb4
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.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.animation.back
+
+import android.view.View
+
+/**
+ * This object that represents the transformation to apply to the target. The properties of this
+ * object are mutable for performance reasons (avoid recreating this object)
+ */
+data class BackTransformation(
+    var translateX: Float = Float.NaN,
+    var translateY: Float = Float.NaN,
+    var scale: Float = Float.NaN,
+)
+
+/** Apply the transformation to the [targetView] */
+fun BackTransformation.applyTo(targetView: View) {
+    if (translateX.isFinite()) targetView.translationX = translateX
+    if (translateY.isFinite()) targetView.translationY = translateY
+    if (scale.isFinite()) {
+        targetView.scaleX = scale
+        targetView.scaleY = scale
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
new file mode 100644
index 0000000..8740d14
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.animation.back
+
+import android.annotation.IntRange
+import android.util.DisplayMetrics
+import android.view.View
+import android.window.BackEvent
+import android.window.OnBackAnimationCallback
+import android.window.OnBackInvokedDispatcher
+import android.window.OnBackInvokedDispatcher.Priority
+
+/**
+ * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be
+ * called on each update passing the current [BackTransformation].
+ *
+ * Optionally, you can specify [onBackStarted], [onBackInvoked], and [onBackCancelled] callbacks.
+ *
+ * @sample com.android.systemui.util.registerAnimationOnBackInvoked
+ */
+fun onBackAnimationCallbackFrom(
+    backAnimationSpec: BackAnimationSpec,
+    displayMetrics: DisplayMetrics, // TODO(b/265060720): We could remove this
+    onBackProgressed: (BackTransformation) -> Unit,
+    onBackStarted: (BackEvent) -> Unit = {},
+    onBackInvoked: () -> Unit = {},
+    onBackCancelled: () -> Unit = {},
+): OnBackAnimationCallback {
+    return object : OnBackAnimationCallback {
+        private var initialY = 0f
+        private val lastTransformation = BackTransformation()
+
+        override fun onBackStarted(backEvent: BackEvent) {
+            initialY = backEvent.touchY
+            onBackStarted(backEvent)
+        }
+
+        override fun onBackProgressed(backEvent: BackEvent) {
+            val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels
+
+            backAnimationSpec.getBackTransformation(
+                backEvent = backEvent,
+                progressY = progressY,
+                result = lastTransformation,
+            )
+
+            onBackProgressed(lastTransformation)
+        }
+
+        override fun onBackInvoked() {
+            onBackInvoked()
+        }
+
+        override fun onBackCancelled() {
+            onBackCancelled()
+        }
+    }
+}
+
+/**
+ * Register [OnBackAnimationCallback] when View is attached and unregister it when View is detached
+ *
+ * @sample com.android.systemui.util.registerAnimationOnBackInvoked
+ */
+fun View.registerOnBackInvokedCallbackOnViewAttached(
+    onBackInvokedDispatcher: OnBackInvokedDispatcher,
+    onBackAnimationCallback: OnBackAnimationCallback,
+    @Priority @IntRange(from = 0) priority: Int = OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+) {
+    addOnAttachStateChangeListener(
+        object : View.OnAttachStateChangeListener {
+            override fun onViewAttachedToWindow(v: View) {
+                onBackInvokedDispatcher.registerOnBackInvokedCallback(
+                    priority,
+                    onBackAnimationCallback
+                )
+            }
+
+            override fun onViewDetachedFromWindow(v: View) {
+                removeOnAttachStateChangeListener(this)
+                onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackAnimationCallback)
+            }
+        }
+    )
+
+    if (isAttachedToWindow) {
+        onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackAnimationCallback)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableImageView.kt
similarity index 90%
rename from packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableImageView.kt
index 7bbfec7..e42b589 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableImageView.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.systemui.common.ui.view
+package com.android.systemui.animation.view
 
 import android.content.Context
 import android.util.AttributeSet
@@ -23,7 +23,8 @@
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
 
-class LaunchableImageView : ImageView, LaunchableView {
+/** An [ImageView] that also implements [LaunchableView]. */
+open class LaunchableImageView : ImageView, LaunchableView {
     private val delegate =
         LaunchableViewDelegate(
             this,
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt
index ddde628..bce2622 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.common.ui.view
+package com.android.systemui.animation.view
 
 import android.content.Context
 import android.util.AttributeSet
@@ -23,7 +23,7 @@
 import com.android.systemui.animation.LaunchableViewDelegate
 
 /** A [LinearLayout] that also implements [LaunchableView]. */
-class LaunchableLinearLayout : LinearLayout, LaunchableView {
+open class LaunchableLinearLayout : LinearLayout, LaunchableView {
     private val delegate =
         LaunchableViewDelegate(
             this,
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableTextView.kt
similarity index 78%
copy from packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
copy to packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableTextView.kt
index 7bbfec7..1476695 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableTextView.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -15,15 +15,16 @@
  *
  */
 
-package com.android.systemui.common.ui.view
+package com.android.systemui.animation.view
 
 import android.content.Context
 import android.util.AttributeSet
-import android.widget.ImageView
+import android.widget.TextView
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
 
-class LaunchableImageView : ImageView, LaunchableView {
+/** A [TextView] that also implements [LaunchableView]. */
+open class LaunchableTextView : TextView, LaunchableView {
     private val delegate =
         LaunchableViewDelegate(
             this,
@@ -38,13 +39,6 @@
         defStyleAttr: Int,
     ) : super(context, attrs, defStyleAttr)
 
-    constructor(
-        context: Context?,
-        attrs: AttributeSet?,
-        defStyleAttr: Int,
-        defStyleRes: Int,
-    ) : super(context, attrs, defStyleAttr, defStyleRes)
-
     override fun setShouldBlockVisibilityChanges(block: Boolean) {
         delegate.setShouldBlockVisibilityChanges(block)
     }
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 0e3d41c..bd91c65 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
@@ -48,7 +48,7 @@
         animator.addUpdateListener { updateListener ->
             val now = updateListener.currentPlayTime
             val progress = updateListener.animatedValue as Float
-            rippleShader.progress = progress
+            rippleShader.rawProgress = progress
             rippleShader.distortionStrength = if (config.shouldDistort) 1 - progress else 0f
             rippleShader.time = now.toFloat()
         }
@@ -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)
+            rippleSize.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 9058510..0b842ad 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
@@ -15,9 +15,11 @@
  */
 package com.android.systemui.surfaceeffects.ripple
 
-import android.graphics.PointF
 import android.graphics.RuntimeShader
+import android.util.Log
 import android.util.MathUtils
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.Interpolators
 import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary
 import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
 
@@ -43,11 +45,26 @@
     }
     // language=AGSL
     companion object {
+        private val TAG = RippleShader::class.simpleName
+
+        // Default fade in/ out values. The value range is [0,1].
+        const val DEFAULT_FADE_IN_START = 0f
+        const val DEFAULT_FADE_OUT_END = 1f
+
+        const val DEFAULT_BASE_RING_FADE_IN_END = 0.1f
+        const val DEFAULT_BASE_RING_FADE_OUT_START = 0.3f
+
+        const val DEFAULT_SPARKLE_RING_FADE_IN_END = 0.1f
+        const val DEFAULT_SPARKLE_RING_FADE_OUT_START = 0.4f
+
+        const val DEFAULT_CENTER_FILL_FADE_IN_END = 0f
+        const val DEFAULT_CENTER_FILL_FADE_OUT_START = 0f
+        const val DEFAULT_CENTER_FILL_FADE_OUT_END = 0.6f
+
         private const val SHADER_UNIFORMS =
             """
             uniform vec2 in_center;
             uniform vec2 in_size;
-            uniform float in_progress;
             uniform float in_cornerRadius;
             uniform float in_thickness;
             uniform float in_time;
@@ -68,7 +85,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;
 
@@ -85,7 +102,7 @@
             vec4 main(vec2 p) {
                 float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius,
                     in_thickness), in_blur);
-                float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius),
+                float inside = soften(sdRoundedBox(p-in_center, in_size * 1.25, in_cornerRadius),
                     in_blur);
                 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
                     * (1.-sparkleRing) * in_fadeSparkle;
@@ -143,11 +160,26 @@
             }
 
         private fun subProgress(start: Float, end: Float, progress: Float): Float {
+            // Avoid division by 0.
+            if (start == end) {
+                // If start and end are the same and progress has exceeded the start/ end point,
+                // treat it as 1, otherwise 0.
+                return if (progress > start) 1f else 0f
+            }
+
             val min = Math.min(start, end)
             val max = Math.max(start, end)
             val sub = Math.min(Math.max(progress, min), max)
             return (sub - start) / (end - start)
         }
+
+        private fun getFade(fadeParams: FadeParams, rawProgress: Float): Float {
+            val fadeIn = subProgress(fadeParams.fadeInStart, fadeParams.fadeInEnd, rawProgress)
+            val fadeOut =
+                1f - subProgress(fadeParams.fadeOutStart, fadeParams.fadeOutEnd, rawProgress)
+
+            return Math.min(fadeIn, fadeOut)
+        }
     }
 
     /** Sets the center position of the ripple. */
@@ -155,40 +187,49 @@
         setFloatUniform("in_center", x, y)
     }
 
-    /** Max width of the ripple. */
-    private var maxSize: PointF = PointF()
-    fun setMaxSize(width: Float, height: Float) {
-        maxSize.x = width
-        maxSize.y = height
-    }
+    /**
+     * Blur multipliers for the ripple.
+     *
+     * <p>It interpolates from [blurStart] to [blurEnd] based on the [progress]. Increase number to
+     * add more blur.
+     */
+    var blurStart: Float = 1.25f
+    var blurEnd: Float = 0.5f
 
-    /** Progress of the ripple. Float value between [0, 1]. */
-    var progress: Float = 0.0f
+    /** Size of the ripple. */
+    val rippleSize = RippleSize()
+
+    /**
+     * Linear progress of the ripple. Float value between [0, 1].
+     *
+     * <p>Note that the progress here is expected to be linear without any curve applied.
+     */
+    var rawProgress: Float = 0.0f
         set(value) {
             field = value
-            setFloatUniform("in_progress", value)
-            val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
+            progress = Interpolators.STANDARD.getInterpolation(value)
 
-            currentWidth = maxSize.x * curvedProg
-            currentHeight = maxSize.y * curvedProg
-            setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
-            setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
-            // radius should not exceed width and height values.
-            setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
+            setFloatUniform("in_fadeSparkle", getFade(sparkleRingFadeParams, value))
+            setFloatUniform("in_fadeRing", getFade(baseRingFadeParams, value))
+            setFloatUniform("in_fadeFill", getFade(centerFillFadeParams, value))
+        }
 
-            setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
+    /** Progress with Standard easing curve applied. */
+    private var progress: Float = 0.0f
+        set(value) {
+            field = value
 
-            val fadeIn = subProgress(0f, 0.1f, value)
-            val fadeOutNoise = subProgress(0.4f, 1f, value)
-            var fadeOutRipple = 0f
-            var fadeFill = 0f
-            if (!rippleFill) {
-                fadeFill = subProgress(0f, 0.6f, value)
-                fadeOutRipple = subProgress(0.3f, 1f, value)
-            }
-            setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
-            setFloatUniform("in_fadeFill", 1 - fadeFill)
-            setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
+            rippleSize.update(value)
+
+            setFloatUniform("in_size", rippleSize.currentWidth, rippleSize.currentHeight)
+            setFloatUniform("in_thickness", rippleSize.currentHeight * 0.5f)
+            // Corner radius is always max of the min between the current width and height.
+            setFloatUniform(
+                "in_cornerRadius",
+                Math.min(rippleSize.currentWidth, rippleSize.currentHeight)
+            )
+
+            setFloatUniform("in_blur", MathUtils.lerp(blurStart, blurEnd, value))
         }
 
     /** Play time since the start of the effect. */
@@ -220,25 +261,189 @@
     var distortionStrength: Float = 0.0f
         set(value) {
             field = value
-            setFloatUniform("in_distort_radial", 75 * progress * value)
+            setFloatUniform("in_distort_radial", 75 * rawProgress * value)
             setFloatUniform("in_distort_xy", 75 * value)
         }
 
+    /**
+     * Pixel density of the screen that the effects are rendered to.
+     *
+     * <p>This value should come from [resources.displayMetrics.density].
+     */
     var pixelDensity: Float = 1.0f
         set(value) {
             field = value
             setFloatUniform("in_pixelDensity", value)
         }
 
+    /** Parameters that are used to fade in/ out of the sparkle ring. */
+    val sparkleRingFadeParams =
+        FadeParams(
+            DEFAULT_FADE_IN_START,
+            DEFAULT_SPARKLE_RING_FADE_IN_END,
+            DEFAULT_SPARKLE_RING_FADE_OUT_START,
+            DEFAULT_FADE_OUT_END
+        )
+
     /**
-     * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
-     * False for a ring effect.
+     * Parameters that are used to fade in/ out of the base ring.
+     *
+     * <p>Note that the shader draws the sparkle ring on top of the base ring.
      */
-    var rippleFill: Boolean = false
+    val baseRingFadeParams =
+        FadeParams(
+            DEFAULT_FADE_IN_START,
+            DEFAULT_BASE_RING_FADE_IN_END,
+            DEFAULT_BASE_RING_FADE_OUT_START,
+            DEFAULT_FADE_OUT_END
+        )
 
-    var currentWidth: Float = 0f
-        private set
+    /** Parameters that are used to fade in/ out of the center fill. */
+    val centerFillFadeParams =
+        FadeParams(
+            DEFAULT_FADE_IN_START,
+            DEFAULT_CENTER_FILL_FADE_IN_END,
+            DEFAULT_CENTER_FILL_FADE_OUT_START,
+            DEFAULT_CENTER_FILL_FADE_OUT_END
+        )
 
-    var currentHeight: Float = 0f
-        private set
+    /**
+     * Parameters used for fade in and outs of the ripple.
+     *
+     * <p>Note that all the fade in/ outs are "linear" progression.
+     * ```
+     *          (opacity)
+     *          1
+     *          │
+     * maxAlpha ←       ――――――――――――
+     *          │      /            \
+     *          │     /              \
+     * minAlpha ←――――/                \―――― (alpha change)
+     *          │
+     *          │
+     *          0 ―――↑―――↑―――――――――↑―――↑――――1 (progress)
+     *               fadeIn        fadeOut
+     *               Start & End   Start & End
+     * ```
+     * <p>If no fade in/ out is needed, set [fadeInStart] and [fadeInEnd] to 0; [fadeOutStart] and
+     * [fadeOutEnd] to 1.
+     */
+    data class FadeParams(
+        /**
+         * The starting point of the fade out which ends at [fadeInEnd], given that the animation
+         * goes from 0 to 1.
+         */
+        var fadeInStart: Float = DEFAULT_FADE_IN_START,
+        /**
+         * The endpoint of the fade in when the fade in starts at [fadeInStart], given that the
+         * animation goes from 0 to 1.
+         */
+        var fadeInEnd: Float,
+        /**
+         * The starting point of the fade out which ends at 1, given that the animation goes from 0
+         * to 1.
+         */
+        var fadeOutStart: Float,
+
+        /** The endpoint of the fade out, given that the animation goes from 0 to 1. */
+        var fadeOutEnd: Float = DEFAULT_FADE_OUT_END,
+    )
+
+    /**
+     * Desired size of the ripple at a point t in [progress].
+     *
+     * <p>Note that [progress] is curved and normalized. Below is an example usage:
+     * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 0.2f, width= 500f, height=
+     * 700f), SizeAtProgress(t= 1f, width= 100f, height= 300f)
+     *
+     * <p>For simple ripple effects, you will want to use [setMaxSize] as it is translated into:
+     * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 1f, width= maxWidth, height=
+     * maxHeight)
+     */
+    data class SizeAtProgress(
+        /** Time t in [0,1] progress range. */
+        var t: Float,
+        /** Target width size of the ripple at time [t]. */
+        var width: Float,
+        /** Target height size of the ripple at time [t]. */
+        var height: Float
+    )
+
+    /** Updates and stores the ripple size. */
+    inner class RippleSize {
+        @VisibleForTesting var sizes = mutableListOf<SizeAtProgress>()
+        @VisibleForTesting var currentSizeIndex = 0
+        @VisibleForTesting val initialSize = SizeAtProgress(0f, 0f, 0f)
+
+        var currentWidth: Float = 0f
+            private set
+        var currentHeight: Float = 0f
+            private set
+
+        /**
+         * Sets the max size of the ripple.
+         *
+         * <p>Use this if the ripple shape simply changes linearly.
+         */
+        fun setMaxSize(width: Float, height: Float) {
+            setSizeAtProgresses(initialSize, SizeAtProgress(1f, width, height))
+        }
+
+        /**
+         * Sets the list of [sizes].
+         *
+         * <p>Note that setting this clears the existing sizes.
+         */
+        fun setSizeAtProgresses(vararg sizes: SizeAtProgress) {
+            // Reset everything.
+            this.sizes.clear()
+            currentSizeIndex = 0
+
+            this.sizes.addAll(sizes)
+            this.sizes.sortBy { it.t }
+        }
+
+        /**
+         * Updates the current ripple size based on the progress.
+         *
+         * <p>Should be called when progress updates.
+         */
+        fun update(progress: Float) {
+            val targetIndex = updateTargetIndex(progress)
+            val prevIndex = Math.max(targetIndex - 1, 0)
+
+            val targetSize = sizes[targetIndex]
+            val prevSize = sizes[prevIndex]
+
+            val subProgress = subProgress(prevSize.t, targetSize.t, progress)
+
+            currentWidth = targetSize.width * subProgress + prevSize.width
+            currentHeight = targetSize.height * subProgress + prevSize.height
+        }
+
+        private fun updateTargetIndex(progress: Float): Int {
+            if (sizes.isEmpty()) {
+                // It could be empty on init.
+                if (progress > 0f) {
+                    Log.e(
+                        TAG,
+                        "Did you forget to set the ripple size? Use [setMaxSize] or " +
+                            "[setSizeAtProgresses] before playing the animation."
+                    )
+                }
+                // If there's no size is set, we set everything to 0 and return early.
+                setSizeAtProgresses(initialSize)
+                return currentSizeIndex
+            }
+
+            var candidate = sizes[currentSizeIndex]
+
+            while (progress > candidate.t) {
+                currentSizeIndex = Math.min(currentSizeIndex + 1, sizes.size - 1)
+                candidate = sizes[currentSizeIndex]
+            }
+
+            return currentSizeIndex
+        }
+    }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index b37c734..4c7c435 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -45,12 +45,8 @@
 
     var duration: Long = 1750
 
-    private var maxWidth: Float = 0.0f
-    private var maxHeight: Float = 0.0f
     fun setMaxSize(maxWidth: Float, maxHeight: Float) {
-        this.maxWidth = maxWidth
-        this.maxHeight = maxHeight
-        rippleShader.setMaxSize(maxWidth, maxHeight)
+        rippleShader.rippleSize.setMaxSize(maxWidth, maxHeight)
     }
 
     private var centerX: Float = 0.0f
@@ -77,13 +73,113 @@
         rippleShader = RippleShader(rippleShape)
 
         rippleShader.color = RippleAnimationConfig.RIPPLE_DEFAULT_COLOR
-        rippleShader.progress = 0f
+        rippleShader.rawProgress = 0f
         rippleShader.sparkleStrength = RippleAnimationConfig.RIPPLE_SPARKLE_STRENGTH
         rippleShader.pixelDensity = resources.displayMetrics.density
 
         ripplePaint.shader = rippleShader
     }
 
+    /**
+     * Sets the fade parameters for the base ring.
+     *
+     * <p>Base ring indicates a blurred ring below the sparkle ring. See
+     * [RippleShader.baseRingFadeParams].
+     */
+    @JvmOverloads
+    fun setBaseRingFadeParams(
+        fadeInStart: Float = rippleShader.baseRingFadeParams.fadeInStart,
+        fadeInEnd: Float = rippleShader.baseRingFadeParams.fadeInEnd,
+        fadeOutStart: Float = rippleShader.baseRingFadeParams.fadeOutStart,
+        fadeOutEnd: Float = rippleShader.baseRingFadeParams.fadeOutEnd
+    ) {
+        setFadeParams(
+            rippleShader.baseRingFadeParams,
+            fadeInStart,
+            fadeInEnd,
+            fadeOutStart,
+            fadeOutEnd
+        )
+    }
+
+    /**
+     * Sets the fade parameters for the sparkle ring.
+     *
+     * <p>Sparkle ring refers to the ring that's drawn on top of the base ring. See
+     * [RippleShader.sparkleRingFadeParams].
+     */
+    @JvmOverloads
+    fun setSparkleRingFadeParams(
+        fadeInStart: Float = rippleShader.sparkleRingFadeParams.fadeInStart,
+        fadeInEnd: Float = rippleShader.sparkleRingFadeParams.fadeInEnd,
+        fadeOutStart: Float = rippleShader.sparkleRingFadeParams.fadeOutStart,
+        fadeOutEnd: Float = rippleShader.sparkleRingFadeParams.fadeOutEnd
+    ) {
+        setFadeParams(
+            rippleShader.sparkleRingFadeParams,
+            fadeInStart,
+            fadeInEnd,
+            fadeOutStart,
+            fadeOutEnd
+        )
+    }
+
+    /**
+     * Sets the fade parameters for the center fill.
+     *
+     * <p>One common use case is set all the params to 1, which completely removes the center fill.
+     * See [RippleShader.centerFillFadeParams].
+     */
+    @JvmOverloads
+    fun setCenterFillFadeParams(
+        fadeInStart: Float = rippleShader.centerFillFadeParams.fadeInStart,
+        fadeInEnd: Float = rippleShader.centerFillFadeParams.fadeInEnd,
+        fadeOutStart: Float = rippleShader.centerFillFadeParams.fadeOutStart,
+        fadeOutEnd: Float = rippleShader.centerFillFadeParams.fadeOutEnd
+    ) {
+        setFadeParams(
+            rippleShader.centerFillFadeParams,
+            fadeInStart,
+            fadeInEnd,
+            fadeOutStart,
+            fadeOutEnd
+        )
+    }
+
+    private fun setFadeParams(
+        fadeParams: RippleShader.FadeParams,
+        fadeInStart: Float,
+        fadeInEnd: Float,
+        fadeOutStart: Float,
+        fadeOutEnd: Float
+    ) {
+        with(fadeParams) {
+            this.fadeInStart = fadeInStart
+            this.fadeInEnd = fadeInEnd
+            this.fadeOutStart = fadeOutStart
+            this.fadeOutEnd = fadeOutEnd
+        }
+    }
+
+    /**
+     * Sets blur multiplier at start and end of the progress.
+     *
+     * <p>It interpolates between [start] and [end]. No need to set it if using default blur.
+     */
+    fun setBlur(start: Float, end: Float) {
+        rippleShader.blurStart = start
+        rippleShader.blurEnd = end
+    }
+
+    /**
+     * Sets the list of [RippleShader.SizeAtProgress].
+     *
+     * <p>Note that this clears the list before it sets with the new data.
+     */
+    fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) {
+        rippleShader.rippleSize.setSizeAtProgresses(*targetSizes)
+    }
+
     @JvmOverloads
     fun startRipple(onAnimationEnd: Runnable? = null) {
         if (animator.isRunning) {
@@ -93,7 +189,7 @@
         animator.addUpdateListener { updateListener ->
             val now = updateListener.currentPlayTime
             val progress = updateListener.animatedValue as Float
-            rippleShader.progress = progress
+            rippleShader.rawProgress = progress
             rippleShader.distortionStrength = 1 - progress
             rippleShader.time = now.toFloat()
             invalidate()
@@ -117,15 +213,6 @@
         rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
     }
 
-    /**
-     * Set whether the ripple should remain filled as the ripple expands.
-     *
-     * See [RippleShader.rippleFill].
-     */
-    fun setRippleFill(rippleFill: Boolean) {
-        rippleShader.rippleFill = rippleFill
-    }
-
     /** Set the intensity of the sparkles. */
     fun setSparkleStrength(strength: Float) {
         rippleShader.sparkleStrength = strength
@@ -144,23 +231,11 @@
         // active effect area. Values here should be kept in sync with the animation implementation
         // in the ripple shader.
         if (rippleShape == RippleShape.CIRCLE) {
-            val maskRadius =
-                (1 -
-                    (1 - rippleShader.progress) *
-                        (1 - rippleShader.progress) *
-                        (1 - rippleShader.progress)) * maxWidth
+            val maskRadius = rippleShader.rippleSize.currentWidth
             canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
-        } else {
-            val maskWidth =
-                (1 -
-                    (1 - rippleShader.progress) *
-                        (1 - rippleShader.progress) *
-                        (1 - rippleShader.progress)) * maxWidth * 2
-            val maskHeight =
-                (1 -
-                    (1 - rippleShader.progress) *
-                        (1 - rippleShader.progress) *
-                        (1 - rippleShader.progress)) * maxHeight * 2
+        } else if (rippleShape == RippleShape.ELLIPSE) {
+            val maskWidth = rippleShader.rippleSize.currentWidth * 2
+            val maskHeight = rippleShader.rippleSize.currentHeight * 2
             canvas.drawRect(
                 /* left= */ centerX - maskWidth,
                 /* top= */ centerY - maskHeight,
@@ -168,6 +243,10 @@
                 /* bottom= */ centerY + maskHeight,
                 ripplePaint
             )
+        } else { // RippleShape.RoundedBox
+            // No masking for the rounded box, as it has more blur which requires larger bounds.
+            // Masking creates sharp bounds even when the masking is 4 times bigger.
+            canvas.drawPaint(ripplePaint)
         }
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
index 8b2f466..7889893 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
@@ -50,9 +50,9 @@
 
             float roundedBoxRing(vec2 p, vec2 size, float cornerRadius,
                 float borderThickness) {
-                float outerRoundBox = sdRoundedBox(p, size, cornerRadius);
-                float innerRoundBox = sdRoundedBox(p, size - vec2(borderThickness),
-                    cornerRadius - borderThickness);
+                float outerRoundBox = sdRoundedBox(p, size + vec2(borderThickness),
+                    cornerRadius + borderThickness);
+                float innerRoundBox = sdRoundedBox(p, size, cornerRadius);
                 return subtract(outerRoundBox, innerRoundBox);
             }
         """
@@ -69,10 +69,13 @@
 
             vec2 u = wh*p, v = wh*wh;
 
-            float U1 = u.y/2.0;  float U5 = 4.0*U1;
-            float U2 = v.y-v.x;  float U6 = 6.0*U1;
-            float U3 = u.x-U2;   float U7 = 3.0*U3;
+            float U1 = u.y/2.0;
+            float U2 = v.y-v.x;
+            float U3 = u.x-U2;
             float U4 = u.x+U2;
+            float U5 = 4.0*U1;
+            float U6 = 6.0*U1;
+            float U7 = 3.0*U3;
 
             float t = 0.5;
             for (int i = 0; i < 3; i ++) {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
index 6715951..79bc2f4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
@@ -57,7 +57,7 @@
     val onAnimationEnd: Runnable? = null
 ) {
     companion object {
-        const val DEFAULT_MAX_DURATION_IN_MILLIS = 7500f
+        const val DEFAULT_MAX_DURATION_IN_MILLIS = 30_000f // Max 30 sec
         const val DEFAULT_EASING_DURATION_IN_MILLIS = 750f
         const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f
         const val DEFAULT_NOISE_GRID_COUNT = 1.2f
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
new file mode 100644
index 0000000..428856d
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
@@ -0,0 +1,51 @@
+/*
+ * 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
+
+import android.app.Dialog
+import android.view.View
+import android.window.OnBackInvokedDispatcher
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.BackTransformation
+import com.android.systemui.animation.back.applyTo
+import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
+import com.android.systemui.animation.back.onBackAnimationCallbackFrom
+import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
+
+/**
+ * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
+ * The [BackTransformation] will be applied on the [targetView].
+ */
+@JvmOverloads
+fun Dialog.registerAnimationOnBackInvoked(
+    targetView: View,
+    backAnimationSpec: BackAnimationSpec =
+        BackAnimationSpec.floatingSystemSurfacesForSysUi(
+            displayMetrics = targetView.resources.displayMetrics,
+        ),
+) {
+    targetView.registerOnBackInvokedCallbackOnViewAttached(
+        onBackInvokedDispatcher = onBackInvokedDispatcher,
+        onBackAnimationCallback =
+            onBackAnimationCallbackFrom(
+                backAnimationSpec = backAnimationSpec,
+                displayMetrics = targetView.resources.displayMetrics,
+                onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) },
+                onBackInvoked = { dismiss() },
+            ),
+    )
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
new file mode 100644
index 0000000..4bc9972
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
@@ -0,0 +1,32 @@
+/*
+ * 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
+
+import android.content.Context
+import android.content.res.Resources
+import android.util.DisplayMetrics
+import android.util.TypedValue
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(context: Context): Float = dpToPx(resources = context.resources)
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(resources: Resources): Float = dpToPx(displayMetrics = resources.displayMetrics)
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(displayMetrics: DisplayMetrics): Float =
+    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, toFloat(), displayMetrics)
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt
new file mode 100644
index 0000000..1f6e603
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UFile
+import org.jetbrains.uast.UImportStatement
+
+/**
+ * Detects violations of the Dependency Rule of Clean Architecture.
+ *
+ * The rule states that code in each layer may only depend on code in the same layer or the layer
+ * directly "beneath" that layer in the layer diagram.
+ *
+ * In System UI, we have three layers; from top to bottom, they are: ui, domain, and data. As a
+ * convention, was used packages with those names to place code in the appropriate layer. We also
+ * make an exception and allow for shared models to live under a separate package named "shared" to
+ * avoid code duplication.
+ *
+ * For more information, please see go/sysui-arch.
+ */
+@Suppress("UnstableApiUsage")
+class CleanArchitectureDependencyViolationDetector : Detector(), Detector.UastScanner {
+    override fun getApplicableUastTypes(): List<Class<out UElement>> {
+        return listOf(UFile::class.java)
+    }
+
+    override fun createUastHandler(context: JavaContext): UElementHandler {
+        return object : UElementHandler() {
+            override fun visitFile(node: UFile) {
+                // Check which Clean Architecture layer this file belongs to:
+                matchingLayer(node.packageName)?.let { layer ->
+                    // The file matches with a Clean Architecture layer. Let's check all of its
+                    // imports.
+                    node.imports.forEach { importStatement ->
+                        visitImportStatement(context, layer, importStatement)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun visitImportStatement(
+        context: JavaContext,
+        layer: Layer,
+        importStatement: UImportStatement,
+    ) {
+        val importText = importStatement.importReference?.asSourceString() ?: return
+        val importedLayer = matchingLayer(importText) ?: return
+
+        // Now check whether the layer of the file may depend on the layer of the import.
+        if (!layer.mayDependOn(importedLayer)) {
+            context.report(
+                issue = ISSUE,
+                scope = importStatement,
+                location = context.getLocation(importStatement),
+                message =
+                    "The ${layer.packageNamePart} layer may not depend on" +
+                        " the ${importedLayer.packageNamePart} layer.",
+            )
+        }
+    }
+
+    private fun matchingLayer(packageName: String): Layer? {
+        val packageNameParts = packageName.split(".").toSet()
+        return Layer.values()
+            .filter { layer -> packageNameParts.contains(layer.packageNamePart) }
+            .takeIf { it.size == 1 }
+            ?.first()
+    }
+
+    private enum class Layer(
+        val packageNamePart: String,
+        val canDependOn: Set<Layer>,
+    ) {
+        SHARED(
+            packageNamePart = "shared",
+            canDependOn = emptySet(), // The shared layer may not depend on any other layer.
+        ),
+        DATA(
+            packageNamePart = "data",
+            canDependOn = setOf(SHARED),
+        ),
+        DOMAIN(
+            packageNamePart = "domain",
+            canDependOn = setOf(SHARED, DATA),
+        ),
+        UI(
+            packageNamePart = "ui",
+            canDependOn = setOf(DOMAIN, SHARED),
+        ),
+        ;
+
+        fun mayDependOn(otherLayer: Layer): Boolean {
+            return this == otherLayer || canDependOn.contains(otherLayer)
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        val ISSUE =
+            Issue.create(
+                id = "CleanArchitectureDependencyViolation",
+                briefDescription = "Violation of the Clean Architecture Dependency Rule.",
+                explanation =
+                    """
+                    Following the \"Dependency Rule\" from Clean Architecture, every layer of code \
+                    can only depend code in its own layer or code in the layer directly \
+                    \"beneath\" it. Therefore, the UI layer can only depend on the" Domain layer \
+                    and the Domain layer can only depend on the Data layer. We" do make an \
+                    exception to allow shared models to exist and be shared across layers by \
+                    placing them under shared/model, which should be done with care. For more \
+                    information about Clean Architecture in System UI, please see go/sysui-arch. \
+                    NOTE: if your code is not using Clean Architecture, please feel free to ignore \
+                    this warning.
+                """,
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(
+                        CleanArchitectureDependencyViolationDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE,
+                    ),
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DumpableNotRegisteredDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DumpableNotRegisteredDetector.kt
new file mode 100644
index 0000000..30e2a25
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DumpableNotRegisteredDetector.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Location
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+
+/**
+ * Checks if any class has implemented the `Dumpable` interface but has not registered itself with
+ * the `DumpManager`.
+ */
+@Suppress("UnstableApiUsage")
+class DumpableNotRegisteredDetector : Detector(), SourceCodeScanner {
+
+    private var isDumpable: Boolean = false
+    private var isCoreStartable: Boolean = false
+    private var hasRegisterCall: Boolean = false
+    private var classLocation: Location? = null
+
+    override fun beforeCheckFile(context: Context) {
+        isDumpable = false
+        isCoreStartable = false
+        hasRegisterCall = false
+        classLocation = null
+    }
+
+    override fun applicableSuperClasses(): List<String> {
+        return listOf(DUMPABLE_CLASS_NAME)
+    }
+
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf("registerDumpable", "registerNormalDumpable", "registerCriticalDumpable")
+    }
+
+    override fun visitClass(context: JavaContext, declaration: UClass) {
+        if (declaration.isInterface || context.evaluator.isAbstract(declaration)) {
+            // Don't require interfaces or abstract classes to call `register` (assume the full
+            // implementations will call it). This also means that we correctly don't warn for the
+            // `Dumpable` interface itself.
+            return
+        }
+
+        classLocation = context.getNameLocation(declaration)
+
+        val superTypeClassNames = declaration.superTypes.mapNotNull { it.resolve()?.qualifiedName }
+        isDumpable = superTypeClassNames.contains(DUMPABLE_CLASS_NAME)
+        isCoreStartable = superTypeClassNames.contains(CORE_STARTABLE_CLASS_NAME)
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (context.evaluator.isMemberInSubClassOf(method, DUMP_MANAGER_CLASS_NAME)) {
+            hasRegisterCall = true
+        }
+    }
+
+    override fun afterCheckFile(context: Context) {
+        if (!isDumpable) {
+            return
+        }
+        if (isDumpable && isCoreStartable) {
+            // CoreStartables will be automatically registered, so classes that implement
+            // CoreStartable do not need a `register` call.
+            return
+        }
+
+        if (!hasRegisterCall) {
+            context.report(
+                issue = ISSUE,
+                location = classLocation!!,
+                message =
+                    "Any class implementing `Dumpable` must call " +
+                        "`DumpManager.registerNormalDumpable` or " +
+                        "`DumpManager.registerCriticalDumpable`",
+            )
+        }
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "DumpableNotRegistered",
+                briefDescription = "Dumpable not registered with DumpManager.",
+                explanation =
+                    """
+                    This class has implemented the `Dumpable` interface, but it has not registered \
+                    itself with the `DumpManager`. This means that the class will never actually \
+                    be dumped. Please call `DumpManager.registerNormalDumpable` or \
+                    `DumpManager.registerCriticalDumpable` in the class's constructor or \
+                    initialization method.""",
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(DumpableNotRegisteredDetector::class.java, Scope.JAVA_FILE_SCOPE)
+            )
+
+        private const val DUMPABLE_CLASS_NAME = "com.android.systemui.Dumpable"
+        private const val CORE_STARTABLE_CLASS_NAME = "com.android.systemui.CoreStartable"
+        private const val DUMP_MANAGER_CLASS_NAME = "com.android.systemui.dump.DumpManager"
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 3f334c1c..84f7050 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -27,9 +27,12 @@
 class SystemUIIssueRegistry : IssueRegistry() {
 
     override val issues: List<Issue>
-        get() = listOf(
+        get() =
+            listOf(
                 BindServiceOnMainThreadDetector.ISSUE,
                 BroadcastSentViaContextDetector.ISSUE,
+                CleanArchitectureDependencyViolationDetector.ISSUE,
+                DumpableNotRegisteredDetector.ISSUE,
                 SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY,
                 SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY,
                 NonInjectedMainThreadDetector.ISSUE,
@@ -37,7 +40,7 @@
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
                 StaticSettingsProviderDetector.ISSUE
-        )
+            )
 
     override val api: Int
         get() = CURRENT_API
@@ -45,9 +48,9 @@
         get() = 8
 
     override val vendor: Vendor =
-            Vendor(
-                    vendorName = "Android",
-                    feedbackUrl = "http://b/issues/new?component=78010",
-                    contact = "jernej@google.com"
-            )
+        Vendor(
+            vendorName = "Android",
+            feedbackUrl = "http://b/issues/new?component=78010",
+            contact = "jernej@google.com"
+        )
 }
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt
new file mode 100644
index 0000000..a4b59fd
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+@Ignore("b/254533331")
+class CleanArchitectureDependencyViolationDetectorTest : SystemUILintDetectorTest() {
+    override fun getDetector(): Detector {
+        return CleanArchitectureDependencyViolationDetector()
+    }
+
+    override fun getIssues(): List<Issue> {
+        return listOf(
+            CleanArchitectureDependencyViolationDetector.ISSUE,
+        )
+    }
+
+    @Test
+    fun `No violations`() {
+        lint()
+            .files(
+                *LEGITIMATE_FILES,
+            )
+            .issues(
+                CleanArchitectureDependencyViolationDetector.ISSUE,
+            )
+            .run()
+            .expectWarningCount(0)
+    }
+
+    @Test
+    fun `Violation - domain depends on ui`() {
+        lint()
+            .files(
+                *LEGITIMATE_FILES,
+                TestFiles.kotlin(
+                    """
+                        package test.domain.interactor
+
+                        import test.ui.viewmodel.ViewModel
+
+                        class BadClass(
+                            private val viewModel: ViewModel,
+                        )
+                    """.trimIndent()
+                )
+            )
+            .issues(
+                CleanArchitectureDependencyViolationDetector.ISSUE,
+            )
+            .testModes(TestMode.DEFAULT)
+            .run()
+            .expectWarningCount(1)
+            .expect(
+                expectedText =
+                    """
+                    src/test/domain/interactor/BadClass.kt:3: Warning: The domain layer may not depend on the ui layer. [CleanArchitectureDependencyViolation]
+                    import test.ui.viewmodel.ViewModel
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                """,
+            )
+    }
+
+    @Test
+    fun `Violation - ui depends on data`() {
+        lint()
+            .files(
+                *LEGITIMATE_FILES,
+                TestFiles.kotlin(
+                    """
+                        package test.ui.viewmodel
+
+                        import test.data.repository.Repository
+
+                        class BadClass(
+                            private val repository: Repository,
+                        )
+                    """.trimIndent()
+                )
+            )
+            .issues(
+                CleanArchitectureDependencyViolationDetector.ISSUE,
+            )
+            .testModes(TestMode.DEFAULT)
+            .run()
+            .expectWarningCount(1)
+            .expect(
+                expectedText =
+                    """
+                    src/test/ui/viewmodel/BadClass.kt:3: Warning: The ui layer may not depend on the data layer. [CleanArchitectureDependencyViolation]
+                    import test.data.repository.Repository
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                """,
+            )
+    }
+
+    @Test
+    fun `Violation - shared depends on all other layers`() {
+        lint()
+            .files(
+                *LEGITIMATE_FILES,
+                TestFiles.kotlin(
+                    """
+                        package test.shared.model
+
+                        import test.data.repository.Repository
+                        import test.domain.interactor.Interactor
+                        import test.ui.viewmodel.ViewModel
+
+                        class BadClass(
+                            private val repository: Repository,
+                            private val interactor: Interactor,
+                            private val viewmodel: ViewModel,
+                        )
+                    """.trimIndent()
+                )
+            )
+            .issues(
+                CleanArchitectureDependencyViolationDetector.ISSUE,
+            )
+            .testModes(TestMode.DEFAULT)
+            .run()
+            .expectWarningCount(3)
+            .expect(
+                expectedText =
+                    """
+                    src/test/shared/model/BadClass.kt:3: Warning: The shared layer may not depend on the data layer. [CleanArchitectureDependencyViolation]
+                    import test.data.repository.Repository
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/test/shared/model/BadClass.kt:4: Warning: The shared layer may not depend on the domain layer. [CleanArchitectureDependencyViolation]
+                    import test.domain.interactor.Interactor
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    src/test/shared/model/BadClass.kt:5: Warning: The shared layer may not depend on the ui layer. [CleanArchitectureDependencyViolation]
+                    import test.ui.viewmodel.ViewModel
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 3 warnings
+                """,
+            )
+    }
+
+    @Test
+    fun `Violation - data depends on domain`() {
+        lint()
+            .files(
+                *LEGITIMATE_FILES,
+                TestFiles.kotlin(
+                    """
+                        package test.data.repository
+
+                        import test.domain.interactor.Interactor
+
+                        class BadClass(
+                            private val interactor: Interactor,
+                        )
+                    """.trimIndent()
+                )
+            )
+            .issues(
+                CleanArchitectureDependencyViolationDetector.ISSUE,
+            )
+            .testModes(TestMode.DEFAULT)
+            .run()
+            .expectWarningCount(1)
+            .expect(
+                expectedText =
+                    """
+                    src/test/data/repository/BadClass.kt:3: Warning: The data layer may not depend on the domain layer. [CleanArchitectureDependencyViolation]
+                    import test.domain.interactor.Interactor
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                """,
+            )
+    }
+
+    companion object {
+        private val MODEL_FILE =
+            TestFiles.kotlin(
+                """
+                    package test.shared.model
+
+                    import test.some.other.thing.SomeOtherThing
+
+                    data class Model(
+                        private val name: String,
+                    )
+                """.trimIndent()
+            )
+        private val REPOSITORY_FILE =
+            TestFiles.kotlin(
+                """
+                    package test.data.repository
+
+                    import test.shared.model.Model
+                    import test.some.other.thing.SomeOtherThing
+
+                    class Repository {
+                        private val models = listOf(
+                            Model("one"),
+                            Model("two"),
+                            Model("three"),
+                        )
+
+                        fun getModels(): List<Model> {
+                            return models
+                        }
+                    }
+                """.trimIndent()
+            )
+        private val INTERACTOR_FILE =
+            TestFiles.kotlin(
+                """
+                    package test.domain.interactor
+
+                    import test.data.repository.Repository
+                    import test.shared.model.Model
+
+                    class Interactor(
+                        private val repository: Repository,
+                    ) {
+                        fun getModels(): List<Model> {
+                            return repository.getModels()
+                        }
+                    }
+                """.trimIndent()
+            )
+        private val VIEW_MODEL_FILE =
+            TestFiles.kotlin(
+                """
+                    package test.ui.viewmodel
+
+                    import test.domain.interactor.Interactor
+                    import test.some.other.thing.SomeOtherThing
+
+                    class ViewModel(
+                        private val interactor: Interactor,
+                    ) {
+                        fun getNames(): List<String> {
+                            return interactor.getModels().map { model -> model.name }
+                        }
+                    }
+                """.trimIndent()
+            )
+        private val NON_CLEAN_ARCHITECTURE_FILE =
+            TestFiles.kotlin(
+                """
+                    package test.some.other.thing
+
+                    import test.data.repository.Repository
+                    import test.domain.interactor.Interactor
+                    import test.ui.viewmodel.ViewModel
+
+                    class SomeOtherThing {
+                        init {
+                            val viewModel = ViewModel(
+                                interactor = Interactor(
+                                    repository = Repository(),
+                                ),
+                            )
+                        }
+                    }
+                """.trimIndent()
+            )
+        private val LEGITIMATE_FILES =
+            arrayOf(
+                MODEL_FILE,
+                REPOSITORY_FILE,
+                INTERACTOR_FILE,
+                VIEW_MODEL_FILE,
+                NON_CLEAN_ARCHITECTURE_FILE,
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DumpableNotRegisteredDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DumpableNotRegisteredDetectorTest.kt
new file mode 100644
index 0000000..3d6cbc7
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DumpableNotRegisteredDetectorTest.kt
@@ -0,0 +1,328 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class DumpableNotRegisteredDetectorTest : SystemUILintDetectorTest() {
+    override fun getDetector(): Detector = DumpableNotRegisteredDetector()
+
+    override fun getIssues(): List<Issue> = listOf(DumpableNotRegisteredDetector.ISSUE)
+
+    @Test
+    fun classIsNotDumpable_noViolation() {
+        lint()
+            .files(
+                TestFiles.java(
+                    """
+                    package test.pkg;
+
+                    class SomeClass() {
+                    }
+                """.trimIndent()
+                ),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun classIsDumpable_andRegisterIsCalled_noViolation() {
+        lint()
+            .files(
+                TestFiles.java(
+                    """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+                    import com.android.systemui.dump.DumpManager;
+
+                    public class SomeClass implements Dumpable {
+                        SomeClass(DumpManager dumpManager) {
+                            dumpManager.registerDumpable(this);
+                        }
+
+                        @Override
+                        void dump(PrintWriter pw, String[] args) {
+                            pw.println("testDump");
+                        }
+                    }
+                """.trimIndent()
+                ),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun classIsDumpable_andRegisterNormalIsCalled_noViolation() {
+        lint()
+            .files(
+                TestFiles.java(
+                    """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+                    import com.android.systemui.dump.DumpManager;
+
+                    public class SomeClass implements Dumpable {
+                        SomeClass(DumpManager dumpManager) {
+                            dumpManager.registerNormalDumpable(this);
+                        }
+
+                        @Override
+                        void dump(PrintWriter pw, String[] args) {
+                            pw.println("testDump");
+                        }
+                    }
+                """.trimIndent()
+                ),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun classIsDumpable_andRegisterCriticalIsCalled_noViolation() {
+        lint()
+            .files(
+                TestFiles.java(
+                    """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+                    import com.android.systemui.dump.DumpManager;
+
+                    public class SomeClass implements Dumpable {
+                        SomeClass(DumpManager dumpManager) {
+                            dumpManager.registerCriticalDumpable(this);
+                        }
+
+                        @Override
+                        void dump(PrintWriter pw, String[] args) {
+                            pw.println("testDump");
+                        }
+                    }
+                """.trimIndent()
+                ),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun classIsDumpable_noRegister_violation() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+
+                    public class SomeClass implements Dumpable {
+                        @Override
+                        public void dump() {
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expect(
+                ("""
+                src/test/pkg/SomeClass.java:5: Warning: Any class implementing Dumpable must call DumpManager.registerNormalDumpable or DumpManager.registerCriticalDumpable [DumpableNotRegistered]
+                public class SomeClass implements Dumpable {
+                             ~~~~~~~~~
+                0 errors, 1 warnings
+                """)
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun classIsDumpable_usesNotDumpManagerMethod_violation() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+                    import com.android.systemui.OtherRegistrationObject;
+
+                    public class SomeClass implements Dumpable {
+                        public SomeClass(OtherRegistrationObject otherRegistrationObject) {
+                            otherRegistrationObject.registerDumpable(this);
+                        }
+                        @Override
+                        public void dump() {
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expect(
+                ("""
+                src/test/pkg/SomeClass.java:6: Warning: Any class implementing Dumpable must call DumpManager.registerNormalDumpable or DumpManager.registerCriticalDumpable [DumpableNotRegistered]
+                public class SomeClass implements Dumpable {
+                             ~~~~~~~~~
+                0 errors, 1 warnings
+                """)
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun classIsDumpableAndCoreStartable_noRegister_noViolation() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+                    import com.android.systemui.CoreStartable;
+
+                    public class SomeClass implements Dumpable, CoreStartable {
+                        @Override
+                        public void start() {
+                        }
+
+                        @Override
+                        public void dump() {
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun classIsAbstract_noRegister_noViolation() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+
+                    import com.android.systemui.Dumpable;
+
+                    public abstract class SomeClass implements Dumpable {
+                        void abstractMethodHere();
+
+                        @Override
+                        public void dump() {
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs,
+            )
+            .issues(DumpableNotRegisteredDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    companion object {
+        private val DUMPABLE_STUB =
+            TestFiles.java(
+                    """
+                    package com.android.systemui;
+
+                    import com.android.systemui.dump.DumpManager;
+                    import java.io.PrintWriter;
+
+                    public interface Dumpable {
+                        void dump();
+                    }
+                """
+                )
+                .indented()
+
+        private val DUMP_MANAGER_STUB =
+            TestFiles.java(
+                    """
+                    package com.android.systemui.dump;
+
+                    public interface DumpManager {
+                        void registerDumpable(Dumpable module);
+                        void registerNormalDumpable(Dumpable module);
+                        void registerCriticalDumpable(Dumpable module);
+                    }
+                """
+                )
+                .indented()
+
+        private val OTHER_REGISTRATION_OBJECT_STUB =
+            TestFiles.java(
+                    """
+                    package com.android.systemui;
+
+                    public interface OtherRegistrationObject {
+                        void registerDumpable(Dumpable module);
+                    }
+                """
+                )
+                .indented()
+
+        private val CORE_STARTABLE_STUB =
+            TestFiles.java(
+                    """
+                    package com.android.systemui;
+
+                    public interface CoreStartable {
+                        void start();
+                    }
+                """
+                )
+                .indented()
+
+        private val stubs =
+            arrayOf(
+                DUMPABLE_STUB,
+                DUMP_MANAGER_STUB,
+                OTHER_REGISTRATION_OBJECT_STUB,
+                CORE_STARTABLE_STUB,
+            )
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
index c742cd3..cfc38df 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
@@ -41,6 +41,7 @@
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCompositionContext
@@ -167,25 +168,25 @@
     val contentColor = controller.contentColor
     val shape = controller.shape
 
-    // TODO(b/230830644): Use movableContentOf to preserve the content state instead once the
-    // Compose libraries have been updated and include aosp/2163631.
     val wrappedContent =
-        @Composable { controller: ExpandableController ->
-            CompositionLocalProvider(
-                LocalContentColor provides contentColor,
-            ) {
-                // We make sure that the content itself (wrapped by the background) is at least
-                // 40.dp, which is the same as the M3 buttons. This applies even if onClick is
-                // null, to make it easier to write expandables that are sometimes clickable and
-                // sometimes not. There shouldn't be any Expandable smaller than 40dp because if
-                // the expandable is not clickable directly, then something in its content should
-                // be (and with a size >= 40dp).
-                val minSize = 40.dp
-                Box(
-                    Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize),
-                    contentAlignment = Alignment.Center,
+        remember(content) {
+            movableContentOf { expandable: Expandable ->
+                CompositionLocalProvider(
+                    LocalContentColor provides contentColor,
                 ) {
-                    content(controller.expandable)
+                    // We make sure that the content itself (wrapped by the background) is at least
+                    // 40.dp, which is the same as the M3 buttons. This applies even if onClick is
+                    // null, to make it easier to write expandables that are sometimes clickable and
+                    // sometimes not. There shouldn't be any Expandable smaller than 40dp because if
+                    // the expandable is not clickable directly, then something in its content
+                    // should be (and with a size >= 40dp).
+                    val minSize = 40.dp
+                    Box(
+                        Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize),
+                        contentAlignment = Alignment.Center,
+                    ) {
+                        content(expandable)
+                    }
                 }
             }
         }
@@ -254,7 +255,7 @@
                     .onGloballyPositioned {
                         controller.boundsInComposeViewRoot.value = it.boundsInRoot()
                     }
-            ) { wrappedContent(controller) }
+            ) { wrappedContent(controller.expandable) }
         }
         else -> {
             val clickModifier =
@@ -285,7 +286,7 @@
                         controller.boundsInComposeViewRoot.value = it.boundsInRoot()
                     },
             ) {
-                wrappedContent(controller)
+                wrappedContent(controller.expandable)
             }
         }
     }
@@ -299,7 +300,7 @@
     animatorState: State<LaunchAnimator.State?>,
     overlay: ViewGroupOverlay,
     controller: ExpandableControllerImpl,
-    content: @Composable (ExpandableController) -> Unit,
+    content: @Composable (Expandable) -> Unit,
     composeViewRoot: View,
     onOverlayComposeViewChanged: (View?) -> Unit,
     density: Density,
@@ -354,7 +355,7 @@
                             // We center the content in the expanding container.
                             contentAlignment = Alignment.Center,
                         ) {
-                            Box(contentModifier) { content(controller) }
+                            Box(contentModifier) { content(controller.expandable) }
                         }
                     }
                 }
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 6e728ce..e253fb9 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -17,13 +17,21 @@
 
 package com.android.systemui.compose
 
+import android.content.Context
+import android.view.View
 import androidx.activity.ComponentActivity
+import androidx.lifecycle.LifecycleOwner
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 
 /** The Compose facade, when Compose is *not* available. */
 object ComposeFacade : BaseComposeFacade {
     override fun isComposeAvailable(): Boolean = false
 
+    override fun composeInitializer(): ComposeInitializer {
+        throwComposeUnavailableError()
+    }
+
     override fun setPeopleSpaceActivityContent(
         activity: ComponentActivity,
         viewModel: PeopleViewModel,
@@ -32,7 +40,15 @@
         throwComposeUnavailableError()
     }
 
-    private fun throwComposeUnavailableError() {
+    override fun createFooterActionsView(
+        context: Context,
+        viewModel: FooterActionsViewModel,
+        qsVisibilityLifecycleOwner: LifecycleOwner
+    ): View {
+        throwComposeUnavailableError()
+    }
+
+    private fun throwComposeUnavailableError(): Nothing {
         error(
             "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
                 " other function on ComposeFacade."
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 6991ff8..1ea18fe 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -16,16 +16,24 @@
 
 package com.android.systemui.compose
 
+import android.content.Context
+import android.view.View
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
+import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.LifecycleOwner
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.people.ui.compose.PeopleScreen
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.compose.FooterActions
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 
 /** The Compose facade, when Compose is available. */
 object ComposeFacade : BaseComposeFacade {
     override fun isComposeAvailable(): Boolean = true
 
+    override fun composeInitializer(): ComposeInitializer = ComposeInitializerImpl
+
     override fun setPeopleSpaceActivityContent(
         activity: ComponentActivity,
         viewModel: PeopleViewModel,
@@ -33,4 +41,14 @@
     ) {
         activity.setContent { PlatformTheme { PeopleScreen(viewModel, onResult) } }
     }
+
+    override fun createFooterActionsView(
+        context: Context,
+        viewModel: FooterActionsViewModel,
+        qsVisibilityLifecycleOwner: LifecycleOwner,
+    ): View {
+        return ComposeView(context).apply {
+            setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt
new file mode 100644
index 0000000..772c891
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.compose
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryController
+import androidx.savedstate.SavedStateRegistryOwner
+import com.android.compose.animation.ViewTreeSavedStateRegistryOwner
+import com.android.systemui.lifecycle.ViewLifecycleOwner
+
+internal object ComposeInitializerImpl : ComposeInitializer {
+    override fun onAttachedToWindow(root: View) {
+        if (ViewTreeLifecycleOwner.get(root) != null) {
+            error("root $root already has a LifecycleOwner")
+        }
+
+        val parent = root.parent
+        if (parent is View && parent.id != android.R.id.content) {
+            error(
+                "ComposeInitializer.onAttachedToWindow(View) must be called on the content child." +
+                    "Outside of activities and dialogs, this is usually the top-most View of a " +
+                    "window."
+            )
+        }
+
+        // The lifecycle owner, which is STARTED when [root] is visible and RESUMED when [root] is
+        // both visible and focused.
+        val lifecycleOwner = ViewLifecycleOwner(root)
+
+        // We create a trivial implementation of [SavedStateRegistryOwner] that does not do any save
+        // or restore because SystemUI process is always running and top-level windows using this
+        // initializer are created once, when the process is started.
+        val savedStateRegistryOwner =
+            object : SavedStateRegistryOwner {
+                private val savedStateRegistry =
+                    SavedStateRegistryController.create(this).apply { performRestore(null) }
+
+                override fun getLifecycle(): Lifecycle = lifecycleOwner.lifecycle
+
+                override fun getSavedStateRegistry(): SavedStateRegistry {
+                    return savedStateRegistry.savedStateRegistry
+                }
+            }
+
+        // We must call [ViewLifecycleOwner.onCreate] after creating the [SavedStateRegistryOwner]
+        // because `onCreate` might move the lifecycle state to STARTED which will make
+        // [SavedStateRegistryController.performRestore] throw.
+        lifecycleOwner.onCreate()
+
+        // Set the owners on the root. They will be reused by any ComposeView inside the root
+        // hierarchy.
+        ViewTreeLifecycleOwner.set(root, lifecycleOwner)
+        ViewTreeSavedStateRegistryOwner.set(root, savedStateRegistryOwner)
+    }
+
+    override fun onDetachedFromWindow(root: View) {
+        (ViewTreeLifecycleOwner.get(root) as ViewLifecycleOwner).onDestroy()
+        ViewTreeLifecycleOwner.set(root, null)
+        ViewTreeSavedStateRegistryOwner.set(root, null)
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt
new file mode 100644
index 0000000..9eb78e1
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.compose.modifiers
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+
+/**
+ * Set a test tag on this node so that it is associated with [resId]. This node will then be
+ * accessible by integration tests using `sysuiResSelector(resId)`.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.sysuiResTag(resId: String): Modifier {
+    return this.semantics { testTagsAsResourceId = true }.testTag("com.android.systemui:id/$resId")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 23dacf9..3eeadae 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -51,6 +51,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.R
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 
@@ -110,7 +111,9 @@
     recentTiles: List<PeopleTileViewModel>,
     onTileClicked: (PeopleTileViewModel) -> Unit,
 ) {
-    Column {
+    Column(
+        Modifier.sysuiResTag("top_level_with_conversations"),
+    ) {
         Column(
             Modifier.fillMaxWidth().padding(PeopleSpacePadding),
             horizontalAlignment = Alignment.CenterHorizontally,
@@ -132,7 +135,7 @@
         }
 
         LazyColumn(
-            Modifier.fillMaxWidth(),
+            Modifier.fillMaxWidth().sysuiResTag("scroll_view"),
             contentPadding =
                 PaddingValues(
                     top = 16.dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index 5c5ceef..349f5c3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -73,6 +73,7 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
@@ -180,9 +181,9 @@
 
             security?.let { SecurityButton(it, Modifier.weight(1f)) }
             foregroundServices?.let { ForegroundServicesButton(it) }
-            userSwitcher?.let { IconButton(it) }
-            IconButton(viewModel.settings)
-            viewModel.power?.let { IconButton(it) }
+            userSwitcher?.let { IconButton(it, Modifier.sysuiResTag("multi_user_switch")) }
+            IconButton(viewModel.settings, Modifier.sysuiResTag("settings_button_container"))
+            viewModel.power?.let { IconButton(it, Modifier.sysuiResTag("pm_lite")) }
         }
     }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 462b90a..86bd5f2 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -54,7 +54,6 @@
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-    var tag: String = "UnnamedClockView"
     var logBuffer: LogBuffer? = null
 
     private val time = Calendar.getInstance()
@@ -132,7 +131,7 @@
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
-        logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
+        logBuffer?.log(TAG, DEBUG, "onAttachedToWindow")
         refreshFormat()
     }
 
@@ -148,7 +147,7 @@
         time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
-        logBuffer?.log(tag, DEBUG,
+        logBuffer?.log(TAG, DEBUG,
                 { str1 = formattedText?.toString() },
                 { "refreshTime: new formattedText=$str1" }
         )
@@ -157,7 +156,7 @@
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
-            logBuffer?.log(tag, DEBUG,
+            logBuffer?.log(TAG, DEBUG,
                     { str1 = formattedText?.toString() },
                     { "refreshTime: done setting new time text to: $str1" }
             )
@@ -167,17 +166,17 @@
             // without being notified TextInterpolator being notified.
             if (layout != null) {
                 textAnimator?.updateLayout(layout)
-                logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
+                logBuffer?.log(TAG, DEBUG, "refreshTime: done updating textAnimator layout")
             }
             requestLayout()
-            logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
+            logBuffer?.log(TAG, DEBUG, "refreshTime: after requestLayout")
         }
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
         time.timeZone = timeZone
         refreshFormat()
-        logBuffer?.log(tag, DEBUG,
+        logBuffer?.log(TAG, DEBUG,
                 { str1 = timeZone?.toString() },
                 { "onTimeZoneChanged newTimeZone=$str1" }
         )
@@ -194,7 +193,7 @@
         } else {
             animator.updateLayout(layout)
         }
-        logBuffer?.log(tag, DEBUG, "onMeasure")
+        logBuffer?.log(TAG, DEBUG, "onMeasure")
     }
 
     override fun onDraw(canvas: Canvas) {
@@ -206,12 +205,12 @@
         } else {
             super.onDraw(canvas)
         }
-        logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
+        logBuffer?.log(TAG, DEBUG, "onDraw")
     }
 
     override fun invalidate() {
         super.invalidate()
-        logBuffer?.log(tag, DEBUG, "invalidate")
+        logBuffer?.log(TAG, DEBUG, "invalidate")
     }
 
     override fun onTextChanged(
@@ -221,7 +220,7 @@
             lengthAfter: Int
     ) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
-        logBuffer?.log(tag, DEBUG,
+        logBuffer?.log(TAG, DEBUG,
                 { str1 = text.toString() },
                 { "onTextChanged text=$str1" }
         )
@@ -238,7 +237,7 @@
     }
 
     fun animateColorChange() {
-        logBuffer?.log(tag, DEBUG, "animateColorChange")
+        logBuffer?.log(TAG, DEBUG, "animateColorChange")
         setTextStyle(
             weight = lockScreenWeight,
             textSize = -1f,
@@ -260,7 +259,7 @@
     }
 
     fun animateAppearOnLockscreen() {
-        logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
+        logBuffer?.log(TAG, DEBUG, "animateAppearOnLockscreen")
         setTextStyle(
             weight = dozingWeight,
             textSize = -1f,
@@ -285,7 +284,7 @@
         if (isAnimationEnabled && textAnimator == null) {
             return
         }
-        logBuffer?.log(tag, DEBUG, "animateFoldAppear")
+        logBuffer?.log(TAG, DEBUG, "animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
             textSize = -1f,
@@ -312,7 +311,7 @@
             // Skip charge animation if dozing animation is already playing.
             return
         }
-        logBuffer?.log(tag, DEBUG, "animateCharge")
+        logBuffer?.log(TAG, DEBUG, "animateCharge")
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -336,7 +335,7 @@
     }
 
     fun animateDoze(isDozing: Boolean, animate: Boolean) {
-        logBuffer?.log(tag, DEBUG, "animateDoze")
+        logBuffer?.log(TAG, DEBUG, "animateDoze")
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
             textSize = -1f,
@@ -455,7 +454,7 @@
             isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
             else -> DOUBLE_LINE_FORMAT_12_HOUR
         }
-        logBuffer?.log(tag, DEBUG,
+        logBuffer?.log(TAG, DEBUG,
                 { str1 = format?.toString() },
                 { "refreshFormat format=$str1" }
         )
@@ -466,6 +465,7 @@
 
     fun dump(pw: PrintWriter) {
         pw.println("$this")
+        pw.println("    alpha=$alpha")
         pw.println("    measuredWidth=$measuredWidth")
         pw.println("    measuredHeight=$measuredHeight")
         pw.println("    singleLineInternal=$isSingleLineInternal")
@@ -626,7 +626,7 @@
     }
 
     companion object {
-        private val TAG = AnimatableClockView::class.simpleName
+        private val TAG = AnimatableClockView::class.simpleName!!
         const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
         private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
         private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 59b4848..00c0a0b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -13,83 +13,180 @@
  */
 package com.android.systemui.shared.clocks
 
+import android.app.ActivityManager
+import android.app.UserSwitchObserver
 import android.content.Context
 import android.database.ContentObserver
 import android.graphics.drawable.Drawable
 import android.net.Uri
-import android.os.Handler
+import android.os.UserHandle
 import android.provider.Settings
 import android.util.Log
 import androidx.annotation.OpenForTesting
-import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
 import com.android.systemui.plugins.ClockProvider
 import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.ClockSettings
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.plugins.PluginManager
-import org.json.JSONObject
+import com.android.systemui.util.Assert
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
-private val TAG = ClockRegistry::class.simpleName
+private val TAG = ClockRegistry::class.simpleName!!
 private const val DEBUG = true
+private val KEY_TIMESTAMP = "appliedTimestamp"
 
 /** ClockRegistry aggregates providers and plugins */
 open class ClockRegistry(
     val context: Context,
     val pluginManager: PluginManager,
-    val handler: Handler,
+    val scope: CoroutineScope,
+    val mainDispatcher: CoroutineDispatcher,
+    val bgDispatcher: CoroutineDispatcher,
     val isEnabled: Boolean,
-    userHandle: Int,
+    val handleAllUsers: Boolean,
     defaultClockProvider: ClockProvider,
     val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
 ) {
-    // Usually this would be a typealias, but a SAM provides better java interop
-    fun interface ClockChangeListener {
-        fun onClockChanged()
+    interface ClockChangeListener {
+        // Called when the active clock changes
+        fun onCurrentClockChanged() {}
+
+        // Called when the list of available clocks changes
+        fun onAvailableClocksChanged() {}
     }
 
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
-    private val settingObserver = object : ContentObserver(handler) {
-        override fun onChange(selfChange: Boolean, uris: Collection<Uri>, flags: Int, userId: Int) =
-            clockChangeListeners.forEach { it.onClockChanged() }
-    }
-
-    private val pluginListener = object : PluginListener<ClockProviderPlugin> {
-        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
-            connectClocks(plugin)
-
-        override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
-            disconnectClocks(plugin)
-    }
-
-    open var currentClockId: ClockId
-        get() {
-            return try {
-                val json = Settings.Secure.getString(
-                    context.contentResolver,
-                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
-                )
-                if (json == null || json.isEmpty()) {
-                    return fallbackClockId
-                }
-                ClockSetting.deserialize(json).clockId
-            } catch (ex: Exception) {
-                Log.e(TAG, "Failed to parse clock setting", ex)
-                fallbackClockId
+    private val settingObserver =
+        object : ContentObserver(null) {
+            override fun onChange(
+                selfChange: Boolean,
+                uris: Collection<Uri>,
+                flags: Int,
+                userId: Int
+            ) {
+                scope.launch(bgDispatcher) { querySettings() }
             }
         }
-        set(value) {
+
+    private val pluginListener =
+        object : PluginListener<ClockProviderPlugin> {
+            override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
+                connectClocks(plugin)
+
+            override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
+                disconnectClocks(plugin)
+        }
+
+    private val userSwitchObserver =
+        object : UserSwitchObserver() {
+            override fun onUserSwitchComplete(newUserId: Int) {
+                scope.launch(bgDispatcher) { querySettings() }
+            }
+        }
+
+    // TODO(b/267372164): Migrate to flows
+    var settings: ClockSettings? = null
+        get() = field
+        protected set(value) {
+            if (field != value) {
+                field = value
+                scope.launch(mainDispatcher) { onClockChanged { it.onCurrentClockChanged() } }
+            }
+        }
+
+    var isRegistered: Boolean = false
+        private set
+
+    @OpenForTesting
+    open fun querySettings() {
+        assertNotMainThread()
+        val result =
             try {
-                val json = ClockSetting.serialize(ClockSetting(value, System.currentTimeMillis()))
+                val json =
+                    if (handleAllUsers) {
+                        Settings.Secure.getStringForUser(
+                            context.contentResolver,
+                            Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
+                            ActivityManager.getCurrentUser()
+                        )
+                    } else {
+                        Settings.Secure.getString(
+                            context.contentResolver,
+                            Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+                        )
+                    }
+
+                ClockSettings.deserialize(json)
+            } catch (ex: Exception) {
+                Log.e(TAG, "Failed to parse clock settings", ex)
+                null
+            }
+        settings = result
+    }
+
+    @OpenForTesting
+    open fun applySettings(value: ClockSettings?) {
+        assertNotMainThread()
+
+        try {
+            value?.metadata?.put(KEY_TIMESTAMP, System.currentTimeMillis())
+
+            val json = ClockSettings.serialize(value)
+            if (handleAllUsers) {
+                Settings.Secure.putStringForUser(
+                    context.contentResolver,
+                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
+                    json,
+                    ActivityManager.getCurrentUser()
+                )
+            } else {
                 Settings.Secure.putString(
                     context.contentResolver,
-                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
+                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
+                    json
                 )
-            } catch (ex: Exception) {
-                Log.e(TAG, "Failed to set clock setting", ex)
             }
+        } catch (ex: Exception) {
+            Log.e(TAG, "Failed to set clock settings", ex)
+        }
+        settings = value
+    }
+
+    @OpenForTesting
+    protected open fun assertMainThread() {
+        Assert.isMainThread()
+    }
+
+    @OpenForTesting
+    protected open fun assertNotMainThread() {
+        Assert.isNotMainThread()
+    }
+
+    private fun onClockChanged(func: (ClockChangeListener) -> Unit) {
+        assertMainThread()
+        clockChangeListeners.forEach(func)
+    }
+
+    public fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) {
+        scope.launch(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) }
+    }
+
+    var currentClockId: ClockId
+        get() = settings?.clockId ?: fallbackClockId
+        set(value) {
+            mutateSetting { it.copy(clockId = value) }
+        }
+
+    var seedColor: Int?
+        get() = settings?.seedColor
+        set(value) {
+            mutateSetting { it.copy(seedColor = value) }
         }
 
     init {
@@ -99,23 +196,56 @@
                 "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
             )
         }
+    }
 
-        if (isEnabled) {
-            pluginManager.addPluginListener(
-                pluginListener,
-                ClockProviderPlugin::class.java,
-                /*allowMultiple=*/ true
-            )
+    fun registerListeners() {
+        if (!isEnabled || isRegistered) {
+            return
+        }
+
+        isRegistered = true
+
+        pluginManager.addPluginListener(
+            pluginListener,
+            ClockProviderPlugin::class.java,
+            /*allowMultiple=*/ true
+        )
+
+        scope.launch(bgDispatcher) { querySettings() }
+        if (handleAllUsers) {
             context.contentResolver.registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
                 /*notifyForDescendants=*/ false,
                 settingObserver,
-                userHandle
+                UserHandle.USER_ALL
+            )
+
+            ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
+        } else {
+            context.contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+                /*notifyForDescendants=*/ false,
+                settingObserver
             )
         }
     }
 
+    fun unregisterListeners() {
+        if (!isRegistered) {
+            return
+        }
+
+        isRegistered = false
+
+        pluginManager.removePluginListener(pluginListener)
+        context.contentResolver.unregisterContentObserver(settingObserver)
+        if (handleAllUsers) {
+            ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver)
+        }
+    }
+
     private fun connectClocks(provider: ClockProvider) {
+        var isAvailableChanged = false
         val currentId = currentClockId
         for (clock in provider.getClocks()) {
             val id = clock.clockId
@@ -126,10 +256,11 @@
                     "Clock Id conflict: $id is registered by both " +
                         "${provider::class.simpleName} and ${current.provider::class.simpleName}"
                 )
-                return
+                continue
             }
 
             availableClocks[id] = ClockInfo(clock, provider)
+            isAvailableChanged = true
             if (DEBUG) {
                 Log.i(TAG, "Added ${clock.clockId}")
             }
@@ -138,28 +269,38 @@
                 if (DEBUG) {
                     Log.i(TAG, "Current clock ($currentId) was connected")
                 }
-                clockChangeListeners.forEach { it.onClockChanged() }
+                onClockChanged { it.onCurrentClockChanged() }
             }
         }
+
+        if (isAvailableChanged) {
+            onClockChanged { it.onAvailableClocksChanged() }
+        }
     }
 
     private fun disconnectClocks(provider: ClockProvider) {
+        var isAvailableChanged = false
         val currentId = currentClockId
         for (clock in provider.getClocks()) {
             availableClocks.remove(clock.clockId)
+            isAvailableChanged = true
+
             if (DEBUG) {
                 Log.i(TAG, "Removed ${clock.clockId}")
             }
 
             if (currentId == clock.clockId) {
                 Log.w(TAG, "Current clock ($currentId) was disconnected")
-                clockChangeListeners.forEach { it.onClockChanged() }
+                onClockChanged { it.onCurrentClockChanged() }
             }
         }
+
+        if (isAvailableChanged) {
+            onClockChanged { it.onAvailableClocksChanged() }
+        }
     }
 
-    @OpenForTesting
-    open fun getClocks(): List<ClockMetadata> {
+    fun getClocks(): List<ClockMetadata> {
         if (!isEnabled) {
             return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata)
         }
@@ -194,36 +335,16 @@
         return createClock(DEFAULT_CLOCK_ID)!!
     }
 
-    private fun createClock(clockId: ClockId): ClockController? =
-        availableClocks[clockId]?.provider?.createClock(clockId)
+    private fun createClock(targetClockId: ClockId): ClockController? {
+        var settings = this.settings ?: ClockSettings()
+        if (targetClockId != settings.clockId) {
+            settings = settings.copy(clockId = targetClockId)
+        }
+        return availableClocks[targetClockId]?.provider?.createClock(settings)
+    }
 
     private data class ClockInfo(
         val metadata: ClockMetadata,
-        val provider: ClockProvider
+        val provider: ClockProvider,
     )
-
-    @Keep
-    data class ClockSetting(
-        val clockId: ClockId,
-        val _applied_timestamp: Long?
-    ) {
-        companion object {
-            private val KEY_CLOCK_ID = "clockId"
-            private val KEY_TIMESTAMP = "_applied_timestamp"
-
-            fun serialize(setting: ClockSetting): String {
-                return JSONObject()
-                    .put(KEY_CLOCK_ID, setting.clockId)
-                    .put(KEY_TIMESTAMP, setting._applied_timestamp)
-                    .toString()
-            }
-
-            fun deserialize(jsonStr: String): ClockSetting {
-                val json = JSONObject(jsonStr)
-                return ClockSetting(
-                    json.getString(KEY_CLOCK_ID),
-                    if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null)
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index e138ef8..4df7a44 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.ClockSettings
 import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import java.util.Locale
@@ -46,6 +47,7 @@
     ctx: Context,
     private val layoutInflater: LayoutInflater,
     private val resources: Resources,
+    private val settings: ClockSettings?,
 ) : ClockController {
     override val smallClock: DefaultClockFaceController
     override val largeClock: LargeClockFaceController
@@ -66,12 +68,14 @@
         smallClock =
             DefaultClockFaceController(
                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
-                    as AnimatableClockView
+                    as AnimatableClockView,
+                settings?.seedColor
             )
         largeClock =
             LargeClockFaceController(
                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
-                    as AnimatableClockView
+                    as AnimatableClockView,
+                settings?.seedColor
             )
         clocks = listOf(smallClock.view, largeClock.view)
 
@@ -85,18 +89,13 @@
         animations = DefaultClockAnimations(dozeFraction, foldFraction)
         events.onColorPaletteChanged(resources)
         events.onTimeZoneChanged(TimeZone.getDefault())
-        events.onTimeTick()
-    }
-
-    override fun setLogBuffer(logBuffer: LogBuffer) {
-        smallClock.view.tag = "smallClockView"
-        largeClock.view.tag = "largeClockView"
-        smallClock.view.logBuffer = logBuffer
-        largeClock.view.logBuffer = logBuffer
+        smallClock.events.onTimeTick()
+        largeClock.events.onTimeTick()
     }
 
     open inner class DefaultClockFaceController(
         override val view: AnimatableClockView,
+        var seedColor: Int?,
     ) : ClockFaceController {
 
         // MAGENTA is a placeholder, and will be assigned correctly in initialize
@@ -104,12 +103,23 @@
         private var isRegionDark = false
         protected var targetRegion: Rect? = null
 
+        override var logBuffer: LogBuffer?
+            get() = view.logBuffer
+            set(value) {
+                view.logBuffer = value
+            }
+
         init {
-            view.setColors(currentColor, currentColor)
+            if (seedColor != null) {
+                currentColor = seedColor!!
+            }
+            view.setColors(DOZE_COLOR, currentColor)
         }
 
         override val events =
             object : ClockFaceEvents {
+                override fun onTimeTick() = view.refreshTime()
+
                 override fun onRegionDarknessChanged(isRegionDark: Boolean) {
                     this@DefaultClockFaceController.isRegionDark = isRegionDark
                     updateColor()
@@ -130,7 +140,9 @@
 
         fun updateColor() {
             val color =
-                if (isRegionDark) {
+                if (seedColor != null) {
+                    seedColor!!
+                } else if (isRegionDark) {
                     resources.getColor(android.R.color.system_accent1_100)
                 } else {
                     resources.getColor(android.R.color.system_accent2_600)
@@ -150,7 +162,8 @@
 
     inner class LargeClockFaceController(
         view: AnimatableClockView,
-    ) : DefaultClockFaceController(view) {
+        seedColor: Int?,
+    ) : DefaultClockFaceController(view, seedColor) {
         override fun recomputePadding(targetRegion: Rect?) {
             // We center the view within the targetRegion instead of within the parent
             // view by computing the difference and adding that to the padding.
@@ -170,8 +183,6 @@
     }
 
     inner class DefaultClockEvents : ClockEvents {
-        override fun onTimeTick() = clocks.forEach { it.refreshTime() }
-
         override fun onTimeFormatChanged(is24Hr: Boolean) =
             clocks.forEach { it.refreshFormat(is24Hr) }
 
@@ -183,6 +194,14 @@
             smallClock.updateColor()
         }
 
+        override fun onSeedColorChanged(seedColor: Int?) {
+            largeClock.seedColor = seedColor
+            smallClock.seedColor = seedColor
+
+            largeClock.updateColor()
+            smallClock.updateColor()
+        }
+
         override fun onLocaleChanged(locale: Locale) {
             val nf = NumberFormat.getInstance(locale)
             if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 4c0504b..0fd1b49 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
 import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.plugins.ClockSettings
 
 private val TAG = DefaultClockProvider::class.simpleName
 const val DEFAULT_CLOCK_NAME = "Default Clock"
@@ -36,12 +37,12 @@
     override fun getClocks(): List<ClockMetadata> =
         listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
 
-    override fun createClock(id: ClockId): ClockController {
-        if (id != DEFAULT_CLOCK_ID) {
-            throw IllegalArgumentException("$id is unsupported by $TAG")
+    override fun createClock(settings: ClockSettings): ClockController {
+        if (settings.clockId != DEFAULT_CLOCK_ID) {
+            throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
         }
 
-        return DefaultClockController(ctx, layoutInflater, resources)
+        return DefaultClockController(ctx, layoutInflater, resources, settings)
     }
 
     override fun getClockThumbnail(id: ClockId): Drawable? {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/shared/model/ClockPreviewConstants.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/customization/src/com/android/systemui/shared/clocks/shared/model/ClockPreviewConstants.kt
index 67733e9..6a77936 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/shared/model/ClockPreviewConstants.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,12 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
+ *
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.shared.clocks.shared.model
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+object ClockPreviewConstants {
+    const val KEY_HIDE_CLOCK = "hide_clock"
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
index 5bb3707..cd9fb88 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
@@ -20,12 +20,15 @@
 import android.annotation.SuppressLint
 import android.content.ContentValues
 import android.content.Context
+import android.content.Intent
 import android.database.ContentObserver
 import android.graphics.Color
 import android.graphics.drawable.Drawable
 import android.net.Uri
+import android.util.Log
 import androidx.annotation.DrawableRes
 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
+import java.net.URISyntaxException
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -169,6 +172,8 @@
          * If `null`, the button should not be shown.
          */
         val enablementActionComponentName: String? = null,
+        /** Optional [Intent] to use to start an activity to configure this affordance. */
+        val configureIntent: Intent? = null,
     )
 
     /** Models a selection of a quick affordance on a slot. */
@@ -337,6 +342,11 @@
                                 Contract.LockScreenQuickAffordances.AffordanceTable.Columns
                                     .ENABLEMENT_COMPONENT_NAME
                             )
+                        val configureIntentColumnIndex =
+                            cursor.getColumnIndex(
+                                Contract.LockScreenQuickAffordances.AffordanceTable.Columns
+                                    .CONFIGURE_INTENT
+                            )
                         if (
                             idColumnIndex == -1 ||
                                 nameColumnIndex == -1 ||
@@ -344,15 +354,17 @@
                                 isEnabledColumnIndex == -1 ||
                                 enablementInstructionsColumnIndex == -1 ||
                                 enablementActionTextColumnIndex == -1 ||
-                                enablementComponentNameColumnIndex == -1
+                                enablementComponentNameColumnIndex == -1 ||
+                                configureIntentColumnIndex == -1
                         ) {
                             return@buildList
                         }
 
                         while (cursor.moveToNext()) {
+                            val affordanceId = cursor.getString(idColumnIndex)
                             add(
                                 CustomizationProviderClient.Affordance(
-                                    id = cursor.getString(idColumnIndex),
+                                    id = affordanceId,
                                     name = cursor.getString(nameColumnIndex),
                                     iconResourceId = cursor.getInt(iconColumnIndex),
                                     isEnabled = cursor.getInt(isEnabledColumnIndex) == 1,
@@ -367,6 +379,10 @@
                                         cursor.getString(enablementActionTextColumnIndex),
                                     enablementActionComponentName =
                                         cursor.getString(enablementComponentNameColumnIndex),
+                                    configureIntent =
+                                        cursor
+                                            .getString(configureIntentColumnIndex)
+                                            ?.toIntent(affordanceId = affordanceId),
                                 )
                             )
                         }
@@ -504,7 +520,19 @@
             .onStart { emit(Unit) }
     }
 
+    private fun String.toIntent(
+        affordanceId: String,
+    ): Intent? {
+        return try {
+            Intent.parseUri(this, 0)
+        } catch (e: URISyntaxException) {
+            Log.w(TAG, "Cannot parse Uri into Intent for affordance with ID \"$affordanceId\"!")
+            null
+        }
+    }
+
     companion object {
+        private const val TAG = "CustomizationProviderClient"
         private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
     }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index 1e2e7d2..c120876 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -113,6 +113,11 @@
                  * opens a destination where the user can re-enable the disabled affordance.
                  */
                 const val ENABLEMENT_COMPONENT_NAME = "enablement_action_intent"
+                /**
+                 * Byte array. Optional parcelled `Intent` to use to start an activity that can be
+                 * used to configure the affordance.
+                 */
+                const val CONFIGURE_INTENT = "configure_intent"
             }
         }
 
@@ -173,6 +178,12 @@
         /** Flag denoting whether the customizable clocks feature is enabled. */
         const val FLAG_NAME_CUSTOM_CLOCKS_ENABLED = "is_custom_clocks_feature_enabled"
 
+        /** Flag denoting whether the Wallpaper preview should use the full screen UI. */
+        const val FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW = "wallpaper_fullscreen_preview"
+
+        /** Flag denoting whether the Monochromatic Theme is enabled. */
+        const val FLAG_NAME_MONOCHROMATIC_THEME = "is_monochromatic_theme_enabled"
+
         object Columns {
             /** String. Unique ID for the flag. */
             const val NAME = "name"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
index 18e8a96..bf922bc 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
@@ -21,4 +21,5 @@
     const val MESSAGE_ID_SLOT_SELECTED = 1337
     const val KEY_SLOT_ID = "slot_id"
     const val KEY_INITIALLY_SELECTED_SLOT_ID = "initially_selected_slot_id"
+    const val KEY_HIGHLIGHT_QUICK_AFFORDANCES = "highlight_quick_affordances"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/Assert.java b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
similarity index 100%
rename from packages/SystemUI/src/com/android/systemui/util/Assert.java
rename to packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
diff --git a/packages/SystemUI/docs/dagger.md b/packages/SystemUI/docs/dagger.md
index 8917013..9b4c21e 100644
--- a/packages/SystemUI/docs/dagger.md
+++ b/packages/SystemUI/docs/dagger.md
@@ -8,105 +8,110 @@
 
  - [User's guide](https://google.github.io/dagger/users-guide)
 
-TODO: Add some links.
-
 ## State of the world
 
-Dagger 2 has been turned on for SystemUI and a early first pass has been taken
-for converting everything in [Dependency.java](packages/systemui/src/com/android/systemui/Dependency.java)
-to use Dagger. Since a lot of SystemUI depends on Dependency, stubs have been added to Dependency 
-to proxy any gets through to the instances provided by dagger, this will allow migration of SystemUI 
-through a number of CLs.
+Dagger 2 has been turned on for SystemUI and much of
+[Dependency.java](../src/com/android/systemui/Dependency.java)
+has been converted to use Dagger. Since a lot of SystemUI depends on Dependency,
+stubs have been added to Dependency to proxy any gets through to the instances
+provided by dagger, this will allow migration of SystemUI through a number of CLs.
 
 ### How it works in SystemUI
 
+There are three high level "scopes" of concern in SystemUI. They all represent
+singleton scopes, but serve different purposes.
+
+* `@Singleton` - Instances that are shared everywhere. There isn't a  lot of
+   code in this scope. Things like the main thread, and Android Framework
+   provided instances mostly.
+* `@WMShell` - WindowManager related code in the SystemUI process. We don't
+   want this code relying on the rest of SystemUI, and we don't want the rest
+   of SystemUI peeking into its internals, so it runs in its own Subcomponent.
+* `@SysUISingleton` - Most of what would be considered "SystemUI". Most feature
+   work by SystemUI developers goes into this scope. Useful interfaces from
+   WindowManager are made available inside this Subcomponent.
+
+The root dagger graph is created by an instance of `SystemUIInitializer`.
+See [README.md](../README.md) for more details.
 For the classes that we're using in Dependency and are switching to dagger, the
 equivalent dagger version is using `@Singleton` and therefore only has one instance.
 To have the single instance span all of SystemUI and be easily accessible for
 other components, there is a single root `@Component` that exists that generates
-these. The component lives in [SystemUIFactory](packages/systemui/src/com/android/systemui/SystemUIFactory.java)
-and is called `SystemUIRootComponent`.
-
-```java
-
-@Singleton
-@Component(modules = {SystemUIFactory.class, DependencyProvider.class, DependencyBinder.class,
-        ContextHolder.class})
-public interface SystemUIRootComponent {
-    @Singleton
-    Dependency.DependencyInjector createDependency();
-}
-```
-
-The root component is composed of root modules, which in turn provide the global singleton 
-dependencies across all of SystemUI.
-
-- `SystemUIFactory` `@Provides` dependencies that need to be overridden by SystemUI
-variants (like other form factors e.g. Car). 
-
-- `DependencyBinder` creates the mapping from interfaces to implementation classes. 
-
-- `DependencyProvider` provides or binds any remaining depedencies required.
-
-### Adding injection to a new SystemUI object
-
-SystemUI object are made injectable by adding an entry in `SystemUIBinder`. SystemUIApplication uses
-information in that file to locate and construct an instance of the requested SystemUI class.
+these. The component lives in
+[ReferenceGlobalRootComponent.java](../src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java).
 
 ### Adding a new injectable object
 
-First tag the constructor with `@Inject`. Also tag it with `@Singleton` if only one
-instance should be created.
+First annotate the constructor with `@Inject`. Also annotate it with
+`@SysUISingleton` if only one instance should be created.
 
-```java
-@Singleton
-public class SomethingController {
-  @Inject
-  public SomethingController(Context context,
-    @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
-      // context and mainHandler will be automatically populated.
-  }
+```kotlin
+@SysUISingleton
+class FeatureStartable
+@Inject
+constructor(
+/* ... */
+) {
+    // ...
 }
 ```
 
-If you have an interface class and an implementation class, dagger needs to know
-how to map it. The simplest way to do this is to add an `@Provides` method to
-DependencyProvider. The type of the return value tells dagger which dependency it's providing.
+If you have an interface class and an implementation class, Dagger needs to
+know how to map it. The simplest way to do this is to add an `@Binds` method
+in a module. The type of the return value tells dagger which dependency it's
+providing:
 
-```java
-public class DependencyProvider {
-  //...
-  @Singleton
-  @Provides
-  public SomethingController provideSomethingController(Context context,
-      @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
-    return new SomethingControllerImpl(context, mainHandler);
-  }
+```kotlin
+@Module
+abstract class FeatureModule {
+    @Binds
+    abstract fun bindsFeature(impl: FeatureImpl): Feature
 }
 ```
 
-If you need to access this from Dependency#get, then add an adapter to Dependency
-that maps to the instance provided by Dagger. The changes should be similar
-to the following diff.
+If you have a class that you want to make injectable that has can not
+be easily constructed by Dagger, write a `@Provides` method for it:
 
-```java
-public class Dependency {
-  //...
-  @Inject Lazy<SomethingController> mSomethingController;
-  //...
-  public void start() {
-    //...
-    mProviders.put(SomethingController.class, mSomethingController::get);
-  }
+```kotlin
+@Module
+abstract class FeatureModule {
+    @Module
+    companion object {
+        @Provides
+        fun providesFeature(ctx: Context): Feature {
+            return FeatureImpl.constructFromContext(ctx)
+        }
+    }
 }
 ```
 
+### Module Organization
+
+Please define your modules on _at least_ per-package level. If the scope of a
+package grows to encompass a great number of features, create per-feature
+modules.
+
+**Do not create catch-all modules.** Those quickly grow unwieldy and
+unmaintainable. Any that exist today should be refactored into obsolescence.
+
+You can then include your module in one of three places:
+
+1) Within another module that depends on it. Ideally, this creates a clean
+   dependency graph between features and utilities.
+2) For features that should exist in all versions of SystemUI (AOSP and
+   any variants), include the module in
+   [SystemUIModule.java](../src/com/android/systemui/dagger/SystemUIModule.java).
+3) For features that should exist only in AOSP, include the module in
+   [ReferenceSystemUIModule.java](../src/com/android/systemui/dagger/ReferenceSystemUIModule.java).
+   Similarly, if you are working on a custom version of SystemUI and have code
+   specific to your version, include it in a module specific to your version.
+
 ### Using injection with Fragments
 
 Fragments are created as part of the FragmentManager, so they need to be
 setup so the manager knows how to create them. To do that, add a method
 to com.android.systemui.fragments.FragmentService$FragmentCreator that
-returns your fragment class. Thats all thats required, once the method
+returns your fragment class. That is all that is required, once the method
 exists, FragmentService will automatically pick it up and use injection
 whenever your fragment needs to be created.
 
@@ -123,48 +128,11 @@
 FragmentHostManager.get(view).create(NavigationBarFragment.class);
 ```
 
-### Using injection with Views
-
-DO NOT ADD NEW VIEW INJECTION. VIEW INJECTION IS BEING ACTIVELY DEPRECATED.
-
-Needing to inject objects into your View's constructor generally implies you
-are doing more work in your presentation layer than is advisable.
-Instead, create an injected controller for you view, inject into the
-controller, and then attach the view to the controller after inflation.
-
-View injection generally causes headaches while testing, as inflating a view
-(which may in turn inflate other views) implicitly causes a Dagger graph to 
-be stood up, which may or may not contain the appropriately 
-faked/mocked/stubbed objects. It is a hard to control process.
-
 ## Updating Dagger2
 
 We depend on the Dagger source found in external/dagger2. We should automatically pick up on updates
 when that repository is updated.
-
-*Deprecated:*
-
-Binaries can be downloaded from https://repo1.maven.org/maven2/com/google/dagger/ and then loaded
-into
-[/prebuilts/tools/common/m2/repository/com/google/dagger/](http://cs/android/prebuilts/tools/common/m2/repository/com/google/dagger/)
-
-The following commands should work, substituting in the version that you are looking for:
-
-````
-cd prebuilts/tools/common/m2/repository/com/google/dagger/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger/2.28.1/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.28.1/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-spi/2.28.1/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.28.1/
-````
-
-Then update `prebuilts/tools/common/m2/Android.bp` to point at your new jars.
  
 ## TODO List
 
- - Eliminate usages of Dependency#get
- - Add links in above TODO
+ - Eliminate usages of Dependency#get: http://b/hotlists/3940788
diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md
index ccb35fa..d662649 100644
--- a/packages/SystemUI/docs/device-entry/quickaffordance.md
+++ b/packages/SystemUI/docs/device-entry/quickaffordance.md
@@ -52,6 +52,15 @@
 * Unselect an already-selected quick affordance from a slot
 * Unselect all already-selected quick affordances from a slot
 
+## Device Policy
+Returning `DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL` or
+`DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL` from
+`DevicePolicyManager#getKeyguardDisabledFeatures` will disable the keyguard quick affordance feature on the device.
+
+## Testing
+* Add a unit test for your implementation of `KeyguardQuickAffordanceConfig`
+* Manually verify that your implementation works in multi-user environments from both the main user and a secondary user
+
 ## Debugging
 To see the current state of the system, you can run `dumpsys`:
 
diff --git a/packages/SystemUI/docs/qs-tiles.md b/packages/SystemUI/docs/qs-tiles.md
index 4cb765d..488f8c7 100644
--- a/packages/SystemUI/docs/qs-tiles.md
+++ b/packages/SystemUI/docs/qs-tiles.md
@@ -301,9 +301,13 @@
     * Use only `handleUpdateState` to modify the values of the state to the new ones. This can be done by polling controllers or through the `arg` parameter.
     * If the controller is not a `CallbackController`, respond to `handleSetListening` by attaching/dettaching from controllers.
     * Implement `isAvailable` so the tile will not be created when it's not necessary.
-4. In `QSFactoryImpl`:
-    * Inject a `Provider` for the tile created before.
-    * Add a case to the `switch` with a unique String spec for the chosen tile.
+4. Either create a new feature module or find an existing related feature module and add the following binding method:
+    * ```kotlin
+      @Binds
+      @IntoMap
+      @StringKey(YourNewTile.TILE_SPEC) // A unique word that will map to YourNewTile
+      fun bindYourNewTile(yourNewTile: YourNewTile): QSTileImpl<*>
+      ```
 5. In [SystemUI/res/values/config.xml](/packages/SystemUI/res/values/config.xml), modify `quick_settings_tiles_stock` and add the spec defined in the previous step. If necessary, add it also to `quick_settings_tiles_default`. The first one contains a list of all the tiles that SystemUI knows how to create (to show to the user in the customization screen). The second one contains only the default tiles that the user will experience on a fresh boot or after they reset their tiles.
 6. In [SystemUI/res/values/tiles_states_strings.xml](/packages/SystemUI/res/values/tiles_states_strings.xml), add a new array for your tile. The name has to be `tile_states_<spec>`. Use a good description to help the translators.
 7. In [`SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt`](/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt), add a new element to the map in `SubtitleArrayMapping` corresponding to the resource created in the previous step.
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index f53e3f6..943d799 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -1,8 +1,5 @@
 +packages/SystemUI
--packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
--packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
 -packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
--packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
 -packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
 -packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
 -packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
@@ -200,13 +197,10 @@
 -packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
 -packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt
 -packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
 -packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -322,8 +316,8 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
--packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
--packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -457,13 +451,6 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -614,7 +601,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
@@ -749,11 +735,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/ShadeExpansionStateManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryStateNotifierTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index dee0f5c..314c736 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -80,6 +80,7 @@
 internal class HueVibrantSecondary() : Hue {
     val hueToRotations = listOf(Pair(0, 18), Pair(41, 15), Pair(61, 10), Pair(101, 12),
             Pair(131, 15), Pair(181, 18), Pair(251, 15), Pair(301, 12), Pair(360, 12))
+
     override fun get(sourceColor: Cam): Double {
         return getHueRotation(sourceColor.hue, hueToRotations)
     }
@@ -88,6 +89,7 @@
 internal class HueVibrantTertiary() : Hue {
     val hueToRotations = listOf(Pair(0, 35), Pair(41, 30), Pair(61, 20), Pair(101, 25),
             Pair(131, 30), Pair(181, 35), Pair(251, 30), Pair(301, 25), Pair(360, 25))
+
     override fun get(sourceColor: Cam): Double {
         return getHueRotation(sourceColor.hue, hueToRotations)
     }
@@ -96,6 +98,7 @@
 internal class HueExpressiveSecondary() : Hue {
     val hueToRotations = listOf(Pair(0, 45), Pair(21, 95), Pair(51, 45), Pair(121, 20),
             Pair(151, 45), Pair(191, 90), Pair(271, 45), Pair(321, 45), Pair(360, 45))
+
     override fun get(sourceColor: Cam): Double {
         return getHueRotation(sourceColor.hue, hueToRotations)
     }
@@ -104,6 +107,7 @@
 internal class HueExpressiveTertiary() : Hue {
     val hueToRotations = listOf(Pair(0, 120), Pair(21, 120), Pair(51, 20), Pair(121, 45),
             Pair(151, 20), Pair(191, 15), Pair(271, 20), Pair(321, 120), Pair(360, 120))
+
     override fun get(sourceColor: Cam): Double {
         return getHueRotation(sourceColor.hue, hueToRotations)
     }
@@ -148,11 +152,11 @@
 }
 
 internal class CoreSpec(
-    val a1: TonalSpec,
-    val a2: TonalSpec,
-    val a3: TonalSpec,
-    val n1: TonalSpec,
-    val n2: TonalSpec
+        val a1: TonalSpec,
+        val a2: TonalSpec,
+        val a3: TonalSpec,
+        val n1: TonalSpec,
+        val n2: TonalSpec
 )
 
 enum class Style(internal val coreSpec: CoreSpec) {
@@ -214,51 +218,86 @@
     )),
 }
 
+class TonalPalette {
+    val shadeKeys = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
+    val allShades: List<Int>
+    val allShadesMapped: Map<Int, Int>
+    val baseColor: Int
+
+    internal constructor(spec: TonalSpec, seedColor: Int) {
+        val seedCam = Cam.fromInt(seedColor)
+        allShades = spec.shades(seedCam)
+        allShadesMapped = shadeKeys.zip(allShades).toMap()
+
+        val h = spec.hue.get(seedCam).toFloat()
+        val c = spec.chroma.get(seedCam).toFloat()
+        baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor))
+    }
+
+    val s10: Int get() = this.allShades[0]
+    val s50: Int get() = this.allShades[1]
+    val s100: Int get() = this.allShades[2]
+    val s200: Int get() = this.allShades[3]
+    val s300: Int get() = this.allShades[4]
+    val s400: Int get() = this.allShades[5]
+    val s500: Int get() = this.allShades[6]
+    val s600: Int get() = this.allShades[7]
+    val s700: Int get() = this.allShades[8]
+    val s800: Int get() = this.allShades[9]
+    val s900: Int get() = this.allShades[10]
+    val s1000: Int get() = this.allShades[11]
+}
+
 class ColorScheme(
-    @ColorInt val seed: Int,
-    val darkTheme: Boolean,
-    val style: Style = Style.TONAL_SPOT
+        @ColorInt val seed: Int,
+        val darkTheme: Boolean,
+        val style: Style = Style.TONAL_SPOT
 ) {
 
-    val accent1: List<Int>
-    val accent2: List<Int>
-    val accent3: List<Int>
-    val neutral1: List<Int>
-    val neutral2: List<Int>
+    val accent1: TonalPalette
+    val accent2: TonalPalette
+    val accent3: TonalPalette
+    val neutral1: TonalPalette
+    val neutral2: TonalPalette
 
     constructor(@ColorInt seed: Int, darkTheme: Boolean) :
             this(seed, darkTheme, Style.TONAL_SPOT)
 
     @JvmOverloads
     constructor(
-        wallpaperColors: WallpaperColors,
-        darkTheme: Boolean,
-        style: Style = Style.TONAL_SPOT
+            wallpaperColors: WallpaperColors,
+            darkTheme: Boolean,
+            style: Style = Style.TONAL_SPOT
     ) :
             this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
 
+    val allHues: List<TonalPalette>
+        get() {
+            return listOf(accent1, accent2, accent3, neutral1, neutral2)
+        }
+
     val allAccentColors: List<Int>
         get() {
             val allColors = mutableListOf<Int>()
-            allColors.addAll(accent1)
-            allColors.addAll(accent2)
-            allColors.addAll(accent3)
+            allColors.addAll(accent1.allShades)
+            allColors.addAll(accent2.allShades)
+            allColors.addAll(accent3.allShades)
             return allColors
         }
 
     val allNeutralColors: List<Int>
         get() {
             val allColors = mutableListOf<Int>()
-            allColors.addAll(neutral1)
-            allColors.addAll(neutral2)
+            allColors.addAll(neutral1.allShades)
+            allColors.addAll(neutral2.allShades)
             return allColors
         }
 
     val backgroundColor
-        get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1[8] else neutral1[0], 0xFF)
+        get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1.s700 else neutral1.s10, 0xFF)
 
     val accentColor
-        get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1[2] else accent1[6], 0xFF)
+        get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1.s100 else accent1.s500, 0xFF)
 
     init {
         val proposedSeedCam = Cam.fromInt(seed)
@@ -269,24 +308,26 @@
         } else {
             seed
         }
-        val camSeed = Cam.fromInt(seedArgb)
-        accent1 = style.coreSpec.a1.shades(camSeed)
-        accent2 = style.coreSpec.a2.shades(camSeed)
-        accent3 = style.coreSpec.a3.shades(camSeed)
-        neutral1 = style.coreSpec.n1.shades(camSeed)
-        neutral2 = style.coreSpec.n2.shades(camSeed)
+
+        accent1 = TonalPalette(style.coreSpec.a1, seedArgb)
+        accent2 = TonalPalette(style.coreSpec.a2, seedArgb)
+        accent3 = TonalPalette(style.coreSpec.a3, seedArgb)
+        neutral1 = TonalPalette(style.coreSpec.n1, seedArgb)
+        neutral2 = TonalPalette(style.coreSpec.n2, seedArgb)
     }
 
+    val shadeCount get() = this.accent1.allShades.size
+
     override fun toString(): String {
         return "ColorScheme {\n" +
                 "  seed color: ${stringForColor(seed)}\n" +
                 "  style: $style\n" +
                 "  palettes: \n" +
-                "  ${humanReadable("PRIMARY", accent1)}\n" +
-                "  ${humanReadable("SECONDARY", accent2)}\n" +
-                "  ${humanReadable("TERTIARY", accent3)}\n" +
-                "  ${humanReadable("NEUTRAL", neutral1)}\n" +
-                "  ${humanReadable("NEUTRAL VARIANT", neutral2)}\n" +
+                "  ${humanReadable("PRIMARY", accent1.allShades)}\n" +
+                "  ${humanReadable("SECONDARY", accent2.allShades)}\n" +
+                "  ${humanReadable("TERTIARY", accent3.allShades)}\n" +
+                "  ${humanReadable("NEUTRAL", neutral1.allShades)}\n" +
+                "  ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" +
                 "}"
     }
 
@@ -385,7 +426,8 @@
                     val existingSeedNearby = seeds.find {
                         val hueA = intToCam[int]!!.hue
                         val hueB = intToCam[it]!!.hue
-                        hueDiff(hueA, hueB) < i } != null
+                        hueDiff(hueA, hueB) < i
+                    } != null
                     if (existingSeedNearby) {
                         continue
                     }
@@ -460,9 +502,9 @@
         }
 
         private fun huePopulations(
-            camByColor: Map<Int, Cam>,
-            populationByColor: Map<Int, Double>,
-            filter: Boolean = true
+                camByColor: Map<Int, Cam>,
+                populationByColor: Map<Int, Double>,
+                filter: Boolean = true
         ): List<Double> {
             val huePopulation = List(size = 360, init = { 0.0 }).toMutableList()
 
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 7709f21..fb1c454 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -29,6 +29,7 @@
         "src/**/*.java",
         "src/**/*.kt",
         "bcsmartspace/src/**/*.java",
+        "bcsmartspace/src/**/*.kt",
     ],
 
     static_libs: [
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
new file mode 100644
index 0000000..509f022
--- /dev/null
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.plugins
+
+// TODO(b/265360975): Evaluate this plugin approach.
+/** Plugin to provide BC smartspace configuration */
+interface BcSmartspaceConfigPlugin {
+    /** Gets default date/weather disabled status. */
+    val isDefaultDateWeatherDisabled: Boolean
+}
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
index 51f5baa..e0d0184 100644
--- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
@@ -50,16 +50,24 @@
     String TAG = "BcSmartspaceDataPlugin";
 
     /** Register a listener to get Smartspace data. */
-    void registerListener(SmartspaceTargetListener listener);
+    default void registerListener(SmartspaceTargetListener listener) {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
 
     /** Unregister a listener. */
-    void unregisterListener(SmartspaceTargetListener listener);
+    default void unregisterListener(SmartspaceTargetListener listener) {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
 
     /** Register a SmartspaceEventNotifier. */
-    default void registerSmartspaceEventNotifier(SmartspaceEventNotifier notifier) {}
+    default void registerSmartspaceEventNotifier(SmartspaceEventNotifier notifier) {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
 
     /** Push a SmartspaceTargetEvent to the SmartspaceEventNotifier. */
-    default void notifySmartspaceEvent(SmartspaceTargetEvent event) {}
+    default void notifySmartspaceEvent(SmartspaceTargetEvent event) {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
 
     /** Allows for notifying the SmartspaceSession of SmartspaceTargetEvents. */
     interface SmartspaceEventNotifier {
@@ -72,16 +80,20 @@
      * will be responsible for correctly setting the LayoutParams
      */
     default SmartspaceView getView(ViewGroup parent) {
-        return null;
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
     }
 
     /**
      * As the smartspace view becomes available, allow listeners to receive an event.
      */
-    default void addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) { }
+    default void addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
 
     /** Updates Smartspace data and propagates it to any listeners. */
-    void onTargetsAvailable(List<SmartspaceTarget> targets);
+    default void onTargetsAvailable(List<SmartspaceTarget> targets) {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
 
     /** Provides Smartspace data to registered listeners. */
     interface SmartspaceTargetListener {
@@ -94,17 +106,18 @@
         void registerDataProvider(BcSmartspaceDataPlugin plugin);
 
         /**
+         * Sets {@link BcSmartspaceConfigPlugin}.
+         */
+        default void registerConfigProvider(BcSmartspaceConfigPlugin configProvider) {
+            throw new UnsupportedOperationException("Not implemented by " + getClass());
+        }
+
+        /**
          * Primary color for unprotected text
          */
         void setPrimaryTextColor(int color);
 
         /**
-         * When the view is displayed on Dream, set the flag to true, immediately after the view is
-         * created.
-         */
-        void setIsDreaming(boolean isDreaming);
-
-        /**
          * Set the UI surface for the cards. Should be called immediately after the view is created.
          */
         void setUiSurface(String uiSurface);
@@ -133,28 +146,38 @@
         /**
          * Set or clear Do Not Disturb information.
          */
-        void setDnd(@Nullable Drawable image, @Nullable String description);
+        default void setDnd(@Nullable Drawable image, @Nullable String description) {
+            throw new UnsupportedOperationException("Not implemented by " + getClass());
+        }
 
         /**
          * Set or clear next alarm information
          */
-        void setNextAlarm(@Nullable Drawable image, @Nullable String description);
+        default void setNextAlarm(@Nullable Drawable image, @Nullable String description) {
+            throw new UnsupportedOperationException("Not implemented by " + getClass());
+        }
 
         /**
          * Set or clear device media playing
          */
-        void setMediaTarget(@Nullable SmartspaceTarget target);
+        default void setMediaTarget(@Nullable SmartspaceTarget target) {
+            throw new UnsupportedOperationException("Not implemented by " + getClass());
+        }
 
         /**
          * Get the index of the currently selected page.
          */
-        int getSelectedPage();
+        default int getSelectedPage() {
+            throw new UnsupportedOperationException("Not implemented by " + getClass());
+        }
 
         /**
          * Return the top padding value from the currently visible card, or 0 if there is no current
          * card.
          */
-        int getCurrentCardTopPadding();
+        default int getCurrentCardTopPadding() {
+            throw new UnsupportedOperationException("Not implemented by " + getClass());
+        }
     }
 
     /** Interface for launching Intents, which can differ on the lockscreen */
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 66e44b9..babe5700 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -17,11 +17,13 @@
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.view.View
+import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.annotations.ProvidesInterface
 import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
+import org.json.JSONObject
 
 /** Identifies a clock design */
 typealias ClockId = String
@@ -41,7 +43,13 @@
     fun getClocks(): List<ClockMetadata>
 
     /** Initializes and returns the target clock design */
-    fun createClock(id: ClockId): ClockController
+    @Deprecated("Use overload with ClockSettings")
+    fun createClock(id: ClockId): ClockController {
+        return createClock(ClockSettings(id, null))
+    }
+
+    /** Initializes and returns the target clock design */
+    fun createClock(settings: ClockSettings): ClockController
 
     /** A static thumbnail for rendering in some examples */
     fun getClockThumbnail(id: ClockId): Drawable?
@@ -62,18 +70,20 @@
     val animations: ClockAnimations
 
     /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
-    fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+    fun initialize(
+        resources: Resources,
+        dozeFraction: Float,
+        foldFraction: Float,
+    ) {
         events.onColorPaletteChanged(resources)
         animations.doze(dozeFraction)
         animations.fold(foldFraction)
-        events.onTimeTick()
+        smallClock.events.onTimeTick()
+        largeClock.events.onTimeTick()
     }
 
     /** Optional method for dumping debug information */
     fun dump(pw: PrintWriter) {}
-
-    /** Optional method for debug logging */
-    fun setLogBuffer(logBuffer: LogBuffer) {}
 }
 
 /** Interface for a specific clock face version rendered by the clock */
@@ -83,13 +93,13 @@
 
     /** Events specific to this clock face */
     val events: ClockFaceEvents
+
+    /** Some clocks may log debug information */
+    var logBuffer: LogBuffer?
 }
 
 /** Events that should call when various rendering parameters change */
 interface ClockEvents {
-    /** Call every time tick */
-    fun onTimeTick() {}
-
     /** Call whenever timezone changes */
     fun onTimeZoneChanged(timeZone: TimeZone) {}
 
@@ -101,6 +111,12 @@
 
     /** Call whenever the color palette should update */
     fun onColorPaletteChanged(resources: Resources) {}
+
+    /** Call if the seed color has changed and should be updated */
+    fun onSeedColorChanged(seedColor: Int?) {}
+
+    /** Call whenever the weather data should update */
+    fun onWeatherDataChanged(data: WeatherData) {}
 }
 
 /** Methods which trigger various clock animations */
@@ -131,6 +147,13 @@
 
 /** Events that have specific data about the related face */
 interface ClockFaceEvents {
+    /** Call every time tick */
+    fun onTimeTick() {}
+
+    /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
+    val tickRate: ClockTickRate
+        get() = ClockTickRate.PER_MINUTE
+
     /** Region Darkness specific to the clock face */
     fun onRegionDarknessChanged(isDark: Boolean) {}
 
@@ -150,8 +173,60 @@
     fun onTargetRegionChanged(targetRegion: Rect?) {}
 }
 
+/** Tick rates for clocks */
+enum class ClockTickRate(val value: Int) {
+    PER_MINUTE(2), // Update the clock once per minute.
+    PER_SECOND(1), // Update the clock once per second.
+    PER_FRAME(0), // Update the clock every second.
+}
+
 /** Some data about a clock design */
 data class ClockMetadata(
     val clockId: ClockId,
     val name: String,
 )
+
+/** Structure for keeping clock-specific settings */
+@Keep
+data class ClockSettings(
+    val clockId: ClockId? = null,
+    val seedColor: Int? = null,
+) {
+    // Exclude metadata from equality checks
+    var metadata: JSONObject = JSONObject()
+
+    companion object {
+        private val KEY_CLOCK_ID = "clockId"
+        private val KEY_SEED_COLOR = "seedColor"
+        private val KEY_METADATA = "metadata"
+
+        fun serialize(setting: ClockSettings?): String {
+            if (setting == null) {
+                return ""
+            }
+
+            return JSONObject()
+                .put(KEY_CLOCK_ID, setting.clockId)
+                .put(KEY_SEED_COLOR, setting.seedColor)
+                .put(KEY_METADATA, setting.metadata)
+                .toString()
+        }
+
+        fun deserialize(jsonStr: String?): ClockSettings? {
+            if (jsonStr.isNullOrEmpty()) {
+                return null
+            }
+
+            val json = JSONObject(jsonStr)
+            val result =
+                ClockSettings(
+                    if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null,
+                    if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
+                )
+            if (!json.isNull(KEY_METADATA)) {
+                result.metadata = json.getJSONObject(KEY_METADATA)
+            }
+            return result
+        }
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
new file mode 100644
index 0000000..52dfc55
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
@@ -0,0 +1,107 @@
+package com.android.systemui.plugins
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
+class WeatherData
+private constructor(
+    val description: String,
+    val state: WeatherStateIcon,
+    val useCelsius: Boolean,
+    val temperature: Int,
+) {
+    companion object {
+        private const val TAG = "WeatherData"
+        @VisibleForTesting const val DESCRIPTION_KEY = "description"
+        @VisibleForTesting const val STATE_KEY = "state"
+        @VisibleForTesting const val USE_CELSIUS_KEY = "use_celsius"
+        @VisibleForTesting const val TEMPERATURE_KEY = "temperature"
+        private const val INVALID_WEATHER_ICON_STATE = -1
+
+        fun fromBundle(extras: Bundle): WeatherData? {
+            val description = extras.getString(DESCRIPTION_KEY)
+            val state =
+                WeatherStateIcon.fromInt(extras.getInt(STATE_KEY, INVALID_WEATHER_ICON_STATE))
+            val temperature = readIntFromBundle(extras, TEMPERATURE_KEY)
+            return if (
+                description == null ||
+                    state == null ||
+                    !extras.containsKey(USE_CELSIUS_KEY) ||
+                    temperature == null
+            )
+                null
+            else
+                WeatherData(
+                    description = description,
+                    state = state,
+                    useCelsius = extras.getBoolean(USE_CELSIUS_KEY),
+                    temperature = temperature
+                )
+        }
+
+        private fun readIntFromBundle(extras: Bundle, key: String): Int? =
+            try {
+                extras.getString(key).toInt()
+            } catch (e: Exception) {
+                null
+            }
+    }
+
+    enum class WeatherStateIcon(val id: Int) {
+        UNKNOWN_ICON(0),
+
+        // Clear, day & night.
+        SUNNY(1),
+        CLEAR_NIGHT(2),
+
+        // Mostly clear, day & night.
+        MOSTLY_SUNNY(3),
+        MOSTLY_CLEAR_NIGHT(4),
+
+        // Partly cloudy, day & night.
+        PARTLY_CLOUDY(5),
+        PARTLY_CLOUDY_NIGHT(6),
+
+        // Mostly cloudy, day & night.
+        MOSTLY_CLOUDY_DAY(7),
+        MOSTLY_CLOUDY_NIGHT(8),
+        CLOUDY(9),
+        HAZE_FOG_DUST_SMOKE(10),
+        DRIZZLE(11),
+        HEAVY_RAIN(12),
+        SHOWERS_RAIN(13),
+
+        // Scattered showers, day & night.
+        SCATTERED_SHOWERS_DAY(14),
+        SCATTERED_SHOWERS_NIGHT(15),
+
+        // Isolated scattered thunderstorms, day & night.
+        ISOLATED_SCATTERED_TSTORMS_DAY(16),
+        ISOLATED_SCATTERED_TSTORMS_NIGHT(17),
+        STRONG_TSTORMS(18),
+        BLIZZARD(19),
+        BLOWING_SNOW(20),
+        FLURRIES(21),
+        HEAVY_SNOW(22),
+
+        // Scattered snow showers, day & night.
+        SCATTERED_SNOW_SHOWERS_DAY(23),
+        SCATTERED_SNOW_SHOWERS_NIGHT(24),
+        SNOW_SHOWERS_SNOW(25),
+        MIXED_RAIN_HAIL_RAIN_SLEET(26),
+        SLEET_HAIL(27),
+        TORNADO(28),
+        TROPICAL_STORM_HURRICANE(29),
+        WINDY_BREEZY(30),
+        WINTRY_MIX_RAIN_SNOW(31);
+
+        companion object {
+            fun fromInt(value: Int) = values().firstOrNull { it.id == value }
+        }
+    }
+
+    override fun toString(): String {
+        val unit = if (useCelsius) "C" else "F"
+        return "$state (\"$description\") $temperature°$unit"
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
index 6436dcb..e99b214 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -159,8 +159,13 @@
      * bug report more actionable, so using the [log] with a messagePrinter to add more detail to
      * every log may do more to improve overall logging than adding more logs with this method.
      */
-    fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
-        log(tag, level, { str1 = message }, { str1!! })
+    @JvmOverloads
+    fun log(
+        tag: String,
+        level: LogLevel,
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(tag, level, { str1 = message }, { str1!! }, exception)
 
     /**
      * You should call [log] instead of this method.
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
index d3fabac..faf1b78 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -21,6 +21,7 @@
 import android.net.Uri
 import android.os.Handler
 import android.os.Looper
+import android.os.Trace
 import android.provider.Settings
 
 /**
@@ -51,14 +52,21 @@
         }
     }
 
+    private fun clearCache() {
+        Trace.beginSection("LogcatEchoTrackerDebug#clearCache")
+        cachedBufferLevels.clear()
+        Trace.endSection()
+    }
+
     private fun attach(mainLooper: Looper) {
+        Trace.beginSection("LogcatEchoTrackerDebug#attach")
         contentResolver.registerContentObserver(
             Settings.Global.getUriFor(BUFFER_PATH),
             true,
             object : ContentObserver(Handler(mainLooper)) {
                 override fun onChange(selfChange: Boolean, uri: Uri?) {
                     super.onChange(selfChange, uri)
-                    cachedBufferLevels.clear()
+                    clearCache()
                 }
             }
         )
@@ -69,10 +77,11 @@
             object : ContentObserver(Handler(mainLooper)) {
                 override fun onChange(selfChange: Boolean, uri: Uri?) {
                     super.onChange(selfChange, uri)
-                    cachedTagLevels.clear()
+                    clearCache()
                 }
             }
         )
+        Trace.endSection()
     }
 
     /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
@@ -97,9 +106,12 @@
 
     private fun readSetting(path: String): LogLevel {
         return try {
+            Trace.beginSection("LogcatEchoTrackerDebug#readSetting")
             parseProp(Settings.Global.getString(contentResolver, path))
         } catch (_: Settings.SettingNotFoundException) {
             DEFAULT_LEVEL
+        } finally {
+            Trace.endSection()
         }
     }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 2b16999..1d28c63 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -16,9 +16,11 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.metrics.LogMaker;
 import android.service.quicksettings.Tile;
+import android.text.TextUtils;
 import android.view.View;
 
 import androidx.annotation.Nullable;
@@ -175,6 +177,24 @@
         public Drawable sideViewCustomDrawable;
         public String spec;
 
+        /** Get the state text. */
+        public String getStateText(int arrayResId, Resources resources) {
+            if (state == Tile.STATE_UNAVAILABLE || this instanceof QSTile.BooleanState) {
+                String[] array = resources.getStringArray(arrayResId);
+                return array[state];
+            } else {
+                return "";
+            }
+        }
+
+        /** Get the text for secondaryLabel. */
+        public String getSecondaryLabel(String stateText) {
+            if (TextUtils.isEmpty(secondaryLabel)) {
+                return stateText;
+            }
+            return secondaryLabel.toString();
+        }
+
         public boolean copyTo(State other) {
             if (other == null) throw new IllegalArgumentException();
             if (!other.getClass().equals(getClass())) throw new IllegalArgumentException();
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
index 9ed3bac..70b5d73 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
@@ -105,6 +105,11 @@
         default void onDozingChanged(boolean isDozing) {}
 
         /**
+         * Callback to be notified when Dreaming changes. Dreaming is stored separately from state.
+         */
+        default void onDreamingChanged(boolean isDreaming) {}
+
+        /**
          * Callback to be notified when the doze amount changes. Useful for animations.
          * Note: this will be called for each animation frame. Please be careful to avoid
          * performance regressions.
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index f96644f..5fc9193 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -16,6 +16,50 @@
   public <init>();
 }
 
+# Needed to ensure callback field references are kept in their respective
+# owning classes when the downstream callback registrars only store weak refs.
+# TODO(b/264686688): Handle these cases with more targeted annotations.
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  private com.android.keyguard.KeyguardUpdateMonitorCallback *;
+  private com.android.systemui.privacy.PrivacyConfig$Callback *;
+  private com.android.systemui.privacy.PrivacyItemController$Callback *;
+  private com.android.systemui.settings.UserTracker$Callback *;
+  private com.android.systemui.statusbar.phone.StatusBarWindowCallback *;
+  private com.android.systemui.util.service.Observer$Callback *;
+  private com.android.systemui.util.service.ObservableServiceConnection$Callback *;
+}
+# Note that these rules are temporary companions to the above rules, required
+# for cases like Kotlin where fields with anonymous types use the anonymous type
+# rather than the supertype.
+-if class * extends com.android.keyguard.KeyguardUpdateMonitorCallback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+-if class * extends com.android.systemui.privacy.PrivacyConfig$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+-if class * extends com.android.systemui.privacy.PrivacyItemController$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+-if class * extends com.android.systemui.settings.UserTracker$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+-if class * extends com.android.systemui.statusbar.phone.StatusBarWindowCallback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+-if class * extends com.android.systemui.util.service.Observer$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+-if class * extends com.android.systemui.util.service.ObservableServiceConnection$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+  <1> *;
+}
+
 -keepclasseswithmembers class * {
     public <init>(android.content.Context, android.util.AttributeSet);
 }
diff --git a/packages/SystemUI/res-keyguard/drawable-mdpi/ic_lockscreen_sim.png b/packages/SystemUI/res-keyguard/drawable-mdpi/ic_lockscreen_sim.png
deleted file mode 100644
index 2e259c3..0000000
--- a/packages/SystemUI/res-keyguard/drawable-mdpi/ic_lockscreen_sim.png
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/res-keyguard/drawable-xhdpi/ic_lockscreen_sim.png b/packages/SystemUI/res-keyguard/drawable-xhdpi/ic_lockscreen_sim.png
deleted file mode 100644
index f4de96a..0000000
--- a/packages/SystemUI/res-keyguard/drawable-xhdpi/ic_lockscreen_sim.png
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/res-keyguard/drawable-hdpi/ic_lockscreen_sim.png b/packages/SystemUI/res-keyguard/drawable/ic_lockscreen_sim.png
similarity index 100%
rename from packages/SystemUI/res-keyguard/drawable-hdpi/ic_lockscreen_sim.png
rename to packages/SystemUI/res-keyguard/drawable/ic_lockscreen_sim.png
Binary files differ
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
index fc18132..6fe7d39 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.systemui.common.ui.view.LaunchableLinearLayout
+<com.android.systemui.animation.view.LaunchableLinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="0dp"
     android:layout_height="@dimen/qs_security_footer_single_line_height"
@@ -63,4 +63,4 @@
         android:src="@*android:drawable/ic_chevron_end"
         android:autoMirrored="true"
         android:tint="?android:attr/textColorSecondary" />
-</com.android.systemui.common.ui.view.LaunchableLinearLayout>
\ No newline at end of file
+</com.android.systemui.animation.view.LaunchableLinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
index 2cac9c7..90851e2 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
@@ -45,7 +45,7 @@
           android:id="@+id/user_switcher_header"
           android:textDirection="locale"
           android:layout_width="@dimen/bouncer_user_switcher_width"
-          android:layout_height="wrap_content" />
+          android:layout_height="match_parent" />
     </com.android.keyguard.KeyguardUserSwitcherAnchor>
 
 </LinearLayout>
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-keyguard/layout/keyguard_host_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
deleted file mode 100644
index 8497ff0..0000000
--- a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 2012, 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.
-*/
--->
-
-<!-- This is the host view that generally contains two sub views: the widget view
-    and the security view. -->
-<com.android.keyguard.KeyguardHostView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/keyguard_host_view"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:paddingTop="@dimen/keyguard_lock_padding"
-    android:importantForAccessibility="yes"> <!-- Needed because TYPE_WINDOW_STATE_CHANGED is sent
-                                                  from this view when bouncer is shown -->
-
-    <com.android.keyguard.KeyguardSecurityContainer
-        android:id="@+id/keyguard_security_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:padding="0dp"
-        android:fitsSystemWindows="true"
-        android:layout_gravity="center">
-        <com.android.keyguard.KeyguardSecurityViewFlipper
-            android:id="@+id/view_flipper"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:clipChildren="false"
-            android:clipToPadding="false"
-            android:paddingTop="@dimen/keyguard_security_view_top_margin"
-            android:layout_gravity="center"
-            android:gravity="center">
-        </com.android.keyguard.KeyguardSecurityViewFlipper>
-    </com.android.keyguard.KeyguardSecurityContainer>
-
-</com.android.keyguard.KeyguardHostView>
-
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
index 411fea5..48769fd 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
@@ -14,10 +14,12 @@
   ~ limitations under the License
   -->
 
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
     <TextView
         android:id="@+id/digit_text"
         style="@style/Widget.TextView.NumPadKey.Digit"
+        android:autoSizeMaxTextSize="32sp"
+        android:autoSizeTextType="uniform"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
index 2b7bdc2..c772c96 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -27,7 +27,7 @@
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    androidprv:layout_maxWidth="@dimen/keyguard_security_width"
+    androidprv:layout_maxWidth="@dimen/biometric_auth_pattern_view_max_size"
     android:layout_gravity="center_horizontal|bottom"
     android:clipChildren="false"
     android:clipToPadding="false">
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 64ece47..ca4028a 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -105,6 +105,7 @@
             android:id="@+id/key1"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key2"
             androidprv:digit="1"
             androidprv:textView="@+id/pinEntry" />
 
@@ -112,6 +113,7 @@
             android:id="@+id/key2"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key3"
             androidprv:digit="2"
             androidprv:textView="@+id/pinEntry" />
 
@@ -119,6 +121,7 @@
             android:id="@+id/key3"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key4"
             androidprv:digit="3"
             androidprv:textView="@+id/pinEntry" />
 
@@ -126,6 +129,7 @@
             android:id="@+id/key4"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key5"
             androidprv:digit="4"
             androidprv:textView="@+id/pinEntry" />
 
@@ -133,6 +137,7 @@
             android:id="@+id/key5"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key6"
             androidprv:digit="5"
             androidprv:textView="@+id/pinEntry" />
 
@@ -140,6 +145,7 @@
             android:id="@+id/key6"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key7"
             androidprv:digit="6"
             androidprv:textView="@+id/pinEntry" />
 
@@ -147,13 +153,16 @@
             android:id="@+id/key7"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key8"
             androidprv:digit="7"
             androidprv:textView="@+id/pinEntry" />
 
+
         <com.android.keyguard.NumPadKey
             android:id="@+id/key8"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key9"
             androidprv:digit="8"
             androidprv:textView="@+id/pinEntry" />
 
@@ -161,34 +170,33 @@
             android:id="@+id/key9"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/delete_button"
             androidprv:digit="9"
             androidprv:textView="@+id/pinEntry" />
 
-
         <com.android.keyguard.NumPadButton
             android:id="@+id/delete_button"
+            style="@style/NumPadKey.Delete"
             android:layout_width="0dp"
             android:layout_height="0dp"
-            style="@style/NumPadKey.Delete"
-            android:contentDescription="@string/keyboardview_keycode_delete"
-            />
+            android:accessibilityTraversalBefore="@id/key0"
+            android:contentDescription="@string/keyboardview_keycode_delete" />
 
         <com.android.keyguard.NumPadKey
             android:id="@+id/key0"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key_enter"
             androidprv:digit="0"
             androidprv:textView="@+id/pinEntry" />
 
         <com.android.keyguard.NumPadButton
             android:id="@+id/key_enter"
+            style="@style/NumPadKey.Enter"
             android:layout_width="0dp"
             android:layout_height="0dp"
-            style="@style/NumPadKey.Enter"
-            android:contentDescription="@string/keyboardview_keycode_enter"
-            />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
+            android:contentDescription="@string/keyboardview_keycode_enter" />
+</androidx.constraintlayout.widget.ConstraintLayout>
 
     <include layout="@layout/keyguard_eca"
              android:id="@+id/keyguard_selector_fade_container"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_security_container_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_security_container_view.xml
new file mode 100644
index 0000000..426cfaf
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_security_container_view.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+
+<com.android.keyguard.KeyguardSecurityContainer
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/keyguard_security_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:paddingTop="@dimen/keyguard_lock_padding"
+    android:importantForAccessibility="yes"> <!-- Needed because TYPE_WINDOW_STATE_CHANGED is sent
+                                                  from this view when bouncer is shown -->
+    <com.android.keyguard.KeyguardSecurityViewFlipper
+        android:id="@+id/view_flipper"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:paddingTop="@dimen/keyguard_security_view_top_margin"
+        android:layout_gravity="center"
+        android:gravity="center">
+    </com.android.keyguard.KeyguardSecurityViewFlipper>
+</com.android.keyguard.KeyguardSecurityContainer>
+
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
index 7db0fe9..728d861 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
 **
 ** Copyright 2012, The Android Open Source Project
 **
@@ -17,185 +16,185 @@
 */
 -->
 <!-- This is the SIM PIN view that allows the user to enter a SIM PIN to unlock the device. -->
-<com.android.keyguard.KeyguardSimPinView
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:androidprv="http://schemas.android.com/apk/res-auto"
-        android:id="@+id/keyguard_sim_pin_view"
-        android:orientation="vertical"
+<com.android.keyguard.KeyguardSimPinView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/keyguard_sim_pin_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    androidprv:layout_maxWidth="@dimen/keyguard_security_width"
+    android:layout_gravity="center_horizontal|bottom">
+    <include layout="@layout/keyguard_bouncer_message_area"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        androidprv:layout_maxWidth="@dimen/keyguard_security_width"
-        android:layout_gravity="center_horizontal|bottom">
-    <include layout="@layout/keyguard_bouncer_message_area" />
-    <Space
-          android:layout_width="match_parent"
-          android:layout_height="0dp"
-          android:layout_weight="1" />
-    <ImageView
-            android:id="@+id/keyguard_sim"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:tint="@color/background_protected"
-            android:src="@drawable/ic_lockscreen_sim"/>
-    <LinearLayout
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:layoutDirection="ltr">
+        <LinearLayout
+            android:id="@+id/pin_area"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"
-            android:gravity="center"
-            android:layoutDirection="ltr"
-            >
-        <include layout="@layout/keyguard_esim_area"
-             android:id="@+id/keyguard_esim_area"
-             android:layout_width="wrap_content"
-             android:layout_height="wrap_content" />
-        <RelativeLayout
-                android:id="@+id/row0"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:paddingBottom="4dp"
-                >
+            android:gravity="center_horizontal"
+            android:paddingTop="@dimen/num_pad_entry_row_margin_bottom"
+            android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom"
+            androidprv:layout_constraintBottom_toTopOf="@+id/flow1"
+            androidprv:layout_constraintEnd_toEndOf="parent"
+            androidprv:layout_constraintStart_toStartOf="parent"
+            androidprv:layout_constraintTop_toTopOf="parent">
+
+            <ImageView
+                android:id="@+id/keyguard_sim"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:layout_gravity="center_horizontal"
+                android:src="@drawable/ic_lockscreen_sim"
+                app:tint="@color/background_protected" />
+
+            <include
+                android:id="@+id/keyguard_esim_area"
+                layout="@layout/keyguard_esim_area"
+                android:layout_gravity="center_horizontal"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
             <com.android.keyguard.PasswordTextView
                 android:id="@+id/simPinEntry"
                 style="@style/Widget.TextView.Password"
                 android:layout_width="@dimen/keyguard_security_width"
                 android:layout_height="@dimen/keyguard_password_height"
-                android:layout_centerHorizontal="true"
-                android:layout_marginRight="72dp"
                 android:contentDescription="@string/keyguard_accessibility_sim_pin_area"
-                android:gravity="center"
+                android:layout_gravity="center_horizontal"
                 androidprv:scaledTextSize="@integer/scaled_password_text_size" />
-        </RelativeLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key1"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="1"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key2"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="2"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key3"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="3"
-                    />
         </LinearLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key4"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="4"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key5"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="5"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key6"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="6"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key7"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="7"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key8"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="8"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key9"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="9"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                >
-            <com.android.keyguard.NumPadButton
-                    android:id="@+id/delete_button"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    android:contentDescription="@string/keyboardview_keycode_delete"
-                    style="@style/NumPadKey.Delete"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key0"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/simPinEntry"
-                    androidprv:digit="0"
-                    />
-            <com.android.keyguard.NumPadButton
-                    android:id="@+id/key_enter"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    style="@style/NumPadKey.Enter"
-                    android:contentDescription="@string/keyboardview_keycode_enter"
-                    />
-        </LinearLayout>
-    </LinearLayout>
-    <include layout="@layout/keyguard_eca"
-             android:id="@+id/keyguard_selector_fade_container"
-             android:layout_width="match_parent"
-             android:layout_height="wrap_content"
-             android:orientation="vertical"
-             android:layout_gravity="bottom|center_horizontal"
-             android:layout_marginTop="@dimen/keyguard_eca_top_margin"
-             android:layout_marginBottom="2dp"
-             android:gravity="center_horizontal"/>
+
+        <androidx.constraintlayout.helper.widget.Flow
+            android:id="@+id/flow1"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:orientation="horizontal"
+            androidprv:constraint_referenced_ids="key1,key2,key3,key4,key5,key6,key7,key8,key9,delete_button,key0,key_enter"
+            androidprv:flow_horizontalGap="@dimen/num_pad_key_margin_end"
+            androidprv:flow_horizontalStyle="packed"
+            androidprv:flow_maxElementsWrap="3"
+            androidprv:flow_verticalBias="1.0"
+            androidprv:flow_verticalGap="@dimen/num_pad_entry_row_margin_bottom"
+            androidprv:flow_verticalStyle="packed"
+            androidprv:flow_wrapMode="aligned"
+            androidprv:layout_constraintBottom_toBottomOf="parent"
+            androidprv:layout_constraintEnd_toEndOf="parent"
+            androidprv:layout_constraintStart_toStartOf="parent"
+            androidprv:layout_constraintTop_toBottomOf="@id/pin_area" />
+
+        <com.android.keyguard.NumPadButton
+            android:id="@+id/delete_button"
+            style="@style/NumPadKey.Delete"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key0"
+            android:contentDescription="@string/keyboardview_keycode_delete" />
+
+        <com.android.keyguard.NumPadButton
+            android:id="@+id/key_enter"
+            style="@style/NumPadKey.Enter"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:contentDescription="@string/keyboardview_keycode_enter" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key1"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key2"
+            androidprv:digit="1"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key2"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key3"
+            androidprv:digit="2"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key3"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key4"
+            androidprv:digit="3"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key4"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key5"
+            androidprv:digit="4"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key5"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key6"
+            androidprv:digit="5"
+            androidprv:textView="@+id/simPinEntry" />
+
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key6"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key7"
+            androidprv:digit="6"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key7"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key8"
+            androidprv:digit="7"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key8"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key9"
+            androidprv:digit="8"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key9"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/delete_button"
+            androidprv:digit="9"
+            androidprv:textView="@+id/simPinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key0"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key_enter"
+            androidprv:digit="0"
+            androidprv:textView="@+id/simPinEntry" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <include
+        android:id="@+id/keyguard_selector_fade_container"
+        layout="@layout/keyguard_eca"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|center_horizontal"
+        android:layout_marginBottom="2dp"
+        android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+        android:gravity="center_horizontal"
+        android:orientation="vertical" />
 </com.android.keyguard.KeyguardSimPinView>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
index 422bd4c..7e24d12 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
@@ -21,6 +21,7 @@
 <com.android.keyguard.KeyguardSimPukView
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
         android:id="@+id/keyguard_sim_puk_view"
         android:orientation="vertical"
         android:layout_width="match_parent"
@@ -29,173 +30,165 @@
         android:layout_gravity="center_horizontal|bottom">
     <include layout="@layout/keyguard_bouncer_message_area"/>
 
-    <Space
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        android:layout_weight="1" />
-
-    <ImageView
-            android:id="@+id/keyguard_sim"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:tint="@color/background_protected"
-            android:src="@drawable/ic_lockscreen_sim"/>
-
-    <LinearLayout
+        android:layout_weight="1"
+        android:layoutDirection="ltr">
+        <LinearLayout
+            android:id="@+id/pin_area"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"
-            android:gravity="center"
-            android:layoutDirection="ltr"
-            >
-        <include layout="@layout/keyguard_esim_area"
-            android:id="@+id/keyguard_esim_area"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content" />
+            android:gravity="center_horizontal"
+            android:paddingTop="@dimen/num_pad_entry_row_margin_bottom"
+            android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom"
+            androidprv:layout_constraintBottom_toTopOf="@+id/flow1"
+            androidprv:layout_constraintEnd_toEndOf="parent"
+            androidprv:layout_constraintStart_toStartOf="parent"
+            androidprv:layout_constraintTop_toTopOf="parent">
 
-        <RelativeLayout
-                android:id="@+id/row0"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:paddingBottom="4dp"
-                >
+            <ImageView
+                android:id="@+id/keyguard_sim"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:layout_gravity="center_horizontal"
+                android:src="@drawable/ic_lockscreen_sim"
+                app:tint="@color/background_protected" />
+
+            <include
+                android:id="@+id/keyguard_esim_area"
+                layout="@layout/keyguard_esim_area"
+                android:layout_gravity="center_horizontal"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
             <com.android.keyguard.PasswordTextView
                 android:id="@+id/pukEntry"
                 style="@style/Widget.TextView.Password"
                 android:layout_width="@dimen/keyguard_security_width"
                 android:layout_height="@dimen/keyguard_password_height"
-                android:layout_centerHorizontal="true"
-                android:layout_marginRight="72dp"
-                android:contentDescription="@string/keyguard_accessibility_sim_puk_area"
-                android:gravity="center"
+                android:contentDescription="@string/keyguard_accessibility_sim_pin_area"
+                android:layout_gravity="center_horizontal"
                 androidprv:scaledTextSize="@integer/scaled_password_text_size" />
-        </RelativeLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key1"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="1"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key2"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="2"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key3"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="3"
-                    />
         </LinearLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
 
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key4"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="4"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key5"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="5"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key6"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="6"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key7"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="7"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key8"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="8"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key9"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="9"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                >
-            <com.android.keyguard.NumPadButton
-                    android:id="@+id/delete_button"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    android:contentDescription="@string/keyboardview_keycode_delete"
-                    style="@style/NumPadKey.Delete"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key0"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pukEntry"
-                    androidprv:digit="0"
-                    />
-            <com.android.keyguard.NumPadButton
-                    android:id="@+id/key_enter"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    style="@style/NumPadKey.Enter"
-                    android:contentDescription="@string/keyboardview_keycode_enter"
-                    />
-        </LinearLayout>
-    </LinearLayout>
+        <androidx.constraintlayout.helper.widget.Flow
+            android:id="@+id/flow1"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:orientation="horizontal"
+            androidprv:constraint_referenced_ids="key1,key2,key3,key4,key5,key6,key7,key8,key9,delete_button,key0,key_enter"
+            androidprv:flow_horizontalGap="@dimen/num_pad_key_margin_end"
+            androidprv:flow_horizontalStyle="packed"
+            androidprv:flow_maxElementsWrap="3"
+            androidprv:flow_verticalBias="1.0"
+            androidprv:flow_verticalGap="@dimen/num_pad_entry_row_margin_bottom"
+            androidprv:flow_verticalStyle="packed"
+            androidprv:flow_wrapMode="aligned"
+            androidprv:layout_constraintBottom_toBottomOf="parent"
+            androidprv:layout_constraintEnd_toEndOf="parent"
+            androidprv:layout_constraintStart_toStartOf="parent"
+            androidprv:layout_constraintTop_toBottomOf="@id/pin_area" />
+
+        <com.android.keyguard.NumPadButton
+            android:id="@+id/delete_button"
+            style="@style/NumPadKey.Delete"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key0"
+            android:contentDescription="@string/keyboardview_keycode_delete" />
+
+        <com.android.keyguard.NumPadButton
+            android:id="@+id/key_enter"
+            style="@style/NumPadKey.Enter"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:contentDescription="@string/keyboardview_keycode_enter" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key1"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key2"
+            androidprv:digit="1"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key2"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key3"
+            androidprv:digit="2"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key3"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key4"
+            androidprv:digit="3"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key4"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key5"
+            androidprv:digit="4"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key5"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key6"
+            androidprv:digit="5"
+            androidprv:textView="@+id/pukEntry" />
+
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key6"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key7"
+            androidprv:digit="6"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key7"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key8"
+            androidprv:digit="7"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key8"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key9"
+            androidprv:digit="8"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key9"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/delete_button"
+            androidprv:digit="9"
+            androidprv:textView="@+id/pukEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key0"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key_enter"
+            androidprv:digit="0"
+            androidprv:textView="@+id/pukEntry" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
     <include layout="@layout/keyguard_eca"
              android:id="@+id/keyguard_selector_fade_container"
diff --git a/packages/SystemUI/res-keyguard/values-af/strings.xml b/packages/SystemUI/res-keyguard/values-af/strings.xml
index f226dea..f2ad66d 100644
--- a/packages/SystemUI/res-keyguard/values-af/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-af/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ongeldige kaart."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Gelaai"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laai tans draadloos"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laai tans"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laaidok"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laai tans"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laai tans vinnig"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laai tans stadig"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laaiproses word geoptimeer om battery te beskerm"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laaiproses word tydelik beperk"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Druk Kieslys om te ontsluit."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Netwerk is gesluit"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Geen SIM nie"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Voeg ’n SIM by."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Die SIM is weg of nie leesbaar nie. Voeg ’n SIM by."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Onbruikbare SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Jou SIM is permanent gedeaktiveer.\n Kontak jou draadlose diensverskaffer vir ’n ander SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM is gesluit."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM is PUK-gesluit."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Ontsluit tans SIM …"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Geen SIM-kaart nie"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Steek \'n SIM-kaart in."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Die SIM-kaart is weg of nie leesbaar nie. Steek \'n SIM-kaart in."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Onbruikbare SIM-kaart."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Jou SIM-kaart is permanent gedeaktiveer.\n Kontak jou draadlose diensverskaffer vir \'n ander SIM-kaart."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kaart is gesluit."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kaart is PUK-geslote."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Ontsluit tans SIM-kaart …"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-area"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Toestelwagwoord"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM-PIN-area"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM vir \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is nou gedeaktiveer. Voer die PUK-kode in om voort te gaan. Kontak die diensverskaffer vir besonderhede."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Voer die gewenste PIN-kode in"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Bevestig gewenste PIN-kode"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Ontsluit tans SIM …"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Ontsluit tans SIM-kaart …"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Tik \'n PIN wat 4 to 8 syfers lank is, in."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-kode moet 8 of meer syfers wees."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Jy het jou PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> keer verkeerd ingetik. \n\nProbeer weer oor <xliff:g id="NUMBER_1">%2$d</xliff:g> sekondes."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Jy het jou wagwoord <xliff:g id="NUMBER_0">%1$d</xliff:g> keer verkeerd ingetik. \n\nProbeer weer oor <xliff:g id="NUMBER_1">%2$d</xliff:g> sekondes."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Jy het jou ontsluitpatroon <xliff:g id="NUMBER_0">%1$d</xliff:g> keer verkeerd geteken. \n\nProbeer weer oor <xliff:g id="NUMBER_1">%2$d</xliff:g> sekondes."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Verkeerde SIM-PIN-kode. Jy sal nou jou diensverskaffer moet kontak om jou toestel te ontsluit."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Verkeerde PIN-kode vir SIM. Jy het # poging oor voordat jy jou diensverskaffer moet kontak om jou toestel te ontsluit.}other{Verkeerde PIN-kode vir SIM. Jy het # pogings oor. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Verkeerde SIM-PIN-kode. Jy het <xliff:g id="NUMBER_1">%d</xliff:g> pogings oor.</item>
+      <item quantity="one">Verkeerde SIM-PIN-kode. Jy het <xliff:g id="NUMBER_0">%d</xliff:g> oorblywende poging voordat jy jou diensverskaffer sal moet kontak om jou toestel te ontsluit.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is onbruikbaar. Kontak jou diensverskaffer."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Verkeerde PUK-kode vir SIM. Jy het # poging oor voordat SIM permanent onbruikbaar word.}other{Verkeerde PUK-kode vir SIM. Jy het # pogings oor voordat SIM permanent onbruikbaar word.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Verkeerde SIM-PUK-kode. Jy het <xliff:g id="NUMBER_1">%d</xliff:g> pogings oor voordat SIM permanent onbruikbaar word.</item>
+      <item quantity="one">Verkeerde SIM-PUK-kode. Jy het <xliff:g id="NUMBER_0">%d</xliff:g> poging oor voordat SIM permanent onbruikbaar word.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM-PIN-bewerking het misluk!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM-PUK-bewerking het misluk!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Wissel invoermetode"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon word vereis nadat toestel herbegin het"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN word vereis nadat toestel herbegin het"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wagwoord word vereis nadat toestel herbegin het"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Gebruik eerder ’n patroon vir bykomende sekuriteit"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Gebruik eerder ’n PIN vir bykomende sekuriteit"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Gebruik eerder ’n wagwoord vir bykomende sekuriteit"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon word vir bykomende sekuriteit vereis"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN word vir bykomende sekuriteit vereis"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wagwoord word vir bykomende sekuriteit vereis"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Toestel is deur administrateur gesluit"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Toestel is handmatig gesluit"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie herken nie"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Skakel kameratoegang aan om Gesigslot te gebruik"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Voer SIM se PIN in. Jy het # poging oor voordat jy jou diensverskaffer moet kontak om jou toestel te ontsluit.}other{Voer SIM se PIN in. Jy het # pogings oor.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is nou gedeaktiveer. Voer PUK-kode in om voort te gaan. Jy het # poging oor voordat die SIM permanent onbruikbaar word. Kontak die diensverskaffer vir besonderhede.}other{SIM is nou gedeaktiveer. Voer PUK-kode in om voort te gaan. Jy het # pogings oor voordat die SIM permanent onbruikbaar word. Kontak die diensverskaffer vir besonderhede.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Skakel "<b>"kameratoegang"</b>" in Instellings &gt; Privaatheid aan om Gesigslot te gebruik"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Voer SIM-PIN in. Jy het <xliff:g id="NUMBER_1">%d</xliff:g> pogings oor.</item>
+      <item quantity="one">Voer SIM-PIN in. Jy het <xliff:g id="NUMBER_0">%d</xliff:g> poging oor voordat jy jou diensverskaffer moet kontak om jou toestel te ontsluit.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM is nou gedeaktiveer. Voer PUK-kode in om voort te gaan. Jy het <xliff:g id="_NUMBER_1">%d</xliff:g> pogings oor voordat die SIM permanent onbruikbaar word. Kontak die diensverskaffer vir besonderhede.</item>
+      <item quantity="one">SIM is nou gedeaktiveer. Voer PUK-kode in om voort te gaan. Jy het <xliff:g id="_NUMBER_0">%d</xliff:g> poging oor voordat die SIM permanent onbruikbaar word. Kontak die diensverskaffer vir besonderhede.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Verstek"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Borrel"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ontsluit jou toestel om voort te gaan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-am/strings.xml b/packages/SystemUI/res-keyguard/values-am/strings.xml
index 647001e..7f4f6fd 100644
--- a/packages/SystemUI/res-keyguard/values-am/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-am/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ልክ ያልሆነ ካርድ።"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ባትሪ ሞልቷል"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • በገመድ አልባ ኃይል በመሙላት ላይ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ኃይል በመሙላት ላይ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • የባትሪ ኃይል መሙያ መትከያ"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ኃይል በመሙላት ላይ"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • በፍጥነት ኃይልን በመሙላት ላይ"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • በዝግታ ኃይልን በመሙላት ላይ"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ባትሪን ለመጠበቅ ኃይል መሙላት ተብቷል"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ኃይል መሙላት ለጊዜው ተገድቧል"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"ለመክፈት ምናሌ ተጫን።"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"አውታረ መረብ ተቆልፏል"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"ምንም SIM የለም"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"ሲም ያክሉ።"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"ሲሙ ጠፍቷል ወይም አይነበብም። ሲም ያክሉ።"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ጥቅም ላይ የማይውል ሲም።"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ሲምዎ በቋሚነት ቦዝኗል።\n ለሌላ ሲም የእርስዎን አገልግሎት ሰጪ ያግኙ።"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"ሲም ተቆልፏል።"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"ሲም በPUK የተቆለፈ ነው።"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"ሲምን በመክፈት ላይ…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"ምንም ሲም ካርድ የለም"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ሲም ካርድ ያስገቡ።"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ሲም ካርዱ ጠፍቷል ወይም መነበብ አይችልም። እባክዎ ሲም ካርድ ያስገቡ።"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"የማይሰራ ሲም ካርድ።"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ሲም ካርድዎ እስከመጨረሻው ተሰናክሏል።\n ሌላ ሲም ካርድ ለማግኘት ከገመድ አልባ አገልግሎት አቅራቢዎ ጋር ይገናኙ።"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"ሲም ካርድ ተዘግቷል።"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ሲም ካርድ በPUK ተቆልፏል።"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"ሲም ካርድን በመክፈት ላይ..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"የፒን አካባቢ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"የመሣሪያ ይለፍ ቃል"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"የሲም ፒን አካባቢ"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ሲም «<xliff:g id="CARRIER">%1$s</xliff:g>» አሁን ተሰናክሏል። ለመቀጠል የPUK ኮድ ያስገቡ። ዝርዝር መረጃን ለማግኘት የተንቀሳቃሽ ስልክ አገልግሎት አቅራቢውን ያነጋግሩ።"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"የተፈለገውን የፒን ኮድ ያስገቡ"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"የተፈለገውን ፒን ኮድ ያረጋግጡ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"ሲምን በመክፈት ላይ…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"ሲም ካርድን በመክፈት ላይ..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"ከ4 እስከ 8 ቁጥሮች የያዘ ፒን ይተይቡ።"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"የPUK ኮድ 8 ወይም ከዚያ በላይ ቁጥሮች ሊኖረው ይገባል።"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"ፒንዎን <xliff:g id="NUMBER_0">%1$d</xliff:g> ጊዜ በትክክል አልተየቡም። \n\nበ<xliff:g id="NUMBER_1">%2$d</xliff:g> ሰኮንዶች ውስጥ እንደገና ይሞክሩ።"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"የይለፍ ቃልዎን <xliff:g id="NUMBER_0">%1$d</xliff:g> ጊዜ ትክክል ባልሆነ መንገድ ተይበዋል።\n\nበ<xliff:g id="NUMBER_1">%2$d</xliff:g> ሰኮንዶች ውስጥ እንደገና ይሞክሩ።"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"የመክፈቻ ስርዓተ ጥለቱን <xliff:g id="NUMBER_0">%1$d</xliff:g> ጊዜ ትክክል ባልሆነ መንገድ ስለውታል።\n\nበ<xliff:g id="NUMBER_1">%2$d</xliff:g> ሰኮንዶች ውስጥ እንደገና ይሞክሩ።"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ልክ ያልሆነ የሲም ፒን ኮድ። አሁን መሣሪያዎን ለማስከፈት አገልግሎት አቅራቢዎን ማነጋገር አለብዎት።"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{የተሳሳተ የሲም ፒን ኮድ፣ መሣሪያዎን ለማስከፈት የአገልግሎት አቅራቢዎን ማነጋገር ግዴታ ከመሆኑ በፊት # ሙከራ ይቀርዎታል።}one{የተሳሳተ የሲም ፒን ኮድ፣ # ሙከራዎች ይቀሩዎታል። }other{የተሳሳተ የሲም ፒን ኮድ፣ # ሙከራዎች ይቀሩዎታል። }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">ልክ ያልሆነ የሲም ፒን ኮድ፣ <xliff:g id="NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል።</item>
+      <item quantity="other">ልክ ያልሆነ የሲም ፒን ኮድ፣ <xliff:g id="NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል።</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ሲሙ ጥቅም ላይ መዋል እይችልም። የአገልግሎት አቅራቢዎን ያነጋግሩ።"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{የተሳሳተ የሲም PUK ኮድ፣ ሲም በቋሚነት መጠቀም የማይቻል ከመሆኑ በፊት # ሙከራ ይቀርዎታል።}one{የተሳሳተ የሲም PUK ኮድ፣ ሲም በቋሚነት መጠቀም የማይቻል ከመሆኑ በፊት # ሙከራ ይቀርዎታል።}other{የተሳሳተ የሲም PUK ኮድ፣ ሲም በቋሚነት መጠቀም የማይቻል ከመሆኑ በፊት # ሙከራዎች ይቀሩዎታል።}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">ልክ ያልሆነ የሲም ፒዩኬ ኮድ፣ ሲሙ እስከመጨረሻው የማይሰራ ከመሆኑ በፊት <xliff:g id="NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል።</item>
+      <item quantity="other">ልክ ያልሆነ የሲም ፒዩኬ ኮድ፣ ሲሙ እስከመጨረሻው የማይሰራ ከመሆኑ በፊት <xliff:g id="NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል።</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"የሲም ፒን ክወና አልተሳካም!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"የሲም PUK ክወና አልተሳካም!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"የግቤት ስልት ቀይር"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"መሣሪያ ዳግም ከጀመረ በኋላ ሥርዓተ ጥለት ያስፈልጋል"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"መሣሪያ ዳግም ከተነሳ በኋላ ፒን ያስፈልጋል"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"መሣሪያ ዳግም ከጀመረ በኋላ የይለፍ ቃል ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ለተጨማሪ ደህንነት በምትኩ ስርዓተ ጥለት ይጠቀሙ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ለተጨማሪ ደህንነት በምትኩ ፒን ይጠቀሙ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ለተጨማሪ ደህንነት በምትኩ የይለፍ ቃል ይጠቀሙ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ሥርዓተ ጥለት ለተጨማሪ ደህንነት ያስፈልጋል"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ፒን ለተጨማሪ ደህንነት ያስፈልጋል"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"የይለፍ ቃል ለተጨማሪ ደህንነት ያስፈልጋል"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"መሣሪያ በአስተዳዳሪ ተቆልፏል"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"መሣሪያ በተጠቃሚው ራሱ ተቆልፏል"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"አልታወቀም"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"በመልክ መክፈትን ለመጠቀም በቅንብሮች ውስጥ የካሜራ መዳረሻን ያብሩ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{የሲም ፒን ያስገቡ። መሣሪያዎን ለማስከፈት የአገልግሎት አቅራቢዎን ማነጋገር ግዴታ ከመሆኑ በፊት # ሙከራ ይቀርዎታል።}one{የሲም ፒን ያስገቡ። # ቀሪ ሙከራዎች አሉዎት።}other{የሲም ፒን ያስገቡ። # ቀሪ ሙከራዎች አሉዎት።}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ሲም አሁን ተሰናክሏል። ለመቀጠል የPUK ኮድ ያስገቡ። ሲም በቋሚነት መጠቀም የማይቻል ከመሆኑ በፊት # ሙከራ ይቀርዎታል። ዝርዝሮችን ለማግኘት የአገልግሎት አቅራቢን ያነጋግሩ።}one{ሲም አሁን ተሰናክሏል። ለመቀጠል የPUK ኮድ ያስገቡ። ሲም በቋሚነት መጠቀም የማይቻል ከመሆኑ በፊት # ሙከራዎች ይቀሩዎታል። ዝርዝሮችን ለማግኘት የአገልግሎት አቅራቢን ያነጋግሩ።}other{ሲም አሁን ተሰናክሏል። ለመቀጠል የPUK ኮድ ያስገቡ። ሲም በቋሚነት መጠቀም የማይቻል ከመሆኑ በፊት # ሙከራዎች ይቀሩዎታል። ዝርዝሮችን ለማግኘት የአገልግሎት አቅራቢን ያነጋግሩ።}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"በመልክ መክፈትን ለመጠቀም "<b>"የካሜራ መዳረሻ"</b>"ን በቅንብሮች እና ግላዊነት ውስጥ ያብሩ"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">የሲም ፒን ያስገቡ። <xliff:g id="NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል።</item>
+      <item quantity="other">የሲም ፒን ያስገቡ። <xliff:g id="NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል።</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">ሲም አሁን ተሰናክሏል። ለመቀጠል የPUK ኮድ ያስገቡ። ሲም እስከመጨረሻው መጠቀም የማይቻል ከመሆኑ በፊት <xliff:g id="_NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል። ዝርዝሮችን ለማግኘት የአገልግሎት አቅራቢን ያነጋግሩ።</item>
+      <item quantity="other">ሲም አሁን ተሰናክሏል። ለመቀጠል የPUK ኮድ ያስገቡ። ሲም እስከመጨረሻው መጠቀም የማይቻል ከመሆኑ በፊት <xliff:g id="_NUMBER_1">%d</xliff:g> ሙከራዎች ይቀረዎታል። ዝርዝሮችን ለማግኘት የአገልግሎት አቅራቢን ያነጋግሩ።</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ነባሪ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"አረፋ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"አናሎግ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ለመቀጠል መሣሪያዎን ይክፈቱ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ar/strings.xml b/packages/SystemUI/res-keyguard/values-ar/strings.xml
index a7fe8e5..5d7ac0d 100644
--- a/packages/SystemUI/res-keyguard/values-ar/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ar/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"بطاقة غير صالحة."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"تم الشحن"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • جارٍ الشحن لاسلكيًا"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • جارٍ الشحن"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • جارٍ الشحن على وحدة الإرساء"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • جارٍ الشحن"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • جارٍ الشحن سريعًا"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • جارٍ الشحن ببطء"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • تم تحسين الشحن لحماية البطارية"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • الشحن محدود مؤقتًا"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"اضغط على \"القائمة\" لإلغاء التأمين."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"الشبكة مؤمّنة"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"‏لا تتوفر شريحة SIM."</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"‏يجب إضافة شريحة SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"‏شريحة SIM مفقودة أو غير قابلة للقراءة. يجب إضافة شريحة SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"‏شريحة SIM غير قابلة للاستخدام."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"‏تم إيقاف شريحة SIM نهائيًا.\n عليك التواصل مع مقدم خدمة اللاسلكي للحصول على شريحة SIM أخرى."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"‏شريحة SIM مُقفَلة."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"‏شريحة SIM مُقفَلة برمز PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"‏جارٍ إلغاء قفل شريحة SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"‏ليست هناك شريحة SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"‏أدخل شريحة SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"‏شريحة SIM مفقودة أو غير قابلة للقراءة. أدخل شريحة SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"‏شريحة SIM غير قابلة للاستخدام."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"‏تم إيقاف شريحة SIM بشكل دائم.\n اتصل بمقدم خدمة اللاسلكي للحصول على شريحة SIM أخرى."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"‏شريحة SIM مؤمّنة."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"‏شريحة SIM مؤمّنة برمز PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"‏جارٍ فتح قفل شريحة SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"منطقة رقم التعريف الشخصي"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"كلمة مرور الجهاز"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"‏منطقة رقم التعريف الشخصي لشريحة SIM"</string>
@@ -50,7 +50,7 @@
     <string name="error_disable_esim_title" msgid="3802652622784813119">"‏يتعذّر إيقاف eSIM."</string>
     <string name="error_disable_esim_msg" msgid="2441188596467999327">"‏يتعذّر إيقاف eSIM بسبب خطأ."</string>
     <string name="keyboardview_keycode_enter" msgid="6727192265631761174">"Enter"</string>
-    <string name="kg_wrong_pattern" msgid="5907301342430102842">"النقش غير صحيح."</string>
+    <string name="kg_wrong_pattern" msgid="5907301342430102842">"النقش غير صحيح"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"كلمة مرور غير صحيحة"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"رقم تعريف شخصي خاطئ"</string>
     <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{أعِد المحاولة خلال ثانية واحدة.}zero{أعِد المحاولة خلال # ثانية.}two{أعِد المحاولة خلال ثانيتين.}few{أعِد المحاولة خلال # ثوانٍ.}many{أعِد المحاولة خلال # ثانية.}other{أعِد المحاولة خلال # ثانية.}}"</string>
@@ -61,16 +61,30 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"‏SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" غير مفعّلة الآن. أدخل رمز PUK للمتابعة. واتصل بمشغل شبكة الجوّال لمعرفة التفاصيل."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"أدخل رمز رقم التعريف الشخصي المطلوب"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"تأكيد رمز رقم التعريف الشخصي المطلوب"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"‏جارٍ إلغاء قفل شريحة SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"‏جارٍ فتح قفل شريحة SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"اكتب رمز رقم التعريف الشخصي المكوّن من ٤ إلى ٨ أرقام."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"‏يجب أن يتضمن رمز PUK‏ ۸ أرقام أو أكثر."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"لقد كتبت رقم التعريف الشخصي بشكل غير صحيح <xliff:g id="NUMBER_0">%1$d</xliff:g> مرة. \n\nأعد المحاولة خلال <xliff:g id="NUMBER_1">%2$d</xliff:g> ثانية."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"لقد كتبت كلمة المرور بشكل غير صحيح <xliff:g id="NUMBER_0">%1$d</xliff:g> مرة. \n\nأعد المحاولة خلال <xliff:g id="NUMBER_1">%2$d</xliff:g> ثانية."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"لقد رسمت نقش فتح القفل بطريقة غير صحيحة <xliff:g id="NUMBER_0">%1$d</xliff:g> مرة. \n\nأعد المحاولة خلال <xliff:g id="NUMBER_1">%2$d</xliff:g> ثانية."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"‏رمز \"رقم التعريف الشخصي\" لشريحة SIM غير صحيح، ويلزمك الاتصال الآن بمشغّل شبكة الجوّال لإلغاء قفل الجهاز."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{‏رقم التعريف الشخصي لشريحة SIM غير صحيح. تتبقّى لديك محاولة واحدة يجب بعدها الاتصال بمشغّل شبكة الجوّال لفتح قفل الجهاز.}zero{‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح. تتبقّى لديك # محاولة. }two{‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح. تتبقّى لديك محاولتان. }few{‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح. تتبقّى لديك # محاولات. }many{‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح. تتبقّى لديك # محاولة. }other{‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح. تتبقّى لديك # محاولة. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="zero">‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح، ولم تتبق لديك أي محاولات (<xliff:g id="NUMBER_1">%d</xliff:g>).</item>
+      <item quantity="two">‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح، ويتبقى لديك محاولتان (<xliff:g id="NUMBER_1">%d</xliff:g>).</item>
+      <item quantity="few">‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح، ويتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولات.</item>
+      <item quantity="many">‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح، ويتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولة.</item>
+      <item quantity="other">‏رمز رقم التعريف الشخصي لشريحة SIM غير صحيح، ويتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولة.</item>
+      <item quantity="one">‏رمز \"رقم التعريف الشخصي\" لشريحة SIM غير صحيح، ويتبقى لديك محاولة واحدة (<xliff:g id="NUMBER_0">%d</xliff:g>) يتعين عليك بعدها الاتصال بمشغّل شبكة الجوّال لإلغاء قفل الجهاز.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"‏شريحة SIM غير صالحة للاستخدام. يُرجى الاتصال بمشغّل شبكة الجوّال."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{‏رمز PUK لشريحة SIM غير صحيح. تتبقّى لديك محاولة واحدة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا.}zero{‏رمز PUK لشريحة SIM غير صحيح. تتبقّى لديك # محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا.}two{‏رمز PUK لشريحة SIM غير صحيح. تتبقّى لديك محاولتان قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا.}few{‏رمز PUK لشريحة SIM غير صحيح. تتبقّى لديك # محاولات قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا.}many{‏رمز PUK لشريحة SIM غير صحيح. تتبقّى لديك # محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا.}other{‏رمز PUK لشريحة SIM غير صحيح. تتبقّى لديك # محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="zero">‏رمز PUK لشريحة SIM غير صحيح، ولم تتبق لديك أي محاولات (<xliff:g id="NUMBER_1">%d</xliff:g>) تصبح بعدها شريحة SIM غير صالحة للاستخدام بشكل دائم.</item>
+      <item quantity="two">‏رمز PUK لشريحة SIM غير صحيح، ويتبقى لديك محاولتان (<xliff:g id="NUMBER_1">%d</xliff:g>) تصبح بعدها شريحة SIM غير صالحة للاستخدام بشكل دائم.</item>
+      <item quantity="few">‏رمز PUK لشريحة SIM غير صحيح، ويتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولات تصبح بعدها شريحة SIM غير صالحة للاستخدام بشكل دائم.</item>
+      <item quantity="many">‏رمز PUK لشريحة SIM غير صحيح، ويتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولة تصبح بعدها شريحة SIM غير صالحة للاستخدام بشكل دائم.</item>
+      <item quantity="other">‏رمز PUK لشريحة SIM غير صحيح، ويتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> من المحاولات تصبح بعدها شريحة SIM غير صالحة للاستخدام بشكل دائم.</item>
+      <item quantity="one">‏رمز PUK لشريحة SIM غير صالح، ويتبقى لديك محاولة واحدة (<xliff:g id="NUMBER_0">%d</xliff:g>)، تصبح بعدها شريحة SIM غير صالحة للاستخدام بشكل دائم.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"‏تعذّر إتمام عملية \"رقم التعريف الشخصي\" لشريحة SIM"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"‏تعذّر إتمام عملية PUK لشريحة SIM"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"تبديل أسلوب الإدخال"</string>
@@ -78,17 +92,30 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"يجب رسم النقش بعد إعادة تشغيل الجهاز"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"يجب إدخال رقم التعريف الشخصي بعد إعادة تشغيل الجهاز"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"يجب إدخال كلمة المرور بعد إعادة تشغيل الجهاز"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"لمزيد من الأمان، استخدِم النقش بدلاً من ذلك."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"لمزيد من الأمان، أدخِل رقم التعريف الشخصي بدلاً من ذلك."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"لمزيد من الأمان، أدخِل كلمة المرور بدلاً من ذلك."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"يجب رسم النقش لمزيد من الأمان"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"يجب إدخال رقم التعريف الشخصي لمزيد من الأمان"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"يجب إدخال كلمة المرور لمزيد من الأمان"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"اختار المشرف قفل الجهاز"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"تم حظر الجهاز يدويًا"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"لم يتم التعرّف عليه."</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"يجب منح الكاميرا إذن الوصول في \"الإعدادات\"."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{‏أدخِل رقم التعريف الشخصي لشريحة SIM. تتبقّى لديك محاولة واحدة ويجب بعدها التواصل مع مشغّل شبكة الجوّال لفتح قفل الجهاز.}zero{‏أدخِل رقم التعريف الشخصي لشريحة SIM. تتبقّى لديك # محاولة.}two{‏أدخِل رقم التعريف الشخصي لشريحة SIM. تتبقّى لديك محاولتان.}few{‏أدخِل رقم التعريف الشخصي لشريحة SIM. تتبقّى لديك # محاولات.}many{‏أدخِل رقم التعريف الشخصي لشريحة SIM. تتبقّى لديك # محاولةً.}other{‏أدخِل رقم التعريف الشخصي لشريحة SIM. تتبقّى لديك # محاولة.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{‏تم إيقاف شريحة SIM الآن. يجب إدخال رمز PUK للمتابعة. وتتبقّى لديك محاولة واحدة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. يمكنك التواصل مع مشغِّل شبكة الجوّال لمعرفة التفاصيل.}zero{‏تم إيقاف شريحة SIM الآن. يجب إدخال رمز PUK للمتابعة. وتتبقّى لديك # محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. يمكنك التواصل مع مشغِّل شبكة الجوّال لمعرفة التفاصيل.}two{‏تم إيقاف شريحة SIM الآن. يجب إدخال رمز PUK للمتابعة. وتتبقّى لديك محاولتان قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. يمكنك التواصل مع مشغِّل شبكة الجوّال لمعرفة التفاصيل.}few{‏تم إيقاف شريحة SIM الآن. يجب إدخال رمز PUK للمتابعة. وتتبقّى لديك # محاولات قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. يمكنك التواصل مع مشغِّل شبكة الجوّال لمعرفة التفاصيل.}many{‏تم إيقاف شريحة SIM الآن. يجب إدخال رمز PUK للمتابعة. وتتبقّى لديك # محاولةً قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. يمكنك التواصل مع مشغِّل شبكة الجوّال لمعرفة التفاصيل.}other{‏تم إيقاف شريحة SIM الآن. يجب إدخال رمز PUK للمتابعة. وتتبقّى لديك # محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. يمكنك التواصل مع مشغِّل شبكة الجوّال لمعرفة التفاصيل.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"‏لاستخدام ميزة \"فتح الجهاز بالتعرف على الوجه\"، عليك منح إذن "<b>"الوصول إلى الكاميرا"</b>" في الإعدادات &gt; الخصوصية."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="zero">‏أدخل رقم التعريف الشخصي لشريحة SIM. تتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولة.</item>
+      <item quantity="two">‏أدخل رقم التعريف الشخصي لشريحة SIM. تتبقى لديك محاولتان (<xliff:g id="NUMBER_1">%d</xliff:g>).</item>
+      <item quantity="few">‏أدخل رقم التعريف الشخصي لشريحة SIM. تتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولات.</item>
+      <item quantity="many">‏أدخل رقم التعريف الشخصي لشريحة SIM. تتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولة.</item>
+      <item quantity="other">‏أدخل رقم التعريف الشخصي لشريحة SIM. تتبقى لديك <xliff:g id="NUMBER_1">%d</xliff:g> محاولة.</item>
+      <item quantity="one">‏أدخل رقم التعريف الشخصي لشريحة SIM. تتبقى لديك محاولة واحدة (<xliff:g id="NUMBER_0">%d</xliff:g>) يجب أن تتصل بعدها بمشغّل شبكة الجوّال لفتح الجهاز.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="zero">‏تم إيقاف شريحة SIM الآن. أدخل رمز PUK للمتابعة، وتتبقى لديك <xliff:g id="_NUMBER_1">%d</xliff:g> محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. ويمكنك الاتصال بمشغل شبكة الجوّال لمعرفة التفاصيل.</item>
+      <item quantity="two">‏تم إيقاف شريحة SIM الآن. أدخل رمز PUK للمتابعة، وتتبقى لديك محاولتان (<xliff:g id="_NUMBER_1">%d</xliff:g>) قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. ويمكنك الاتصال بمشغل شبكة الجوّال لمعرفة التفاصيل.</item>
+      <item quantity="few">‏تم إيقاف شريحة SIM الآن. أدخل رمز PUK للمتابعة، وتتبقى لديك <xliff:g id="_NUMBER_1">%d</xliff:g> محاولات قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. ويمكنك الاتصال بمشغل شبكة الجوّال لمعرفة التفاصيل.</item>
+      <item quantity="many">‏تم إيقاف شريحة SIM الآن. أدخل رمز PUK للمتابعة، وتتبقى لديك <xliff:g id="_NUMBER_1">%d</xliff:g> محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. ويمكنك الاتصال بمشغل شبكة الجوّال لمعرفة التفاصيل.</item>
+      <item quantity="other">‏تم إيقاف شريحة SIM الآن. أدخل رمز PUK للمتابعة، وتتبقى لديك <xliff:g id="_NUMBER_1">%d</xliff:g> محاولة قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. ويمكنك الاتصال بمشغل شبكة الجوّال لمعرفة التفاصيل.</item>
+      <item quantity="one">‏تم إيقاف شريحة SIM الآن. أدخل رمز PUK للمتابعة، وتتبقى لديك محاولة واحدة (<xliff:g id="_NUMBER_0">%d</xliff:g>) قبل أن تصبح شريحة SIM غير صالحة للاستخدام نهائيًا. ويمكنك الاتصال بمشغل شبكة الجوّال لمعرفة التفاصيل.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"تلقائي"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"فقاعة"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ساعة تقليدية"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"يجب فتح قفل الجهاز للمتابعة"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-as/strings.xml b/packages/SystemUI/res-keyguard/values-as/strings.xml
index 4fcc56d..186f47a 100644
--- a/packages/SystemUI/res-keyguard/values-as/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-as/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ব্যৱহাৰৰ অযোগ্য ছিম কাৰ্ড"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"চ্চার্জ কৰা হ’ল"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • বেতাঁৰৰ জৰিয়তে চাৰ্জ কৰি থকা হৈছে"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চাৰ্জ কৰি থকা হৈছে"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চাৰ্জিং ডক"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চ্চার্জ কৰি থকা হৈছে"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • দ্ৰুত গতিৰে চ্চাৰ্জ কৰি থকা হৈছে"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • লাহে লাহে চ্চাৰ্জ কৰি থকা হৈছে"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • বেটাৰী সুৰক্ষিত কৰিবলৈ চাৰ্জিং অপ্টিমাইজ কৰা হৈছে"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চাৰ্জ কৰাটো সাময়িকভাৱে সীমিত কৰা হৈছে"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"আনলক কৰিবলৈ মেনু টিপক।"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"নেটৱর্ক লক কৰা অৱস্থাত আছে"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"কোনো ছিম নাই"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"এখন ছিম যোগ দিয়ক।"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"ছিম নাই অথবা সেইখন পঢ়িব নোৱাৰি। এখন ছিম যোগ দিয়ক।"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ব্যৱহাৰ কৰিব নোৱৰা ছিম।"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"আপোনাৰ ছিমখন স্থায়ীভাৱে নিষ্ক্ৰিয় কৰা হৈছে।\n অন্য এখন ছিমৰ বাবে আপোনাৰ ৱায়াৰলেছ সেৱা প্ৰদানকাৰীৰ সৈতে যোগাযোগ কৰক।"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"ছিমখন লক হৈ আছে।"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"ছিমখন PUKৰ দ্বাৰা লক হৈ আছে।"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"ছিম আনলক কৰি থকা হৈছে…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"কোনো ছিম কাৰ্ড নাই"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"এখন ছিম কাৰ্ড ভৰাওক।"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ছিম কাৰ্ডখন নাই বা চিনাক্ত কৰিব নোৱাৰি। এখন ছিম কাৰ্ড ভৰাওক।"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"ব্যৱহাৰৰ অযোগ্য ছিম কাৰ্ড।"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"আপোনাৰ ছিম কাৰ্ডখন স্থায়ীভাৱে অক্ষম হৈছে।\n অন্য এখন ছিমৰ বাবে আপোনাৰ ৱায়াৰলেছ সেৱা প্ৰদানকাৰীৰ সৈতে যোগাযোগ কৰক।"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"ছিম কাৰ্ড লক কৰা হৈছে।"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ছিম কার্ডখন PUKৰ দ্বাৰা লক কৰা হৈছে।"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"ছিম কার্ড আনলক কৰি থকা হৈছে…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"পিনৰ ক্ষেত্ৰ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ডিভাইচৰ পাছৱৰ্ড"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"ছিম পিনৰ ক্ষেত্ৰ"</string>
@@ -61,34 +61,45 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" ছিমখন বর্তমান অক্ষম অৱস্থাত আছে। অব্যাহত ৰাখিবলৈ PUK ক\'ড দিয়ক। সবিশেষ জানিবলৈ বাহকৰ সৈতে যোগাযোগ কৰক।"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"আপোনাৰ পছন্দৰ পিন ক\'ড লিখক"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"আপোনাৰ পচন্দৰ পিন ক\'ড নিশ্চিত কৰক"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"ছিম আনলক কৰি থকা হৈছে…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"ছিম কার্ড আনলক কৰি থকা হৈছে…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"৪টাৰ পৰা ৮টা সংখ্যাযুক্ত এটা পিন লিখক।"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK ক\'ডটো ৮টা বা তাতকৈ অধিক সংখ্যা থকা হ\'ব লাগিব।"</string>
-    <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"আপুনি আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
+    <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"আপুনি আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"আপুনি আপোনাৰ পাছৱৰ্ড <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
-    <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"আপুনি আপোনাৰ আনলক আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ আঁকিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
+    <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"আপুনি আপোনাৰ আনলক আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ আঁকিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ছিমৰ ভুল পিন ক\'ড, আপোনাৰ ডিভাইচটো আনলক কৰিবলৈ আপুনি এতিয়া আপোনাৰ বাহকৰ সৈতে যোগাযোগ কৰিবই লাগিব।"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{ছিমৰ পিন ক’ড ভুল হৈছে, আপোনাৰ ডিভাইচ আনলক কৰিবলৈ আপোনাৰ বাহকৰ লগত যোগাযোগ কৰিবই লগা হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}one{ছিমৰ পিন ক’ড ভুল হৈছে, আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। }other{ছিমৰ পিন ক’ড ভুল হৈছে, আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">ছিমৰ ভুল পিন ক’ড, আপুনি আৰু <xliff:g id="NUMBER_1">%d</xliff:g> বাৰ প্ৰয়াস কৰিব পাৰিব।</item>
+      <item quantity="other">ছিমৰ ভুল পিন ক’ড, আপুনি আৰু <xliff:g id="NUMBER_1">%d</xliff:g> বাৰ প্ৰয়াস কৰিব পাৰিব।</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ছিম ব্যৱহাৰযোগ্য নহয়। আপোনাৰ বাহকৰ সৈতে যোগাযোগ কৰক।"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{ছিমৰ PUK ক’ড ভুল হৈছে, ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}one{ছিমৰ PUK ক’ড ভুল হৈছে, ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}other{ছিমৰ PUK ক’ড ভুল হৈছে, ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">ছিমৰ ভুল PUK ক\'ড, আপুনি আৰু <xliff:g id="NUMBER_1">%d</xliff:g> বাৰ ভুল ক’ড দিলে আপোনাৰ ছিম চিৰকালৰ বাবে ব্যৱহাৰৰ অনুপযোগী হ’ব।</item>
+      <item quantity="other">ছিমৰ ভুল PUK ক\'ড, আপুনি আৰু <xliff:g id="NUMBER_1">%d</xliff:g> বাৰ ভুল ক’ড দিলে আপোনাৰ ছিম চিৰকালৰ বাবে ব্যৱহাৰৰ অনুপযোগী হ’ব।</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"ছিম পিনৰ জৰিয়তে আনলক কৰিব পৰা নগ\'ল!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"ছিম PUKৰ জৰিয়তে আনলক কৰিব পৰা নগ\'ল!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ইনপুট পদ্ধতি সলনি কৰক"</string>
     <string name="airplane_mode" msgid="2528005343938497866">"এয়াৰপ্লে’ন ম’ড"</string>
-    <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত আৰ্হি দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পিন দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পাছৱৰ্ড দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"অতিৰিক্ত সুৰক্ষাৰ বাবে, ইয়াৰ পৰিৱৰ্তে আৰ্হি ব্যৱহাৰ কৰক"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"অতিৰিক্ত সুৰক্ষাৰ বাবে, ইয়াৰ পৰিৱৰ্তে পিন ব্যৱহাৰ কৰক"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"অতিৰিক্ত সুৰক্ষাৰ বাবে, ইয়াৰ পৰিৱৰ্তে পাছৱৰ্ড ব্যৱহাৰ কৰক"</string>
+    <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পিছত আৰ্হি দিয়াটো বাধ্যতামূলক"</string>
+    <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পিছত পিন দিয়াটো বাধ্যতামূলক"</string>
+    <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পিছত পাছৱৰ্ড দিয়াটো বাধ্যতামূলক"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিৰিক্ত সুৰক্ষাৰ বাবে আর্হি দিয়াটো বাধ্যতামূলক"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিৰিক্ত সুৰক্ষাৰ বাবে পিন দিয়াটো বাধ্যতামূলক"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিৰিক্ত সুৰক্ষাৰ বাবে পাছৱর্ড দিয়াটো বাধ্যতামূলক"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্ৰশাসকে ডিভাইচ লক কৰি ৰাখিছে"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইচটো মেনুৱেলভাৱে লক কৰা হৈছিল"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"চিনাক্ত কৰিব পৰা নাই"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ফেচ আনলক ব্যৱহাৰ কৰিবলৈ ছেটিঙত কেমেৰাৰ এক্সেছ অন কৰক"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{ছিমৰ পিন দিয়ক। আপোনাৰ ডিভাইচ আনলক কৰিবলৈ আপোনাৰ বাহকৰ লগত যোগাযোগ কৰিবই লগা হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}one{ছিমৰ পিন দিয়ক। আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}other{ছিমৰ পিন দিয়ক। আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ছিমখন এতিয়া অক্ষম কৰা হৈছে। অব্যাহত ৰাখিবলৈ PUK ক’ড দিয়ক। ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। সবিশেষৰ বাবে বাহকৰ সৈতে যোগাযোগ কৰক।}one{ছিমখন এতিয়া অক্ষম কৰা হৈছে। অব্যাহত ৰাখিবলৈ PUK ক’ড দিয়ক। ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। সবিশেষৰ বাবে বাহকৰ সৈতে যোগাযোগ কৰক।}other{ছিমখন এতিয়া অক্ষম কৰা হৈছে। অব্যাহত ৰাখিবলৈ PUK ক’ড দিয়ক। ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। সবিশেষৰ বাবে বাহকৰ সৈতে যোগাযোগ কৰক।}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ফেচ আনলক সুবিধাটো ব্যৱহাৰ কৰিবলৈ ছেটিং &gt; গোপনীয়তাত "<b>"কেমেৰাৰ এক্সেছ"</b>" অন কৰক"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">ছিমৰ পিন দিয়ক। আপুনি আৰু <xliff:g id="NUMBER_1">%d</xliff:g>বাৰ প্ৰয়াস কৰিব পাৰে।</item>
+      <item quantity="other">ছিমৰ পিন দিয়ক। আপুনি আৰু <xliff:g id="NUMBER_1">%d</xliff:g>বাৰ প্ৰয়াস কৰিব পাৰে।</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">ছিমখন অক্ষম হ’ল। অব্যাহত ৰাখিবলৈ PUK দিয়ক। ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ হাতত <xliff:g id="_NUMBER_1">%d</xliff:g>টা প্ৰয়াস বাকী আছে। সবিশেষ জানিবলৈ বাহকৰ সৈতে যোগাযোগ কৰক।</item>
+      <item quantity="other">ছিমখন অক্ষম হ’ল। অব্যাহত ৰাখিবলৈ PUK দিয়ক। ছিমখন স্থায়ীভাৱে ব্যৱহাৰৰ অনুপযোগী হোৱাৰ পূৰ্বে আপোনাৰ হাতত <xliff:g id="_NUMBER_1">%d</xliff:g>টা প্ৰয়াস বাকী আছে। সবিশেষ জানিবলৈ বাহকৰ সৈতে যোগাযোগ কৰক।</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ডিফ’ল্ট"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"এনাল’গ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"অব্যাহত ৰাখিবলৈ আপোনাৰ ডিভাইচটো আনলক কৰক"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-az/strings.xml b/packages/SystemUI/res-keyguard/values-az/strings.xml
index e8f0f81..71fd47b 100644
--- a/packages/SystemUI/res-keyguard/values-az/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-az/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Yanlış Kart."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Enerji yığılıb"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Simsiz şəkildə batareya yığır"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj edilir"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj Doku"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Enerji yığır"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Sürətlə enerji yığır"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Yavaş enerji yığır"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Batareyanı qorumaq üçün şarj optimallaşdırılıb"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj müvəqqəti məhdudlaşdırılıb"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Kilidi açmaq üçün Menyu düyməsinə basın."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Şəbəkə kilidlidir"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM yoxdur"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM əlavə edin."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM kart yoxdur və ya oxuna bilinmir. SIM əlavə edin."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"İstifadəyə yararsız SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM kartınız həmişəlik deaktiv edilib.\n Başqa SIM kart üçün simsiz xidmət provayderinizə müraciət edin."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM kilidlənib."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM kart PUK ilə kilidlənib."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM kiliddən çıxarılır…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM kart yoxdur."</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM kart daxil edin."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM kart yoxdur və ya oxuna bilinmir. SIM kart daxil edin."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Yararsız SIM kart."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM kartınız həmişəlik deaktivləşib.\n Başqa SIM kart üçün simsiz xidmət provayderinə müraciət edin."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kart kilidlənib."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kart PUK ilə kilidlənib."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM kartın kilidi açılır..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN sahəsi"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Cihaz parolu"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN sahəsi"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" indi deaktivdir. Davam etmək üçün PUK kodu daxil edin. Ətraflı məlumat üçün operatorla əlaqə saxlayın."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"İstədiyiniz PIN kodu daxil edin"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"İstədiyiniz PIN kodu təsdiqləyin"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM kiliddən çıxarılır…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM kartın kilidi açılır..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4-8 rəqəmli PIN daxil edin."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kod 8 rəqəm və ya daha çox olmalıdır."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN kodu <xliff:g id="NUMBER_0">%1$d</xliff:g> dəfə yanlış daxil etdiniz. \n \n<xliff:g id="NUMBER_1">%2$d</xliff:g> saniyə sonra yenidən cəhd edin."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Parolu <xliff:g id="NUMBER_0">%1$d</xliff:g> dəfə yanlış daxil etdiniz. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> saniyə sonra yenidən cəhd edin."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Kilid modelini <xliff:g id="NUMBER_0">%1$d</xliff:g> dəfə yanlış çəkdiniz. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> saniyə sonra yenidən cəhd edin."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Yanlış SIM PIN kodu  cihazın açılması üçün operatorla indi əlaqə saxlamalısınız."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Yanlış SIM PIN kodu, # cəhdiniz qalıb, sonra cihazı kiliddən çıxarmaq üçün operatorla əlaqə saxlamalı olacaqsınız.}other{Yanlış SIM PIN kodu, # cəhdiniz qalıb. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Yanlış SIM PIN kodu, <xliff:g id="NUMBER_1">%d</xliff:g> cəhdiniz qalır.</item>
+      <item quantity="one">Yanlış SIM PIN kodu, <xliff:g id="NUMBER_0">%d</xliff:g> cəhddən sonra cihazı kiliddən çıxarmaq üçün operatorla əlaqə saxlamalısınız.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM yararsızdır. Operatorla əlaqə saxlayın."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Yanlış SIM PUK kodu, # cəhdiniz qalıb, sonra SIM kart həmişəlik yararsız olacaq.}other{Yanlış SIM PUK kodu, # cəhdiniz qalıb, sonra SIM kart həmişəlik yararsız olacaq.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Yanlış SIM PUK kodu,<xliff:g id="NUMBER_1">%d</xliff:g> cəhddən sonra SIM kart həmişəlik yararsız olacaq.</item>
+      <item quantity="one">Yanlış SIM PUK kodu, <xliff:g id="NUMBER_0">%d</xliff:g> cəhddən sonra SIM kart həmişəlik yararsız olacaq.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN əməliyyatı alınmadı!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK əməliyyatı alınmadı!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Daxiletmə metoduna keçin"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yenidən başladıqdan sonra model tələb olunur"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıqdan sonra PIN tələb olunur"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıqdan sonra parol tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Əlavə təhlükəsizlik üçün modeldən istifadə edin"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Əlavə təhlükəsizlik üçün PIN istifadə edin"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Əlavə təhlükəsizlik üçün paroldan istifadə edin"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Əlavə təhlükəsizlik üçün model tələb olunur"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Əlavə təhlükəsizlik üçün PIN tələb olunur"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Əlavə təhlükəsizlik üçün parol tələb olunur"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz admin tərəfindən kilidlənib"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihaz əl ilə kilidləndi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmır"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Üz ilə Kiliddən Açmanı istifadə etmək üçün Ayarlarda kameraya girişi aktiv edin"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PIN kodunu daxil edin. # cəhdiniz qalıb, sonra cihazınızı kiliddən çıxarmaq üçün operatorunuzla əlaqə saxlamalı olacaqsınız.}other{SIM PIN kodunu daxil edin. # cəhdiniz qalıb.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM indi deaktivdir. Davam etmək üçün PUK kodunu daxil edin. # cəhdiniz qalıb, sonra SIM birdəfəlik yararsız olacaq. Ətraflı məlumat üçün operatorla əlaqə saxlayın.}other{SIM indi deaktivdir. Davam etmək üçün PUK kodunu daxil edin. # cəhdiniz qalıb, sonra SIM birdəfəlik yararsız olacaq. Ətraflı məlumat üçün operatorla əlaqə saxlayın.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Üz ilə Kiliddən Açma funksiyasını istifadə etmək üçün Ayarlar &gt; Məxfilik bölməsində "<b>"Kameraya girişi"</b>" aktiv edin"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN-ni daxil edin. <xliff:g id="NUMBER_1">%d</xliff:g> cəhdiniz qalır.</item>
+      <item quantity="one">SIM PIN-ni daxil edin. Cihazınızı kiliddən çıxarmaq üçün operatorunuzla əlaqə saxlamadan öncə <xliff:g id="NUMBER_0">%d</xliff:g> cəhdiniz qalır.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM indi deaktivdir. Davam etmək üçün PUK kodunu daxil edin. SIM birdəfəlik yararsız olmadan öncə <xliff:g id="_NUMBER_1">%d</xliff:g> cəhdiniz qalır. Ətraflı məlumat üçün operatorla əlaqə saxlayın.</item>
+      <item quantity="one">SIM indi deaktivdir. Davam etmək üçün PUK kodunu daxil edin. SIM birdəfəlik yararsız olmadan öncə <xliff:g id="_NUMBER_0">%d</xliff:g> cəhdiniz qalır. Ətraflı məlumat üçün operatorla əlaqə saxlayın.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Defolt"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Qabarcıq"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoq"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Davam etmək üçün cihazınızın kilidini açın"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
index c3b7cc6..c04a766 100644
--- a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Nevažeća kartica."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Napunjena je"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Bežično punjenje"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Puni se"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Bazna stanica za punjenje"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Puni se"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Brzo se puni"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Sporo se puni"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje je optimizovano da bi se zaštitila baterija"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje je privremeno ograničeno"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pritisnite Meni da biste otključali."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Mreža je zaključana"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nema SIM-a"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Dodajte SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM nedostaje ili ne može da se pročita. Dodajte SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Neupotrebljiv SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM je trajno deaktiviran.\n Obratite se dobavljaču usluge bežične telefonije da biste dobili drugi SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM je zaključan."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM je zaključan PUK-om."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Otključava se SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nema SIM kartice"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Umetnite SIM karticu."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM kartica nedostaje ili ne može da se pročita. Umetnite SIM karticu."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM kartica je neupotrebljiva."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM kartica je trajno onemogućena.\nObratite se dobavljaču usluge bežične mreže da biste dobili drugu SIM karticu."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kartica je zaključana."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kartica je zaključana PUK kodom."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM kartica se otključava…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Oblast za PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Lozinka za uređaj"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Oblast za PIN za SIM"</string>
@@ -61,16 +61,24 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM „<xliff:g id="CARRIER">%1$s</xliff:g>“ je sada onemogućen. Unesite PUK kôd da biste nastavili. Detaljne informacije potražite od mobilnog operatera."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Unesite željeni PIN kôd"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potvrdite željeni PIN kôd"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Otključava se SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM kartica se otključava…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Unesite PIN koji ima 4–8 brojeva."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kôd treba da ima 8 ili više brojeva."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Uneli ste pogrešan PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> puta. \n\nProbajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> sek."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Uneli ste pogrešnu lozinku <xliff:g id="NUMBER_0">%1$d</xliff:g> puta. \n\nProbajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> sek."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Nacrtali ste netačan šablon za otključavanje <xliff:g id="NUMBER_0">%1$d</xliff:g> puta. \n\nProbajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> sek."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Netačan PIN kôd za SIM. Sada morate da kontaktirate mobilnog operatera da biste otključali uređaj."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Netačan PIN za SIM kôd. Imate još # pokušaj, a onda morate da se obratite mobilnom operateru da biste otključali uređaj.}one{Netačan PIN za SIM kôd. Imate još # pokušaj. }few{Netačan PIN za SIM kôd. Imate još # pokušaja. }other{Netačan PIN za SIM kôd. Imate još # pokušaja. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Netačan PIN kôd za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj.</item>
+      <item quantity="few">Netačan PIN kôd za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+      <item quantity="other">Netačan PIN kôd za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kartica je neupotrebljiva. Kontaktirajte mobilnog operatera."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Netačan SIM PUK kôd. Imate još # pokušaj pre nego što SIM kartica postane trajno neupotrebljiva.}one{Netačan PUK kôd za SIM. Imate još # pokušaj pre nego što SIM kartica postane trajno neupotrebljiva.}few{Netačan PUK kôd za SIM. Imate još # pokušaja pre nego što SIM kartica postane trajno neupotrebljiva.}other{Netačan PUK kôd za SIM. Imate još # pokušaja pre nego što SIM kartica postane trajno neupotrebljiva.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Netačan PUK kôd za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj pre nego što SIM kartica postane trajno neupotrebljiva.</item>
+      <item quantity="few">Netačan PUK kôd za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja pre nego što SIM kartica postane trajno neupotrebljiva.</item>
+      <item quantity="other">Netačan PUK kôd za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja pre nego što SIM kartica postane trajno neupotrebljiva.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Radnja sa PIN kodom za SIM nije uspela!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Radnja sa PUK kodom za SIM nije uspela!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Promeni metod unosa"</string>
@@ -78,17 +86,24 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Treba da unesete šablon kada se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Treba da unesete PIN kada se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Treba da unesete lozinku kada se uređaj ponovo pokrene"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Za dodatnu bezbednost koristite šablon"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Za dodatnu bezbednost koristite PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Za dodatnu bezbednost koristite lozinku"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Treba da unesete šablon radi dodatne bezbednosti"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Treba da unesete PIN radi dodatne bezbednosti"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Treba da unesete lozinku radi dodatne bezbednosti"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznat"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Otključavanje licem traži pristup kameri u Podešavanjima"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Unesite PIN za SIM. Još # pokušaj i moraćete da se obratite mobilnom operateru da biste otključali uređaj.}one{Unesite PIN za SIM. Imate još # pokušaj.}few{Unesite PIN za SIM. Imate još # pokušaja.}other{Unesite PIN za SIM. Imate još # pokušaja.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaj pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.}one{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaj pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.}few{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaja pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.}other{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaja pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Da biste koristili otključavanje licem, uključite "<b>"pristup kameri"</b>" u odeljku Podešavanja &gt; Privatnost"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj.</item>
+      <item quantity="few">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+      <item quantity="other">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaj pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.</item>
+      <item quantity="few">SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaja pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.</item>
+      <item quantity="other">SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaja pre nego što SIM postane trajno neupotrebljiv. Detaljne informacije potražite od mobilnog operatera.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Podrazumevani"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurići"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Otključajte uređaj da biste nastavili"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-be/strings.xml b/packages/SystemUI/res-keyguard/values-be/strings.xml
index dce92dc..eda5497f 100644
--- a/packages/SystemUI/res-keyguard/values-be/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-be/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Несапраўдная картка."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Зараджаны"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ідзе бесправадная зарадка"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ідзе зарадка"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ідзе зарадка праз док-станцыю"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ідзе зарадка"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ідзе хуткая зарадка"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ідзе павольная зарадка"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • У мэтах зберажэння акумулятара зарадка аптымізавана"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарадка часова абмежавана"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Націсніце кнопку \"Меню\", каб разблакіраваць."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Сетка заблакіравана"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Няма SIM-карты"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Дадайце SIM-карту."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-карта адсутнічае ці не чытаецца. Дадайце SIM-карту."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Непрыдатная для выкарыстання SIM-карта."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Ваша SIM-карта адключана назаўсёды.\n Звяжыцеся з аператарам бесправадной сувязі, каб атрымаць іншую SIM-карту."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-карта заблакіравана."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-карта заблакіравана PUK-кодам."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Разблакіраванне SIM-карты…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Няма SIM-карты"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Устаўце SIM-карту."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-карта адсутнічае ці не чытаецца. Устаўце SIM-карту."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM-карту немагчыма выкарыстоўваць."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Ваша SIM-карта была адключана назаўсёды.\n Звяжыцеся з аператарам бесправадной сувязі, каб атрымаць іншую SIM-карту."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-карта заблакіравана."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-карта заблакіравана PUK-кодам."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Ідзе разблакіроўка SIM-карты…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Поле для PIN-кода"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Пароль прылады"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Поле для PIN-кода SIM-карты"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-карта \"<xliff:g id="CARRIER">%1$s</xliff:g>\" зараз адключана. Увядзіце PUK-код, каб працягнуць. Каб атрымаць дадатковую інфармацыю, звяжыцеся з аператарам."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Увядзіце пажаданы PIN-код"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Пацвердзіце пажаданы PIN-код"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Разблакіраванне SIM-карты…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Ідзе разблакіроўка SIM-карты…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Увядзіце PIN-код, які змяшчае ад 4 да 8 лічбаў."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-код павінен утрымліваць 8 лічбаў ці больш."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Вы няправільна ўвялі PIN-код столькі разоў: <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nПаўтарыце спробу праз <xliff:g id="NUMBER_1">%2$d</xliff:g> с."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Вы няправільна ўвялі пароль столькі разоў: <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nПаўтарыце спробу праз <xliff:g id="NUMBER_1">%2$d</xliff:g> с."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Вы няправільна ўвялі ўзор разблакіроўкі столькі разоў: <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nПаўтарыце спробу праз <xliff:g id="NUMBER_1">%2$d</xliff:g> с."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Няправільны PIN-код SIM-карты, цяпер вы павінны звязацца з аператарам для разблакіроўкі прылады."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Няправільны PIN-код SIM-карты. У вас засталася # спроба, інакш вам прыйдзецца звязацца з аператарам для разблакіроўкі прылады.}one{Няправільны PIN-код SIM-карты, у вас засталіся # спроба. }few{Няправільны PIN-код SIM-карты, у вас засталіся # спробы. }many{Няправільны PIN-код SIM-карты, у вас засталіся # спроб. }other{Няправільны PIN-код SIM-карты, у вас засталіся # спробы. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Няправільны PIN-код SIM-карты, у вас засталася <xliff:g id="NUMBER_1">%d</xliff:g> спроба.</item>
+      <item quantity="few">Няправільны PIN-код SIM-карты, у вас засталіся <xliff:g id="NUMBER_1">%d</xliff:g> спробы.</item>
+      <item quantity="many">Няправільны PIN-код SIM-карты, у вас засталіся <xliff:g id="NUMBER_1">%d</xliff:g> спроб.</item>
+      <item quantity="other">Няправільны PIN-код SIM-карты, у вас засталіся <xliff:g id="NUMBER_1">%d</xliff:g> спробы.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-карта не прыдатная для выкарыстання. Звяжыцеся з аператарам."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Няправільны PUK-код SIM-карты, у вас засталася # спроба перад тым, як SIM-карта перастане працаваць назаўжды.}one{Няправільны PUK-код SIM-карты, у вас засталося # спроба перад тым, як SIM-карта перастане працаваць назаўжды.}few{Няправільны PUK-код SIM-карты, у вас засталося # спробы перад тым, як SIM-карта перастане працаваць назаўжды.}many{Няправільны PUK-код SIM-карты, у вас засталося # спроб перад тым, як SIM-карта перастане працаваць назаўжды.}other{Няправільны PUK-код SIM-карты, у вас засталося # спробы перад тым, як SIM-карта перастане працаваць назаўжды.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Няправільны PUK-код SIM-карты, у вас засталася <xliff:g id="NUMBER_1">%d</xliff:g> спроба перад тым, як SIM-карта перастане працаваць назаўжды.</item>
+      <item quantity="few">Няправільны PUK-код SIM-карты, у вас засталіся <xliff:g id="NUMBER_1">%d</xliff:g> спробы перад тым, як SIM-карта перастане працаваць назаўжды.</item>
+      <item quantity="many">Няправільны PUK-код SIM-карты, у вас засталіся <xliff:g id="NUMBER_1">%d</xliff:g> спроб перад тым, як SIM-карта перастане працаваць назаўжды.</item>
+      <item quantity="other">Няправільны PUK-код SIM-карты, у вас засталіся <xliff:g id="NUMBER_1">%d</xliff:g> спробы перад тым, як SIM-карта перастане працаваць назаўжды.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Разблакіраваць SIM-карту PIN-кодам не атрымалася!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Разблакіраваць SIM-карту PUK-кодам не атрымалася!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Пераключэнне рэжыму ўводу"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Пасля перазапуску прылады патрабуецца ўзор"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Пасля перазапуску прылады патрабуецца PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Пасля перазапуску прылады патрабуецца пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"У мэтах дадатковай бяспекі скарыстайце ўзор разблакіроўкі"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"У мэтах дадатковай бяспекі скарыстайце PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"У мэтах дадатковай бяспекі скарыстайце пароль"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для забеспячэння дадатковай бяспекі патрабуецца ўзор"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для забеспячэння дадатковай бяспекі патрабуецца PIN-код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для забеспячэння дадатковай бяспекі патрабуецца пароль"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Прылада заблакіравана адміністратарам"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Прылада была заблакіравана ўручную"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распазнана"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Для распазнавання твару ўключыце доступ да камеры"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Увядзіце PIN-код SIM-карты. У вас засталася # спроба. Пасля гэтага вам давядзецца звярнуцца да свайго аператара, каб разблакіраваць прыладу.}one{Увядзіце PIN-код SIM-карты. У вас засталося # спроба.}few{Увядзіце PIN-код SIM-карты. У вас засталося # спробы.}many{Увядзіце PIN-код SIM-карты. У вас засталося # спроб.}other{Увядзіце PIN-код SIM-карты. У вас засталося # спробы.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ # спроба, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.}one{SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ # спроба, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.}few{SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ # спробы, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.}many{SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ # спроб, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.}other{SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ # спробы, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Каб выкарыстоўваць распазнаванне твару, уключыце "<b>"доступ да камеры"</b>" праз раздзел \"Налады &gt; Прыватнасць\""</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Увядзіце PIN-код SIM-карты. У вас засталася <xliff:g id="NUMBER_1">%d</xliff:g> спроба.</item>
+      <item quantity="few">Увядзіце PIN-код SIM-карты. У вас засталося <xliff:g id="NUMBER_1">%d</xliff:g> спробы.</item>
+      <item quantity="many">Увядзіце PIN-код SIM-карты. У вас засталося <xliff:g id="NUMBER_1">%d</xliff:g> спроб.</item>
+      <item quantity="other">Увядзіце PIN-код SIM-карты. У вас засталося <xliff:g id="NUMBER_1">%d</xliff:g> спробы.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ <xliff:g id="_NUMBER_1">%d</xliff:g> спроба, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.</item>
+      <item quantity="few">SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ <xliff:g id="_NUMBER_1">%d</xliff:g> спробы, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.</item>
+      <item quantity="many">SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ <xliff:g id="_NUMBER_1">%d</xliff:g> спроб, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.</item>
+      <item quantity="other">SIM-карта заблакіравана. Каб працягнуць, увядзіце PUK-код. У вас ёсць яшчэ <xliff:g id="_NUMBER_1">%d</xliff:g> спробы, пасля чаго SIM-карта будзе заблакіравана назаўсёды. Звярніцеся да аператара, каб даведацца больш.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Стандартны"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бурбалкі"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Са стрэлкамі"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Каб працягнуць, разблакіруйце прыладу"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bg/strings.xml b/packages/SystemUI/res-keyguard/values-bg/strings.xml
index 25c9cf6..dcf0a05 100644
--- a/packages/SystemUI/res-keyguard/values-bg/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bg/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Картата е невалидна."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Заредена"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарежда се безжично"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарежда се"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Докинг станция за зареждане"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарежда се"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарежда се бързо"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарежда се бавно"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зареждането е оптимизирано с цел запазване на батерията"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зареждането временно е ограничено"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Натиснете „Меню“, за да отключите."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Мрежата е заключена"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Няма SIM карта"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Добавете SIM карта."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM картата липсва или е нечетлива. Добавете SIM карта."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Неизползваема SIM карта."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM картата ви е деактивирана за постоянно.\nЗа да получите друга, се свържете с доставчика си на безжична услуга."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM картата е заключена."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM картата е заключена с PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM картата се отключва…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Няма SIM карта"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Поставете SIM карта."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM картата липсва или е нечетлива. Поставете SIM карта."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Неизползваема SIM карта."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM картата ви е деактивирана за постоянно.\nЗа да получите друга, се свържете с доставчика на безжичната си услуга."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM картата е заключена."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM картата е заключена с PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM картата се отключва..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Област за ПИН кода"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Парола за устройството"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Област за ПИН кода на SIM картата"</string>
@@ -61,34 +61,45 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM картата „<xliff:g id="CARRIER">%1$s</xliff:g>“ вече е деактивирана. Въведете PUK кодa, за да продължите. Свържете се с оператора за подробности."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Въведете желания ПИН код"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Потвърдете желания ПИН код"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM картата се отключва…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM картата се отключва..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Въведете ПИН код с четири до осем цифри."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK кодът трябва да е с осем или повече цифри."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Въведохте неправилно ПИН кода си <xliff:g id="NUMBER_0">%1$d</xliff:g> пъти. \n\nОпитайте отново след <xliff:g id="NUMBER_1">%2$d</xliff:g> секунди."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Въведохте неправилно паролата си <xliff:g id="NUMBER_0">%1$d</xliff:g> пъти. \n\nОпитайте отново след <xliff:g id="NUMBER_1">%2$d</xliff:g> секунди."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Начертахте неправилно фигурата си за отключване <xliff:g id="NUMBER_0">%1$d</xliff:g> пъти. \n\nОпитайте отново след <xliff:g id="NUMBER_1">%2$d</xliff:g> секунди."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Неправилен ПИН код за SIM картата – сега трябва да се свържете с оператора си, за да отключите устройството."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Неправилен ПИН код за SIM картата. Остава ви # опит, преди да трябва да се свържете с оператора си, за да отключите устройството.}other{Неправилен ПИН код за SIM картата. Остават ви # опита. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Неправилен ПИН код за SIM картата – остават ви <xliff:g id="NUMBER_1">%d</xliff:g> опита.</item>
+      <item quantity="one">Неправилен ПИН код за SIM картата – остава ви <xliff:g id="NUMBER_0">%d</xliff:g> опит, преди да трябва да се свържете с оператора си, за да отключите устройството.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM картата е неизползваема. Свържете се с оператора си."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Неправилен PUK код за SIM картата. Остава ви # опит, преди тя да стане неизползваема завинаги.}other{Неправилен PUK код за SIM картата. Остават ви # опита, преди тя да стане неизползваема завинаги.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Неправилен PUK код за SIM картата – остават ви <xliff:g id="NUMBER_1">%d</xliff:g> опита, преди тя да стане неизползваема завинаги.</item>
+      <item quantity="one">Неправилен PUK код за SIM картата – остава ви <xliff:g id="NUMBER_0">%d</xliff:g> опит, преди тя да стане неизползваема завинаги.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Операцията с ПИН кода за SIM картата не бе успешна!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Операцията с PUK кода за SIM картата не бе успешна!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Превключване на метода на въвеждане"</string>
-    <string name="airplane_mode" msgid="2528005343938497866">"Самолет. режим"</string>
+    <string name="airplane_mode" msgid="2528005343938497866">"Самолетен режим"</string>
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"След рестартиране на устройството се изисква фигура"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"След рестартиране на устройството се изисква ПИН код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"След рестартиране на устройството се изисква парола"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За допълнителна сигурност използвайте фигура вместо това"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За допълнителна сигурност използвайте ПИН код вместо това"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За допълнителна сигурност използвайте парола вместо това"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"За допълнителна сигурност се изисква фигура"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"За допълнителна сигурност се изисква ПИН код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"За допълнителна сигурност се изисква парола"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройството е заключено от администратора"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройството бе заключено ръчно"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не е разпознато"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"За да ползвате „Отключване с лице“, вкл. достъпа до камерата от настройките"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Въведете ПИН кода за SIM картата. Остава ви # опит, преди да трябва да се свържете с оператора си, за да отключите устройството.}other{Въведете ПИН кода за SIM картата. Остават ви # опита.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM картата вече е деактивирана. Въведете PUK кода, за да продължите. Остава ви # опит, преди SIM картата да стане неизползваема завинаги. Свържете се с оператора за подробности.}other{SIM картата вече е деактивирана. Въведете PUK кода, за да продължите. Остават ви # опита, преди SIM картата да стане неизползваема завинаги. Свържете се с оператора за подробности.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"За да използвате функцията „Отключване с лице“, включете "<b>"достъпа до камерата"</b>" от „Настройки &gt; Поверителност“"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Въведете ПИН кода за SIM картата – остават ви <xliff:g id="NUMBER_1">%d</xliff:g> опита.</item>
+      <item quantity="one">Въведете ПИН кода за SIM картата – остава ви <xliff:g id="NUMBER_0">%d</xliff:g> опит, преди да се наложи да се свържете с оператора си, за да отключите устройството.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM картата вече е деактивирана. Въведете PUK кода, за да продължите. Остават ви <xliff:g id="_NUMBER_1">%d</xliff:g> опита, преди SIM картата да стане неизползваема завинаги. Свържете се с оператора за подробности.</item>
+      <item quantity="one">SIM картата вече е деактивирана. Въведете PUK кода, за да продължите. Остава ви <xliff:g id="_NUMBER_0">%d</xliff:g> опит, преди SIM картата да стане неизползваема завинаги. Свържете се с оператора за подробности.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Стандартен"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Балонен"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогов"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Отключете устройството си, за да продължите"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bn/strings.xml b/packages/SystemUI/res-keyguard/values-bn/strings.xml
index 1e87585..f96db50 100644
--- a/packages/SystemUI/res-keyguard/values-bn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bn/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ভুল কার্ড।"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"চার্জ হয়েছে"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ওয়্যারলেস পদ্ধতিতে চার্জ হচ্ছে"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চার্জ হচ্ছে"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চার্জিং ডক"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চার্জ হচ্ছে"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • দ্রুত চার্জ হচ্ছে"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ধীরে চার্জ হচ্ছে"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ব্যাটারি ভাল রাখতে চার্জিং অপ্টিমাইজ করা হয়েছে"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • চার্জ করা সাময়িকভাবে বন্ধ রাখা হয়েছে"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"আনলক করতে মেনুতে টিপুন।"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"নেটওয়ার্ক লক করা আছে"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"কোনও সিম নেই"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"সিম যোগ করুন।"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"সিম নেই অথবা সেটি রিড করা যাচ্ছে না। সিম যোগ করুন।"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ব্যবহারযোগ্য নয় এমন সিম।"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"আপনার সিম স্থায়ীভাবে বন্ধ করে দেওয়া হয়েছে।\n অন্য একটি সিমের জন্য আপনার ওয়্যারলেস পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"সিম লক করা হয়েছে।"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"সিম PUK লক করা হয়েছে।"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"সিম আনলক করা হচ্ছে…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"কোনো সিম কার্ড নেই"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"একটি সিম কার্ড লাগান।"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"সিম কার্ড নেই বা সেটি পড়া যাচ্ছে না। একটি সিম কার্ড লাগান।"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"অব্যবহারযোগ্য সিম কার্ড।"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"আপনার সিম কার্ডটি স্থায়ীভাবে অক্ষম করা হয়েছে।\n অন্য একটি সিম কার্ড পেতে আপনার ওয়্যারলেস পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"সিম কার্ড লক করা আছে।"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"সিম কার্ডটি PUK কোড দিয়ে লক করা আছে।"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"সিম কার্ড আনলক করা হচ্ছে…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"পিন অঞ্চল"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ডিভাইসের পাসওয়ার্ড"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"সিম পিন অঞ্চল"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"সিম <xliff:g id="CARRIER">%1$s</xliff:g> এখন অক্ষম করা হয়েছে। চালিয়ে যেতে PUK কোডটি লিখুন। বিশদ বিবরণের জন্য পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"আপনার পছন্দের পিন কোড লিখুন"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"পছন্দের পিন কোড নিশ্চিত করুন"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"সিম আনলক করা হচ্ছে…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"সিম কার্ড আনলক করা হচ্ছে…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"একটি ৪ থেকে ৮ সংখ্যার পিন লিখুন।"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK কোডটি ৮ বা তার বেশি সংখ্যার হতে হবে।"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"আপনি আপনার পিন টাইপ করতে <xliff:g id="NUMBER_0">%1$d</xliff:g> বার ভুল করেছেন৷ \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> সেকেন্ডের মধ্যে আবার চেষ্টা করুন৷"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"আপনি <xliff:g id="NUMBER_0">%1$d</xliff:g> বার ভুলভাবে আপনার পাসওয়ার্ড লিখেছেন।\n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> সেকেন্ডের মধ্যে আবার চেষ্টা করুন।"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"আপনি <xliff:g id="NUMBER_0">%1$d</xliff:g> বার ভুলভাবে আপনার আনলকের প্যাটার্ন এঁকেছেন।\n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> সেকেন্ডের মধ্যে আবার চেষ্টা করুন।"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ভুল সিম পিন কোড দিয়েছেন, আপনার ডিভাইসটি আনলক করতে এখন আপনাকে অবশ্যই আপনার পরিষেবা প্রদানকারীর সাথে যোগাযোগ করতে হবে।"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{সিম কার্ডের ভুল পিন কোড দিয়েছেন, আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে ডিভাইস আনলক করতে পরিষেবা প্রদানকারীর সাথে যোগাযোগ করতে হবে।}one{সিম কার্ডের ভুল পিন কোড দিয়েছেন, আপনি আরও # বার চেষ্টা করতে পারবেন। }other{সিম কার্ডের ভুল পিন কোড দিয়েছেন, আপনি আরও # বার চেষ্টা করতে পারবেন। }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">সিমের পিন কোডটি ভুল, আপনি আর <xliff:g id="NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারেন।</item>
+      <item quantity="other">সিমের পিন কোডটি ভুল, আপনি আর <xliff:g id="NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারেন।</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"সিমটি ব্যবহার করা যাবে না। আপনার পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{সিম কার্ডের ভুল PUK কোড দিয়েছেন, আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে এই সিম কার্ডটি স্থায়ীভাবে কাজ করা বন্ধ করে দেবে।}one{সিম কার্ডের ভুল PUK কোড দিয়েছেন, আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে এই সিম কার্ডটি স্থায়ীভাবে কাজ করা বন্ধ করে দেবে।}other{সিম কার্ডের ভুল PUK কোড দিয়েছেন, আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে এই সিম কার্ডটি স্থায়ীভাবে কাজ করা বন্ধ করে দেবে।}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">সিমের PUK কোডটি ভুল, আপনি আর <xliff:g id="NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারেন, তারপর আপনার সিমটি স্থায়ীভাবে অব্যবহারযোগ্য হবে।</item>
+      <item quantity="other">সিমের PUK কোডটি ভুল, আপনি আর <xliff:g id="NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারেন, তারপর আপনার সিমটি স্থায়ীভাবে অব্যবহারযোগ্য হবে।</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"সিম পিন দিয়ে আনলক করা যায়নি!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"সিম PUK দিয়ে আনলক করা যায়নি!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ইনপুট পদ্ধতি পরিবর্তন করুন"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইসটি পুনরায় চালু হওয়ার পর প্যাটার্নের প্রয়োজন হবে"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইসটি পুনরায় চালু হওয়ার পর পিন প্রয়োজন হবে"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইসটি পুনরায় চালু হওয়ার পর পাসওয়ার্ডের প্রয়োজন হবে"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"অতিরিক্ত সুরক্ষার জন্য, এর বদলে প্যাটার্ন ব্যবহার করুন"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"অতিরিক্ত সুরক্ষার জন্য, এর বদলে পিন ব্যবহার করুন"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"অতিরিক্ত সুরক্ষার জন্য, এর বদলে পাসওয়ার্ড ব্যবহার করুন"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিরিক্ত সুরক্ষার জন্য প্যাটার্ন দেওয়া প্রয়োজন"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিরিক্ত সুরক্ষার জন্য পিন দেওয়া প্রয়োজন"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিরিক্ত সুরক্ষার জন্য পাসওয়ার্ড দেওয়া প্রয়োজন"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্রশাসক ডিভাইসটি লক করেছেন"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইসটিকে ম্যানুয়ালি লক করা হয়েছে"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"শনাক্ত করা যায়নি"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"\'ফেস আনলক\' ফিচার ব্যবহার করতে, \'সেটিংস\' থেকে ক্যামেরার অ্যাক্সেস চালু করুন"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{সিম কার্ডের পিন লিখুন। আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে ডিভাইস আনলক করতে পরিষেবা প্রদানকারীর সাথে যোগাযোগ করতে হবে।}one{সিম কার্ডের পিন লিখুন। আপনি আরও # বার চেষ্টা করতে পারবেন।}other{সিম কার্ডের পিন লিখুন। আপনি আরও # বার চেষ্টা করতে পারবেন।}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{সিম কার্ডটি এখন বন্ধ করা হয়েছে। চালিয়ে যেতে PUK কোড লিখুন। আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে এই সিম কার্ডটি স্থায়ীভাবে কাজ করা বন্ধ করে দেবে। বিশদ বিবরণের জন্য পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।}one{সিম কার্ডটি এখন বন্ধ করা হয়েছে। চালিয়ে যেতে PUK কোড লিখুন। আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে এই সিম কার্ডটি স্থায়ীভাবে কাজ করা বন্ধ করে দেবে। বিশদ বিবরণের জন্য পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।}other{সিম কার্ডটি এখন বন্ধ করা হয়েছে। চালিয়ে যেতে PUK কোড লিখুন। আপনি আরও # বার চেষ্টা করতে পারবেন, তারপরে এই সিম কার্ডটি স্থায়ীভাবে কাজ করা বন্ধ করে দেবে। বিশদ বিবরণের জন্য পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"\'ফেস আনলক\' ফিচার ব্যবহার করতে \'সেটিংস ও গোপনীয়তা\' বিকল্পে গিয়ে "<b>"ক্যামেরায় অ্যাক্সেস দিন"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">সিমের পিন লিখুন। আপনি আর <xliff:g id="NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারবেন।</item>
+      <item quantity="other">সিমের পিন লিখুন। আপনি আর <xliff:g id="NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারবেন।</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">সিম অক্ষম করা হয়েছে। চালিয়ে যেতে PUK কোড লিখুন। আপনি আর <xliff:g id="_NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারবেন, তারপরে এই সিমটি আর একেবারেই ব্যবহার করা যাবে না। বিশদে জানতে পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।</item>
+      <item quantity="other">সিম অক্ষম করা হয়েছে। চালিয়ে যেতে PUK কোড লিখুন। আপনি আর <xliff:g id="_NUMBER_1">%d</xliff:g> বার চেষ্টা করতে পারবেন, তারপরে এই সিমটি আর একেবারেই ব্যবহার করা যাবে না। বিশদে জানতে পরিষেবা প্রদানকারীর সাথে যোগাযোগ করুন।</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ডিফল্ট"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"অ্যানালগ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"চালিয়ে যেতে আপনার ডিভাইস আনলক করুন"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bs/strings.xml b/packages/SystemUI/res-keyguard/values-bs/strings.xml
index 3118d5b..4a47157 100644
--- a/packages/SystemUI/res-keyguard/values-bs/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bs/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Nevažeća kartica."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Napunjeno"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Bežično punjenje"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Priključna stanica za punjenje"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Brzo punjenje"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Sporo punjenje"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje je optimizirano radi zaštite baterije"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje je privremeno ograničeno"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pritisnite meni da otključate."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Mreža je zaključana"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nema SIM-a"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Dodajte SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM nedostaje ili se ne može čitati. Dodajte SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Neupotrebljiv SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM je trajno deaktiviran.\n Kontaktirajte pružaoca bežičnih usluga za drugi SIM"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM je zaključan."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM je zaključan PUK-om."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Otključavanje SIM-a…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nema SIM kartice"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Umetnite SIM karticu."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM kartica nije umetnuta ili je uređaj ne može očitati. Umetnite SIM karticu."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Neupotrebljiva SIM kartica."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Vaša SIM kartica je trajno onemogućena.\nDa dobijete drugu SIM karticu, obratite se pružaocu bežičnih usluga."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kartica je zaključana."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kartica je zaključana PUK kodom."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Otključavanje SIM kartice…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Prostor za PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Lozinka uređaja"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Prostor za PIN za SIM karticu"</string>
@@ -61,16 +61,24 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Operater SIM kartice \"<xliff:g id="CARRIER">%1$s</xliff:g>\" sada je onemogućen. Unesite PUK kôd da nastavite. Za više informacija obratite se operateru."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Unesite željeni PIN"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potvrdite željeni PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Otključavanje SIM-a…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Otključavanje SIM kartice…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Unesite PIN koji sadrži 4 do 8 brojeva."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kôd treba sadržavati najmanje 8 brojeva."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Pogrešno ste unijeli PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> puta. \n\nPokušajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Pogrešno ste unijeli lozinku <xliff:g id="NUMBER_0">%1$d</xliff:g> puta. \n\nPokušajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Pogrešno ste nacrtali svoj uzorak za otključavanje <xliff:g id="NUMBER_0">%1$d</xliff:g> puta. \n\nPokušajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"PIN za SIM karticu je netačan. Za otključavanje uređaja sada se morate obratiti svom operateru."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{PIN kôd za SIM je netačan. Imate još # pokušaj prije nego što budete morali kontaktirati mobilnog operatera da vam otključa uređaj.}one{PIN kôd za SIM je netačan. Imate još # pokušaj. }few{PIN kôd za SIM je netačan. Imate još # pokušaja. }other{PIN kôd za SIM je netačan. Imate još # pokušaja. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">PIN za SIM karticu je netačan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj.</item>
+      <item quantity="few">PIN za SIM karticu je netačan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+      <item quantity="other">PIN za SIM karticu je netačan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kartica je neupotrebljiva. Obratite se svom operateru."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{PUK kôd za SIM karticu je netačan. Imate još # pokušaj prije nego što SIM postane trajno neupotrebljiv.}one{PUK kôd za SIM karticu je netačan. Imate još # pokušaj prije nego što SIM postane trajno neupotrebljiv.}few{PUK kôd za SIM karticu je netačan. Imate još # pokušaja prije nego što SIM postane trajno neupotrebljiv.}other{PUK kôd za SIM karticu je netačan. Imate još # pokušaja prije nego što SIM postane trajno neupotrebljiv.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">PUK kôd za SIM karticu je netačan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj prije nego što SIM kartica postane trajno neupotrebljiva.</item>
+      <item quantity="few">PUK kôd za SIM karticu je netačan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja prije nego što SIM kartica postane trajno neupotrebljiva.</item>
+      <item quantity="other">PUK kôd za SIM karticu je netačan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja prije nego što SIM kartica postane trajno neupotrebljiva.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Korištenje PIN-a za SIM karticu nije uspjelo!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Korištenje PUK-a za SIM nije uspjelo!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Promjena načina unosa"</string>
@@ -78,17 +86,24 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Potreban je uzorak nakon što se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Potreban je PIN nakon što se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Potrebna je lozinka nakon što se uređaj ponovo pokrene"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Radi dodatne zaštite, umjesto toga koristite uzorak"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Radi dodatne zaštite, umjesto toga koristite PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Radi dodatne zašitite, umjesto toga koristite lozinku"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Uzorak je potreban radi dodatne sigurnosti"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN je potreban radi dodatne sigurnosti"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lozinka je potrebna radi dodatne sigurnosti"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Uređaj je zaključao administrator"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Za otključavanje licem uključite pristup kameri"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Unesite PIN za SIM. Imate još # pokušaj prije nego što budete morali kontaktirati mobilnog operatera da vam otključa uređaj.}one{Unesite PIN za SIM. Imate još # pokušaj.}few{Unesite PIN za SIM. Imate još # pokušaja.}other{Unesite PIN za SIM. Imate još # pokušaja.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM je sada onemogućen. Unesite PUK kôd da nastavite. Imate još # pokušaj prije nego što SIM postane trajno neupotrebljiv. Za više informacija kontaktirajte mobilnog operatera.}one{SIM je sada onemogućen. Unesite PUK kôd da nastavite. Imate još # pokušaja prije nego što SIM postane trajno neupotrebljiv. Za više informacija kontaktirajte mobilnog operatera.}few{SIM je sada onemogućen. Unesite PUK kôd da nastavite. Imate još # pokušaja prije nego što SIM postane trajno neupotrebljiv. Za više informacija kontaktirajte mobilnog operatera.}other{SIM je sada onemogućen. Unesite PUK kôd da nastavite. Imate još # pokušaja prije nego što SIM postane trajno neupotrebljiv. Za više informacija kontaktirajte mobilnog operatera.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Da koristite otključavanje licem, uključite "<b>"Pristup kameri"</b>" u meniju Postavke &gt; Privatnost"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj.</item>
+      <item quantity="few">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+      <item quantity="other">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM kartica je onemogućena. Unesite PUK kôd da nastavite. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaj prije nego što SIM kartica postane trajno neupotrebljiva. Za više informacija kontaktirajte mobilnog operatera.</item>
+      <item quantity="few">SIM kartica je onemogućena. Unesite PUK kôd da nastavite. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaja prije nego što SIM kartica postane trajno neupotrebljiva. Za više informacija kontaktirajte mobilnog operatera.</item>
+      <item quantity="other">SIM kartica je onemogućena. Unesite PUK kôd da nastavite. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaja prije nego što SIM kartica postane trajno neupotrebljiva. Za više informacija kontaktirajte mobilnog operatera.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurići"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Otključajte uređaj da nastavite"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ca/strings.xml b/packages/SystemUI/res-keyguard/values-ca/strings.xml
index e45df7ab..9cc855a 100644
--- a/packages/SystemUI/res-keyguard/values-ca/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ca/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"La targeta no és vàlida."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Bateria carregada"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • S\'està carregant sense fil"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • S\'està carregant"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Base de càrrega"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • S\'està carregant"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • S\'està carregant ràpidament"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • S\'està carregant lentament"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Càrrega optimitzada per protegir la bateria"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Càrrega limitada temporalment"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Prem Menú per desbloquejar."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"La xarxa està bloquejada"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No hi ha cap SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Afegeix una SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Falta la SIM o no es pot llegir. Afegeix una SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"La SIM no es pot utilitzar."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"La SIM s\'ha desactivat permanentment.\n Contacta amb el proveïdor de serveis sense fil per obtenir-ne una altra."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"La SIM està bloquejada."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"La SIM està bloquejada pel PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"S\'està desbloquejant la targeta SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"No hi ha cap SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insereix una targeta SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Falta la targeta SIM o no es pot llegir. Insereix-ne una."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"La targeta SIM no es pot fer servir."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"La targeta SIM s\'ha desactivat permanentment.\nContacta amb el teu proveïdor de serveis sense fil per obtenir-ne una altra."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"La targeta SIM està bloquejada."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"La targeta SIM està bloquejada pel PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"S\'està desbloquejant la targeta SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Zona del PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Contrasenya del dispositiu"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Zona del PIN de la SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Patró incorrecte"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Contrasenya incorrecta"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"El PIN no és correcte"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Torna-ho a provar d\'aquí a # segon.}many{Torna-ho a provar d\'aquí a # segons.}other{Torna-ho a provar d\'aquí a # segons.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Torna-ho a provar d\'aquí a # segon.}other{Torna-ho a provar d\'aquí a # segons.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Introdueix el PIN de la SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Introdueix el PIN de la SIM de: <xliff:g id="CARRIER">%1$s</xliff:g>."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Desactiva l\'eSIM per utilitzar el dispositiu sense servei mòbil."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"La SIM <xliff:g id="CARRIER">%1$s</xliff:g> està desactivada. Introdueix el codi PUK per continuar. Contacta amb l\'operador de telefonia mòbil per obtenir més informació."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Introdueix el codi PIN que vulguis"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirma el codi PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"S\'està desbloquejant la targeta SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"S\'està desbloquejant la targeta SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Escriu un PIN que tingui entre 4 i 8 números."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"El codi PUK ha de tenir 8 números o més."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Has escrit el PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> vegades de manera incorrecta. \n\nTorna-ho a provar d\'aquí a <xliff:g id="NUMBER_1">%2$d</xliff:g> segons."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Has escrit la contrasenya <xliff:g id="NUMBER_0">%1$d</xliff:g> vegades de manera incorrecta. \n\nTorna-ho a provar d\'aquí a <xliff:g id="NUMBER_1">%2$d</xliff:g> segons."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Has dibuixat el patró de desbloqueig <xliff:g id="NUMBER_0">%1$d</xliff:g> vegades de manera incorrecta. \n\nTorna-ho a provar d\'aquí a <xliff:g id="NUMBER_1">%2$d</xliff:g> segons."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"El codi PIN de la SIM no és correcte. Contacta amb l\'operador de telefonia mòbil per desbloquejar el dispositiu."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{El codi PIN de la SIM no és correcte. Et queda # intent; si no l\'encertes, contacta amb l\'operador per desbloquejar el dispositiu.}many{El codi PIN de la SIM no és correcte. Et queden # intents. }other{El codi PIN de la SIM no és correcte. Et queden # intents. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">El codi PIN de la SIM no és correcte. Et queden <xliff:g id="NUMBER_1">%d</xliff:g> intents.</item>
+      <item quantity="one">El codi PIN de la SIM no és correcte. Et queda <xliff:g id="NUMBER_0">%d</xliff:g> intent; si no l\'encertes, contacta amb l\'operador de telefonia mòbil per desbloquejar el dispositiu.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"La SIM no es pot fer servir. Contacta amb l\'operador de telefonia mòbil."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{El codi PUK de la SIM no és correcte. Et queda # intent; si no l\'encertes, la SIM no es podrà tornar a fer servir.}many{El codi PUK de la SIM no és correcte. Et queden # intents; si no l\'encertes, la SIM no es podrà tornar a fer servir.}other{El codi PUK de la SIM no és correcte. Et queden # intents; si no l\'encertes, la SIM no es podrà tornar a fer servir.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">El codi PUK de la SIM no és correcte. Et queden <xliff:g id="NUMBER_1">%d</xliff:g> intents; si els esgotes tots, la SIM no es podrà tornar a fer servir.</item>
+      <item quantity="one">El codi PUK de la SIM no és correcte. Et queda <xliff:g id="NUMBER_0">%d</xliff:g> intent; si no l\'encertes, la SIM no es podrà tornar a fer servir.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Ha fallat l\'operació del PIN de la SIM"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"No s\'ha pogut desbloquejar la SIM amb el codi PUK."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Canvia el mètode d\'introducció"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cal introduir el patró quan es reinicia el dispositiu"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cal introduir el PIN quan es reinicia el dispositiu"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cal introduir la contrasenya quan es reinicia el dispositiu"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Per a més seguretat, utilitza el patró"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Per a més seguretat, utilitza el PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Per a més seguretat, utilitza la contrasenya"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Cal introduir el patró per disposar de més seguretat"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Cal introduir el PIN per disposar de més seguretat"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Cal introduir la contrasenya per disposar de més seguretat"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'administrador ha bloquejat el dispositiu"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositiu s\'ha bloquejat manualment"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No s\'ha reconegut"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Desbloqueig facial necessita accés a la càmera"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Introdueix el PIN de la SIM. Et queda # intent; si no l\'encertes, contacta amb l\'operador per desbloquejar el dispositiu.}many{Introdueix el PIN de la SIM. Et queden # intents.}other{Introdueix el PIN de la SIM. Et queden # intents.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queda # intent; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador per obtenir informació.}many{La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queden # intents; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador per obtenir informació.}other{La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queden # intents; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador per obtenir informació.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Per utilitzar Desbloqueig facial, activa "<b>"Accés a la càmera"</b>" a Configuració &gt; Privadesa"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Introdueix el PIN de la SIM. Et queden <xliff:g id="NUMBER_1">%d</xliff:g> intents.</item>
+      <item quantity="one">Introdueix el PIN de la SIM. Et queda <xliff:g id="NUMBER_0">%d</xliff:g> intent; si no l\'encertes, contacta amb l\'operador de telefonia mòbil per desbloquejar el dispositiu.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queden <xliff:g id="_NUMBER_1">%d</xliff:g> intents; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador de telefonia mòbil per obtenir-ne més informació.</item>
+      <item quantity="one">La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queda <xliff:g id="_NUMBER_0">%d</xliff:g> intent; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador de telefonia mòbil per obtenir-ne més informació.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminada"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bombolla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analògica"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueja el dispositiu per continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-cs/strings.xml b/packages/SystemUI/res-keyguard/values-cs/strings.xml
index ae1ebcb..606bff5 100644
--- a/packages/SystemUI/res-keyguard/values-cs/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-cs/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Neplatná karta."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Nabito"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Bezdrátové nabíjení"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjení"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjecí dok"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjení"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Rychlé nabíjení"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Pomalé nabíjení"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Optimalizované nabíjení za účelem ochrany baterie"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjení je dočasně omezeno"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Klávesy odemknete stisknutím tlačítka nabídky."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Síť je blokována"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Žádná SIM karta"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Přidejte SIM kartu."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM karta chybí nebo je nečitelná. Přidejte SIM kartu."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM kartu nelze použít."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM karta byla natrvalo deaktivována.\n Požádejte svého poskytovatele bezdrátových služeb o další SIM kartu."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM karta je zablokována."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM karta je blokována pomocí kódu PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Odblokování SIM karty…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Chybí SIM karta"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Vložte SIM kartu."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM karta chybí nebo je nečitelná. Vložte SIM kartu."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Nepoužitelná SIM karta."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Vaše SIM karta byla natrvalo zablokována.\n Požádejte svého poskytovatele bezdrátových služeb o další SIM kartu."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM karta je zablokována."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM karta je zablokována pomocí kódu PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Odblokování SIM karty…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Oblast kódu PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Heslo zařízení"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Oblast kódu PIN SIM karty"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM karta <xliff:g id="CARRIER">%1$s</xliff:g> je teď zablokována. Chcete-li pokračovat, zadejte kód PUK. Podrobnosti vám poskytne operátor."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Zadejte požadovaný kód PIN"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potvrďte požadovaný kód PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Odblokování SIM karty…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Odblokování SIM karty…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Zadejte kód PIN o délce 4–8 číslic."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Minimální délka kódu PUK je 8 číslic."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Již <xliff:g id="NUMBER_0">%1$d</xliff:g>krát jste zadali nesprávný kód PIN. \n\nZkuste to znovu za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Již <xliff:g id="NUMBER_0">%1$d</xliff:g>krát jste nesprávně zadali heslo. \n\nZkuste to znovu za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Již <xliff:g id="NUMBER_0">%1$d</xliff:g>krát jste zadali nesprávné bezpečnostní gesto. \n\nZkuste to znovu za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Zadali jste nesprávný kód PIN SIM karty. Nyní musíte za účelem odemknutí zařízení kontaktovat svého operátora."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Zadali jste nesprávný PIN SIM karty. Zbývá vám # pokus, poté zařízení bude muset odemknout operátor.}few{Zadali jste nesprávný PIN SIM karty. Máte ještě # pokusy. }many{Zadali jste nesprávný PIN SIM karty. Máte ještě # pokusu. }other{Zadali jste nesprávný PIN SIM karty. Máte ještě # pokusů. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="few">Zadali jste nesprávný kód PIN SIM karty. Máte ještě <xliff:g id="NUMBER_1">%d</xliff:g> pokusy.</item>
+      <item quantity="many">Zadali jste nesprávný kód PIN SIM karty. Máte ještě <xliff:g id="NUMBER_1">%d</xliff:g> pokusu.</item>
+      <item quantity="other">Zadali jste nesprávný kód PIN SIM karty. Máte ještě <xliff:g id="NUMBER_1">%d</xliff:g> pokusů.</item>
+      <item quantity="one">Zadali jste nesprávný PIN SIM karty. Zbývá <xliff:g id="NUMBER_0">%d</xliff:g> pokus, poté bude muset zařízení odemknout operátor.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kartu nelze použít. Kontaktujte operátora."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Nesprávný PUK SIM karty. Máte ještě # pokus, poté bude SIM karta natrvalo zablokována.}few{Nesprávný PUK SIM karty. Máte ještě # pokusy, poté bude SIM karta natrvalo zablokována.}many{Nesprávný PUK SIM karty. Máte ještě # pokusu, poté bude SIM karta natrvalo zablokována.}other{Nesprávný PUK SIM karty. Máte ještě # pokusů, poté bude SIM karta natrvalo zablokována.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="few">Nesprávný kód PUK SIM karty. Máte ještě <xliff:g id="NUMBER_1">%d</xliff:g> pokusy, poté bude SIM karta natrvalo zablokována.</item>
+      <item quantity="many">Nesprávný kód PUK SIM karty. Máte ještě <xliff:g id="NUMBER_1">%d</xliff:g> pokusu, poté bude SIM karta natrvalo zablokována.</item>
+      <item quantity="other">Nesprávný kód PUK SIM karty. Máte ještě <xliff:g id="NUMBER_1">%d</xliff:g> pokusů, poté bude SIM karta natrvalo zablokována.</item>
+      <item quantity="one">Nesprávný kód PUK SIM karty. Máte ještě <xliff:g id="NUMBER_0">%d</xliff:g> pokus, poté bude SIM karta natrvalo zablokována.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operace pomocí kódu PIN SIM karty se nezdařila."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operace pomocí kódu PUK SIM karty se nezdařila."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Přepnout metodu zadávání"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po restartování zařízení je vyžadováno gesto"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po restartování zařízení je vyžadován kód PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po restartování zařízení je vyžadováno heslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Z bezpečnostních důvodů raději použijte gesto"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Z bezpečnostních důvodů raději použijte PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Z bezpečnostních důvodů raději použijte heslo"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pro ještě lepší zabezpečení je vyžadováno gesto"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pro ještě lepší zabezpečení je vyžadován kód PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pro ještě lepší zabezpečení je vyžadováno heslo"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zařízení je uzamknuto administrátorem"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zařízení bylo ručně uzamčeno"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznáno"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"V Nastavení zapněte přístup k fotoaparátu"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Zadejte PIN SIM karty. Zbývá # pokus, poté bude muset zařízení odemknout operátor.}few{Zadejte PIN SIM karty. Zbývají # pokusy.}many{Zadejte PIN SIM karty. Zbývá # pokusu.}other{Zadejte PIN SIM karty. Zbývá # pokusů.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM karta je nyní zablokována. Chcete-li pokračovat, zadejte PUK. Máte ještě # pokus, poté bude SIM karta natrvalo zablokována. Podrobnosti poskytne operátor.}few{SIM karta je nyní zablokována. Chcete-li pokračovat, zadejte PUK. Máte ještě # pokusy, poté bude SIM karta natrvalo zablokována. Podrobnosti poskytne operátor.}many{SIM karta je nyní zablokována. Chcete-li pokračovat, zadejte PUK. Máte ještě # pokusu, poté bude SIM karta natrvalo zablokována. Podrobnosti poskytne operátor.}other{SIM karta je nyní zablokována. Chcete-li pokračovat, zadejte PUK. Máte ještě # pokusů, poté bude SIM karta natrvalo zablokována. Podrobnosti poskytne operátor.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Pokud chcete používat odemknutí obličejem, v Nastavení &gt; Soukromí zapnetě "<b>"přístup k fotoaparátu"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="few">Zadejte PIN SIM karty. Zbývají <xliff:g id="NUMBER_1">%d</xliff:g> pokusy.</item>
+      <item quantity="many">Zadejte PIN SIM karty. Zbývá <xliff:g id="NUMBER_1">%d</xliff:g> pokusu.</item>
+      <item quantity="other">Zadejte PIN SIM karty. Zbývá <xliff:g id="NUMBER_1">%d</xliff:g> pokusů.</item>
+      <item quantity="one">Zadejte PIN SIM karty. Zbývá <xliff:g id="NUMBER_0">%d</xliff:g> pokus, poté bude muset zařízení odemknout operátor.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="few">SIM karta je teď zablokována. Chcete-li pokračovat, zadejte kód PUK. Máte ještě <xliff:g id="_NUMBER_1">%d</xliff:g> pokusy, poté bude SIM karta natrvalo zablokována. Podrobnosti vám poskytne operátor.</item>
+      <item quantity="many">SIM karta je teď zablokována. Chcete-li pokračovat, zadejte kód PUK. Máte ještě <xliff:g id="_NUMBER_1">%d</xliff:g> pokusu, poté bude SIM karta natrvalo zablokována. Podrobnosti vám poskytne operátor.</item>
+      <item quantity="other">SIM karta je teď zablokována. Chcete-li pokračovat, zadejte kód PUK. Máte ještě <xliff:g id="_NUMBER_1">%d</xliff:g> pokusů, poté bude SIM karta natrvalo zablokována. Podrobnosti vám poskytne operátor.</item>
+      <item quantity="one">SIM karta je teď zablokována. Chcete-li pokračovat, zadejte kód PUK. Máte ještě <xliff:g id="_NUMBER_0">%d</xliff:g> pokus, poté bude SIM karta natrvalo zablokována. Podrobnosti vám poskytne operátor.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Výchozí"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogové"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Pokud chcete pokračovat, odemkněte zařízení"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-da/strings.xml b/packages/SystemUI/res-keyguard/values-da/strings.xml
index 735aacf..8f7b8d0 100644
--- a/packages/SystemUI/res-keyguard/values-da/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-da/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ugyldigt kort."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Opladet"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Trådløs opladning"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oplader"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oplader i dockingstation"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oplader"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oplader hurtigt"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oplader langsomt"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Opladning er optimeret for at beskytte batteriet"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Opladningen er midlertidigt begrænset"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Tryk på menuen for at låse op."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Netværket er låst"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Intet SIM-kort"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Tilføj et SIM-kort."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-kortet mangler eller kan ikke læses. Tilføj et SIM-kort."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Deaktiveret SIM-kort."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Dit SIM-kort er permanent deaktiveret.\n Kontakt din tjenesteudbyder for at få et nyt SIM-kort."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-kortet er låst."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-kortet er låst med PUK-koden."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-kortet låses op…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Intet SIM-kort"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Indsæt et SIM-kort."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-kortet mangler eller kan ikke læses. Indsæt et SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Ubrugeligt SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Dit SIM-kort er blevet permanent deaktiveret.\nKontakt din tjenesteudbyder for at få et nyt SIM-kort."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kortet er låst."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kortet er låst med PUK-kode."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Låser SIM-kortet op…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Område for pinkoden"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Adgangskode til enhed"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Område for pinkoden til SIM-kortet"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-kortet fra \"<xliff:g id="CARRIER">%1$s</xliff:g>\" er nu deaktiveret. Angiv PUK-koden for at fortsætte. Kontakt mobilselskabet for at få flere oplysninger."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Angiv den ønskede pinkode"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Bekræft den ønskede pinkode"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-kortet låses op…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Låser SIM-kortet op…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Angiv en pinkode på mellem 4 og 8 tal."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-koden skal være på 8 tal eller mere."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Du har indtastet en forkert pinkode <xliff:g id="NUMBER_0">%1$d</xliff:g> gange. \n\nPrøv igen om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Du har indtastet din adgangskode forkert <xliff:g id="NUMBER_0">%1$d</xliff:g> gange. \n\nPrøv igen om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Du har tegnet dit oplåsningsmønster forkert <xliff:g id="NUMBER_0">%1$d</xliff:g> gange. \n\nPrøv igen om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Forkert pinkode til SIM-kort. Du er nu nødt til at kontakte dit mobilselskab for at låse din enhed op."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Du har angivet en forkert pinkode til SIM-kortet. Du har # forsøg tilbage, før du skal kontakte dit mobilselskab for at få låst din enhed op.}one{Du har angivet en forkert pinkode til SIM-kortet. Du har # forsøg tilbage. }other{Du har angivet en forkert pinkode til SIM-kortet. Du har # forsøg tilbage. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Forkert pinkode til SIM-kort. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøg tilbage.</item>
+      <item quantity="other">Forkert pinkode til SIM-kort. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøg tilbage.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-kortet er ubrugeligt. Kontakt dit mobilselskab."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Du har angivet en forkert PUK-kode til SIM-kortet. Du har # forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt.}one{Du har angivet en forkert PUK-kode til SIM-kortet. Du har # forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt.}other{Du har angivet en forkert PUK-kode til SIM-kortet. Du har # forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Forkert PUK-kode til SIM-kort. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt.</item>
+      <item quantity="other">Forkert PUK-kode til SIM-kort. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Pinkoden til SIM-kortet blev afvist"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"PUK-koden til SIM-kortet blev afvist"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Skift indtastningsmetode"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du skal angive et mønster, når du har genstartet enheden"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Der skal angives en pinkode efter genstart af enheden"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Der skal angives en adgangskode efter genstart af enheden"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Øg sikkerheden ved at bruge dit oplåsningsmønter i stedet"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Øg sikkerheden ved at bruge din pinkode i stedet"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Øg sikkerheden ved at bruge din adgangskode i stedet"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Der kræves et mønster som ekstra beskyttelse"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Der kræves en pinkode som ekstra beskyttelse"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Der kræves en adgangskode som ekstra beskyttelse"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheden er blevet låst af administratoren"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheden blev låst manuelt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke genkendt"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Aktivér kameraadgang i Indstillinger for at bruge ansigtslås"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Angiv pinkoden til SIM-kortet. Du har # forsøg tilbage, før du skal kontakte dit mobilselskab for at låse din enhed op.}one{Angiv pinkoden til SIM-kortet. Du har # forsøg tilbage.}other{Angiv pinkoden til SIM-kortet. Du har # forsøg tilbage.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-kortet er nu deaktiveret. Angiv PUK-koden for at fortsætte. Du har # forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt. Kontakt dit mobilselskab for at få flere oplysninger.}one{SIM-kortet er nu deaktiveret. Angiv PUK-koden for at fortsætte. Du har # forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt. Kontakt dit mobilselskab for at få flere oplysninger.}other{SIM-kortet er nu deaktiveret. Angiv PUK-koden for at fortsætte. Du har # forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt. Kontakt dit mobilselskab for at få flere oplysninger.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Hvis du vil bruge ansigtslåsen, skal du aktivere "<b>"Kameraadgang"</b>" under Indstillinger &gt; Privatliv"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Angiv pinkoden til SIM-kortet. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøg tilbage.</item>
+      <item quantity="other">Angiv pinkoden til SIM-kortet. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøg tilbage.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM-kortet er nu deaktiveret. Angiv PUK-koden for at fortsætte. Du har <xliff:g id="_NUMBER_1">%d</xliff:g> forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt. Kontakt dit mobilselskab for at få flere oplysninger.</item>
+      <item quantity="other">SIM-kortet er nu deaktiveret. Angiv PUK-koden for at fortsætte. Du har <xliff:g id="_NUMBER_1">%d</xliff:g> forsøg tilbage, før SIM-kortet bliver permanent ubrugeligt. Kontakt dit mobilselskab for at få flere oplysninger.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lås din enhed op for at fortsætte"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-de/strings.xml b/packages/SystemUI/res-keyguard/values-de/strings.xml
index 5e7a5c7..6391ed0 100644
--- a/packages/SystemUI/res-keyguard/values-de/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-de/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ungültige Karte."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Aufgeladen"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kabelloses Laden"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Wird geladen"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladestation"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Wird geladen"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Wird schnell geladen"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Wird langsam geladen"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Optimiertes Laden zur Akkuschonung"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laden vorübergehend eingeschränkt"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Zum Entsperren die Menütaste drücken."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Netzwerk gesperrt"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Keine SIM-Karte"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Lege eine SIM-Karte ein."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-Karte fehlt oder ist nicht lesbar. Lege eine SIM-Karte ein."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-Karte ist nicht nutzbar."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Deine SIM-Karte wurde dauerhaft deaktiviert.\n Wende dich an deinen Mobilfunkanbieter, um eine andere SIM-Karte zu erhalten."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-Karte ist gesperrt."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-Karte ist mit einem PUK gesperrt."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-Karte wird entsperrt…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Keine SIM-Karte"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM-Karte einlegen."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-Karte fehlt oder ist nicht lesbar. Bitte lege eine SIM-Karte ein."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM-Karte unbrauchbar."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Deine SIM-Karte wurde dauerhaft deaktiviert.\n Wende dich an deinen Mobilfunkanbieter, um eine andere SIM-Karte zu erhalten."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-Karte ist gesperrt."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"PUK-Sperre auf SIM-Karte."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM-Karte wird entsperrt..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-Bereich"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Gerätepasswort"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM-PIN-Bereich"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Die SIM-Karte \"<xliff:g id="CARRIER">%1$s</xliff:g>\" ist jetzt deaktiviert. Gib den PUK-Code ein, um fortzufahren. Weitere Informationen erhältst du von deinem Mobilfunkanbieter."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Gewünschten PIN-Code eingeben"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Gewünschten PIN-Code bestätigen"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-Karte wird entsperrt…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM-Karte wird entsperrt..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Gib eine 4- bis 8-stellige PIN ein."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Der PUK-Code muss mindestens 8 Ziffern aufweisen."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Du hast deine PIN <xliff:g id="NUMBER_0">%1$d</xliff:g>-mal falsch eingegeben.\n\nBitte versuche es in <xliff:g id="NUMBER_1">%2$d</xliff:g> Sekunden noch einmal."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Du hast dein Passwort <xliff:g id="NUMBER_0">%1$d</xliff:g>-mal falsch eingegeben.\n\nBitte versuche es in <xliff:g id="NUMBER_1">%2$d</xliff:g> Sekunden noch einmal."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Du hast dein Entsperrungsmuster <xliff:g id="NUMBER_0">%1$d</xliff:g>-mal falsch gezeichnet. \n\nBitte versuche es in <xliff:g id="NUMBER_1">%2$d</xliff:g> Sekunden noch einmal."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Falscher PIN-Code der SIM-Karte. Bitte wende dich an deinen Mobilfunkanbieter, damit er dein Gerät entsperrt."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Falscher PIN-Code für die SIM-Karte. Du hast noch # Versuch, bevor das Gerät nur noch vom Mobilfunkanbieter entsperrt werden kann.}other{Falscher PIN-Code für die SIM-Karte. Du hast noch # Versuche. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Falscher PIN-Code der SIM-Karte. Du hast noch <xliff:g id="NUMBER_1">%d</xliff:g> Versuche.</item>
+      <item quantity="one">Falscher PIN-Code der SIM-Karte. Du hast noch <xliff:g id="NUMBER_0">%d</xliff:g> Versuch, bevor du das Gerät von deinem Mobilfunkanbieter entsperren lassen musst.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Die SIM-Karte kann nicht verwendet werden. Bitte wende dich an deinen Mobilfunkanbieter."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Falscher PUK-Code für die SIM-Karte. Du hast noch # Versuch, bevor die SIM-Karte endgültig gesperrt wird.}other{Falscher PUK-Code für die SIM-Karte. Du hast noch # Versuche, bevor die SIM-Karte endgültig gesperrt wird.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Falscher PUK-Code der SIM-Karte. Du hast noch <xliff:g id="NUMBER_1">%d</xliff:g> Versuche, bevor deine SIM-Karte endgültig gesperrt wird.</item>
+      <item quantity="one">Falscher PUK-Code der SIM-Karte. Du hast noch <xliff:g id="NUMBER_0">%d</xliff:g> Versuch, bevor deine SIM-Karte endgültig gesperrt wird.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Fehler beim Entsperren der SIM-Karte mit der PIN."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Fehler beim Entsperren der SIM-Karte mithilfe des PUK-Codes."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Eingabemethode wechseln"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nach dem Neustart des Geräts ist die Eingabe des Musters erforderlich"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nach dem Neustart des Geräts ist die Eingabe der PIN erforderlich"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nach dem Neustart des Geräts ist die Eingabe des Passworts erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Verwende für mehr Sicherheit stattdessen dein Muster"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Verwende für mehr Sicherheit stattdessen deine PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Verwende für mehr Sicherheit stattdessen dein Passwort"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zur Verbesserung der Sicherheit ist ein Muster erforderlich"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zur Verbesserung der Sicherheit ist eine PIN erforderlich"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zur Verbesserung der Sicherheit ist ein Passwort erforderlich"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Gerät vom Administrator gesperrt"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Gerät manuell gesperrt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nicht erkannt"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Die Gesichtsentsperrung benötigt Kamerazugriff"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Gib die PIN für die SIM-Karte ein. Du hast noch # Versuch, bevor das Gerät nur noch vom Mobilfunkanbieter entsperrt werden kann.}other{Gib die PIN für die SIM-Karte ein. Du hast noch # Versuche.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Die SIM-Karte ist jetzt deaktiviert. Gib den PUK-Code ein, um fortzufahren. Du hast noch # Versuch, bevor die SIM-Karte endgültig gesperrt wird. Wende dich für weitere Informationen an deinen Mobilfunkanbieter.}other{Die SIM-Karte ist jetzt deaktiviert. Gib den PUK-Code ein, um fortzufahren. Du hast noch # Versuche, bevor die SIM-Karte endgültig gesperrt wird. Wende dich für weitere Informationen an deinen Mobilfunkanbieter.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Wenn du die Entsperrung per Gesichtserkennung verwenden möchtest, aktiviere in den Einstellungen unter „Datenschutz“ die Option "<b>"Kamerazugriff"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Gib die PIN für die SIM-Karte ein. Du hast noch <xliff:g id="NUMBER_1">%d</xliff:g> Versuche.</item>
+      <item quantity="one">Gib die PIN für die SIM-Karte ein. Du hast noch <xliff:g id="NUMBER_0">%d</xliff:g> Versuch, bevor das Gerät nur noch vom Mobilfunkanbieter entsperrt werden kann.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Die SIM-Karte ist jetzt deaktiviert. Gib den PUK-Code ein, um fortzufahren. Du hast noch <xliff:g id="_NUMBER_1">%d</xliff:g> Versuche, bevor die SIM-Karte endgültig gesperrt wird. Weitere Informationen erhältst du von deinem Mobilfunkanbieter.</item>
+      <item quantity="one">Die SIM-Karte ist jetzt deaktiviert. Gib den PUK-Code ein, um fortzufahren. Du hast noch <xliff:g id="_NUMBER_0">%d</xliff:g> Versuch, bevor die SIM-Karte endgültig gesperrt wird. Weitere Informationen erhältst du von deinem Mobilfunkanbieter.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Gerät entsperren, um fortzufahren"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-el/strings.xml b/packages/SystemUI/res-keyguard/values-el/strings.xml
index cd0b01e..a5bbb9a 100644
--- a/packages/SystemUI/res-keyguard/values-el/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-el/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Μη έγκυρη κάρτα."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Φορτίστηκε"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ασύρματη φόρτιση"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Φόρτιση"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Βάση φόρτισης"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Φόρτιση"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Γρήγορη φόρτιση"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Αργή φόρτιση"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Η φόρτιση βελτιστοποιήθηκε για την προστασία της μπαταρίας"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Προσωρινός περιορισμός φόρτισης"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Πατήστε \"Μενού\" για ξεκλείδωμα."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Κλειδωμένο δίκτυο"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Δεν υπάρχει SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Προσθέστε μια SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Η SIM λείπει ή δεν είναι δυνατή η ανάγνωσή της. Προσθέστε μια SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Η SIM δεν μπορεί να χρησιμοποιηθεί."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Η SIM απενεργοποιήθηκε οριστικά.\n Επικοινωνήστε με τον πάροχο υπηρεσιών ασύρματου δικτύου για μια νέα SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Η SIM είναι κλειδωμένη."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Η SIM έχει κλειδωθεί με κωδικό PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Ξεκλείδωμα SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Δεν υπάρχει SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Τοποθετήστε μια κάρτα SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Η κάρτα SIM δεν υπάρχει ή δεν είναι δυνατή η ανάγνωσή της. Τοποθετήστε μια κάρτα SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Η κάρτα SIM δεν μπορεί να χρησιμοποιηθεί."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Η κάρτα SIM έχει απενεργοποιηθεί οριστικά.\n Επικοινωνήστε με τον πάροχο υπηρεσιών ασύρματου δικτύου για να λάβετε μια νέα κάρτα SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Η κάρτα SIM είναι κλειδωμένη."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Η κάρτα SIM είναι κλειδωμένη με κωδικό PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Ξεκλείδωμα κάρτας SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Περιοχή αριθμού PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Κωδικός πρόσβασης συσκευής"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Περιοχή αριθμού PIN κάρτας SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Η SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" έχει απενεργοποιηθεί. Εισάγετε τον κωδικό PUK για συνέχεια. Επικοινωνήστε με την εταιρεία κινητής τηλεφωνίας για λεπτομέρειες."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Εισαγάγετε τον απαιτούμενο κωδικό PIN"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Επιβεβαιώστε τον απαιτούμενο κωδικό PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Ξεκλείδωμα SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Ξεκλείδωμα κάρτας SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Πληκτρολογήστε έναν αριθμό PIN που να αποτελείται από 4 έως 8 αριθμούς."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Ο κωδικός PUK θα πρέπει να περιέχει τουλάχιστον 8 αριθμούς."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Έχετε πληκτρολογήσει τον αριθμό PIN εσφαλμένα <xliff:g id="NUMBER_0">%1$d</xliff:g> φορές. \n\nΠροσπαθήστε ξανά σε <xliff:g id="NUMBER_1">%2$d</xliff:g> δευτερόλεπτα."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Έχετε πληκτρολογήσει τον κωδικό πρόσβασης εσφαλμένα <xliff:g id="NUMBER_0">%1$d</xliff:g> φορές. \n\nΠροσπαθήστε ξανά σε <xliff:g id="NUMBER_1">%2$d</xliff:g> δευτερόλεπτα."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Σχεδιάσατε εσφαλμένα το μοτίβο ξεκλειδώματος<xliff:g id="NUMBER_0">%1$d</xliff:g> φορές. \n\nΠροσπαθήστε ξανά σε <xliff:g id="NUMBER_1">%2$d</xliff:g> δευτερόλεπτα."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Λανθασμένος κωδικός PIN κάρτας SIM. Θα πρέπει να επικοινωνήσετε με την εταιρεία κινητής τηλεφωνίας σας για να ξεκλειδώσετε τη συσκευή σας."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Εσφαλμένος κωδικός PIN κάρτας SIM. Απομένει άλλη # προσπάθεια. Στη συνέχεια, θα πρέπει να επικοινωνήσετε με τον πάροχο κινητής τηλεφωνίας σας για να ξεκλειδώσετε τη συσκευή σας.}other{Εσφαλμένος κωδικός PIN κάρτας SIM. Απομένουν # προσπάθειες. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Λανθασμένος κωδικός PIN κάρτας SIM. Απομένουν άλλες <xliff:g id="NUMBER_1">%d</xliff:g> προσπάθειες.</item>
+      <item quantity="one">Λανθασμένος κωδικός PIN κάρτας SIM. Απομένει άλλη <xliff:g id="NUMBER_0">%d</xliff:g> προσπάθεια. Στη συνέχεια, θα πρέπει να επικοινωνήσετε με την εταιρεία κινητής τηλεφωνίας σας για να ξεκλειδώσετε τη συσκευή σας.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Η κάρτα SIM δεν μπορεί να χρησιμοποιηθεί. Επικοινωνήστε με την εταιρεία κινητής τηλεφωνίας σας."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Λανθασμένος κωδικός PUK κάρτας SIM. Απομένει άλλη # προσπάθεια προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM.}other{Εσφαλμένος κωδικός PUK κάρτας SIM. Απομένουν # προσπάθειες προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Λανθασμένος κωδικός PUK κάρτας SIM. Απομένουν άλλες <xliff:g id="NUMBER_1">%d</xliff:g> προσπάθειες προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM.</item>
+      <item quantity="one">Λανθασμένος κωδικός PUK κάρτας SIM. Απομένει άλλη <xliff:g id="NUMBER_0">%d</xliff:g> προσπάθεια προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Αποτυχία λειτουργίας κωδικού PIN κάρτας SIM!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Αποτυχία λειτουργίας κωδικού PUK κάρτας SIM!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Εναλλαγή μεθόδου εισαγωγής"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Απαιτείται μοτίβο μετά από την επανεκκίνηση της συσκευής"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Απαιτείται PIN μετά από την επανεκκίνηση της συσκευής"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Απαιτείται κωδικός πρόσβασης μετά από την επανεκκίνηση της συσκευής"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Για πρόσθετη ασφάλεια, χρησιμοποιήστε εναλλακτικά μοτίβο"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Για πρόσθετη ασφάλεια, χρησιμοποιήστε εναλλακτικά PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Για πρόσθετη ασφάλεια, χρησιμοποιήστε εναλλακτικά κωδικό πρόσβασης"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Απαιτείται μοτίβο για πρόσθετη ασφάλεια"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Απαιτείται PIN για πρόσθετη ασφάλεια"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Απαιτείται κωδικός πρόσβασης για πρόσθετη ασφάλεια"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Η συσκευή κλειδώθηκε από τον διαχειριστή"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Η συσκευή κλειδώθηκε με μη αυτόματο τρόπο"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Δεν αναγνωρίστηκε"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Ενεργοπ. πρόσβ. κάμ. στις Ρυθμ. για Ξεκλείδ. με το πρόσωπο."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Εισαγάγετε τον κωδικό PIN της κάρτας SIM. Απομένει άλλη # προσπάθεια. Στη συνέχεια, θα πρέπει να επικοινωνήσετε με τον πάροχο κινητής τηλεφωνίας, για να ξεκλειδώσετε τη συσκευή.}other{Εισαγάγετε τον κωδικό PIN της κάρτας SIM. Απομένουν # προσπάθειες.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Η κάρτα SIM απενεργοποιήθηκε. Εισαγάγετε τον κωδικό PUK, για να συνεχίσετε. Απομένει # ακόμη προσπάθεια προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM. Επικοινωνήστε με την εταιρεία κινητής τηλεφωνίας για λεπτομέρειες.}other{Η κάρτα SIM απενεργοποιήθηκε. Εισαγάγετε τον κωδικό PUK, για να συνεχίσετε. Απομένουν # ακόμη προσπάθειες προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM. Επικοινωνήστε με την εταιρεία κινητής τηλεφωνίας για λεπτομέρειες.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Για να χρησιμοποιήσετε τη λειτουργία Ξεκλείδωμα με το πρόσωπο, ενεργοποιήστε την επιλογή "<b>"Πρόσβαση στην κάμερα"</b>" από το μενού Ρυθμίσεις &gt; Απόρρητο"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Εισαγάγετε τον αριθμό PIN της κάρτας SIM. Απομένουν άλλες <xliff:g id="NUMBER_1">%d</xliff:g> προσπάθειες.</item>
+      <item quantity="one">Εισαγάγετε τον αριθμό PIN της κάρτας SIM. Απομένει άλλη <xliff:g id="NUMBER_0">%d</xliff:g> προσπάθεια. Στη συνέχεια, θα πρέπει να επικοινωνήσετε με τον πάροχο κινητής τηλεφωνίας, για να ξεκλειδώσετε τη συσκευή.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Η κάρτα SIM απενεργοποιήθηκε. Καταχωρίστε τον κωδικό PUK, για να συνεχίσετε. Απομένουν <xliff:g id="_NUMBER_1">%d</xliff:g> ακόμη προσπάθειες προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM. Επικοινωνήστε με την εταιρεία κινητής τηλεφωνίας για λεπτομέρειες.</item>
+      <item quantity="one">Η κάρτα SIM απενεργοποιήθηκε. Καταχωρίστε τον κωδικό PUK, για να συνεχίσετε. Απομένει <xliff:g id="_NUMBER_0">%d</xliff:g> ακόμη προσπάθεια προτού να μην είναι πλέον δυνατή η χρήση της κάρτας SIM. Επικοινωνήστε με την εταιρεία κινητής τηλεφωνίας για λεπτομέρειες.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Προεπιλογή"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Συννεφάκι"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Αναλογικό"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ξεκλειδώστε τη συσκευή σας για να συνεχίσετε"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
index de9dbae..abfe5be 100644
--- a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid card."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Charged"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging wirelessly"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging dock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging rapidly"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging slowly"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging optimised to protect battery"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging temporarily limited"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Press Menu to unlock."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Network locked"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Add a SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"The SIM is missing or not readable. Add a SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Unusable SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Your SIM has been permanently deactivated.\n Contact your wireless service provider for another SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM is locked."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM is PUK-locked."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Unlocking SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"No SIM card"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insert a SIM card."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"The SIM card is missing or not readable. Insert a SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Unusable SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Your SIM card has been permanently disabled.\n Contact your wireless service provider for another SIM card."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM card is locked."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM card is PUK-locked."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Unlocking SIM card…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN area"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Device password"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN area"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact operator for details."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Enter desired PIN code"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirm desired PIN code"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Unlocking SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Unlocking SIM card…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Type a PIN that is 4 to 8 numbers."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK code should be 8 numbers or more."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"You have incorrectly typed your PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"You have incorrectly typed your password <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"You have incorrectly drawn your unlock pattern <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code; you must now contact your operator to unlock your device."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code; you have # remaining attempt before you must contact your operator to unlock your device.}other{Incorrect SIM PIN code; you have # remaining attempts. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your provider to unlock your device.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your operator."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code; you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable.</item>
+      <item quantity="one">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Switch input method"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your operator to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact operator for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact operator for details.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"To use Face Unlock, turn on "<b>"Camera access"</b>" in Settings &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Enter SIM PIN. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Enter SIM PIN. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your operator to unlock your device.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable. Contact operator for details.</item>
+      <item quantity="one">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable. Contact operator for details.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
index 5ae7080..69bcf89 100644
--- a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
@@ -23,24 +23,24 @@
     <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"Enter your PIN"</string>
     <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"Enter your pattern"</string>
     <string name="keyguard_enter_your_password" msgid="7225626204122735501">"Enter your password"</string>
-    <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid Card."</string>
+    <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid card."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Charged"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging wirelessly"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging dock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging rapidly"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging slowly"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging optimized to protect battery"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging temporarily limited"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Press Menu to unlock."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Network locked"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Add a SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"The SIM is missing or not readable. Add a SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Unusable SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Your SIM has been permanently deactivated.\n Contact your wireless service provider for another SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM is locked."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM is PUK-locked."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Unlocking SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"No SIM card"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insert a SIM card."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"The SIM card is missing or not readable. Insert a SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Unusable SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Your SIM card has been permanently disabled.\n Contact your wireless service provider for another SIM card."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM card is locked."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM card is PUK-locked."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Unlocking SIM card…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN area"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Device password"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN area"</string>
@@ -55,22 +55,28 @@
     <string name="kg_wrong_pin" msgid="4160978845968732624">"Wrong PIN"</string>
     <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Try again in # second.}other{Try again in # seconds.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Enter SIM PIN."</string>
-    <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Enter SIM PIN for \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
+    <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Enter SIM PIN for \'<xliff:g id="CARRIER">%1$s</xliff:g>\'."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Disable eSIM to use device without mobile service."</string>
-    <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"SIM is now disabled. Enter PUK code to continue. Contact carrier for details."</string>
-    <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact carrier for details."</string>
+    <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"SIM is now disabled. Enter PUK code to continue. Contact operator for details."</string>
+    <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact operator for details."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Enter desired PIN code"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirm desired PIN code"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Unlocking SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Unlocking SIM card…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Type a PIN that is 4 to 8 numbers."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK code should be 8 numbers or more."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"You have incorrectly typed your PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"You have incorrectly typed your password <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"You have incorrectly drawn your unlock pattern <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
-    <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code you must now contact your carrier to unlock your device."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code, you have # remaining attempt before you must contact your carrier to unlock your device.}other{Incorrect SIM PIN code, you have # remaining attempts. }}"</string>
-    <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your carrier."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code, you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string>
+    <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code; you must now contact your operator to unlock your device."</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your provider to unlock your device.</item>
+    </plurals>
+    <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your operator."</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable.</item>
+      <item quantity="one">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Switch input method"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
-    <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognized"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your carrier to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact carrier for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact carrier for details.}}"</string>
+    <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"To use Face Unlock, turn on "<b>"Camera access"</b>" in Settings &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Enter SIM PIN. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Enter SIM PIN. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your operator to unlock your device.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable. Contact operator for details.</item>
+      <item quantity="one">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable. Contact operator for details.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
-    <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
+    <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
index de9dbae..abfe5be 100644
--- a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid card."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Charged"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging wirelessly"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging dock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging rapidly"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging slowly"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging optimised to protect battery"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging temporarily limited"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Press Menu to unlock."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Network locked"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Add a SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"The SIM is missing or not readable. Add a SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Unusable SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Your SIM has been permanently deactivated.\n Contact your wireless service provider for another SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM is locked."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM is PUK-locked."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Unlocking SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"No SIM card"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insert a SIM card."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"The SIM card is missing or not readable. Insert a SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Unusable SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Your SIM card has been permanently disabled.\n Contact your wireless service provider for another SIM card."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM card is locked."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM card is PUK-locked."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Unlocking SIM card…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN area"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Device password"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN area"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact operator for details."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Enter desired PIN code"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirm desired PIN code"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Unlocking SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Unlocking SIM card…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Type a PIN that is 4 to 8 numbers."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK code should be 8 numbers or more."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"You have incorrectly typed your PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"You have incorrectly typed your password <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"You have incorrectly drawn your unlock pattern <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code; you must now contact your operator to unlock your device."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code; you have # remaining attempt before you must contact your operator to unlock your device.}other{Incorrect SIM PIN code; you have # remaining attempts. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your provider to unlock your device.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your operator."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code; you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable.</item>
+      <item quantity="one">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Switch input method"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your operator to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact operator for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact operator for details.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"To use Face Unlock, turn on "<b>"Camera access"</b>" in Settings &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Enter SIM PIN. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Enter SIM PIN. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your operator to unlock your device.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable. Contact operator for details.</item>
+      <item quantity="one">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable. Contact operator for details.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
index de9dbae..abfe5be 100644
--- a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid card."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Charged"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging wirelessly"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging dock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging rapidly"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging slowly"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging optimised to protect battery"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging temporarily limited"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Press Menu to unlock."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Network locked"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Add a SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"The SIM is missing or not readable. Add a SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Unusable SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Your SIM has been permanently deactivated.\n Contact your wireless service provider for another SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM is locked."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM is PUK-locked."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Unlocking SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"No SIM card"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insert a SIM card."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"The SIM card is missing or not readable. Insert a SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Unusable SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Your SIM card has been permanently disabled.\n Contact your wireless service provider for another SIM card."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM card is locked."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM card is PUK-locked."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Unlocking SIM card…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN area"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Device password"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN area"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact operator for details."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Enter desired PIN code"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirm desired PIN code"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Unlocking SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Unlocking SIM card…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Type a PIN that is 4 to 8 numbers."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK code should be 8 numbers or more."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"You have incorrectly typed your PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"You have incorrectly typed your password <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"You have incorrectly drawn your unlock pattern <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code; you must now contact your operator to unlock your device."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code; you have # remaining attempt before you must contact your operator to unlock your device.}other{Incorrect SIM PIN code; you have # remaining attempts. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Incorrect SIM PIN code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your provider to unlock your device.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your operator."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code; you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable.</item>
+      <item quantity="one">Incorrect SIM PUK code. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Switch input method"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your operator to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact operator for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact operator for details.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"To use Face Unlock, turn on "<b>"Camera access"</b>" in Settings &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Enter SIM PIN. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="one">Enter SIM PIN. You have <xliff:g id="NUMBER_0">%d</xliff:g> remaining attempt before you must contact your operator to unlock your device.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_1">%d</xliff:g> remaining attempts before SIM becomes permanently unusable. Contact operator for details.</item>
+      <item quantity="one">SIM is now disabled. Enter PUK code to continue. You have <xliff:g id="_NUMBER_0">%d</xliff:g> remaining attempt before SIM becomes permanently unusable. Contact operator for details.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
index 7f29899..804b6ae 100644
--- a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‏‎‎‎‏‏‎‏‏‏‏‏‎‎‏‎‏‎‏‎‎‏‏‎‏‏‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‏‏‏‏‎‏‏‎‎Invalid Card.‎‏‎‎‏‎"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‎‏‏‎‏‎‏‎‎‏‎‎‏‏‎‎‏‎‎‏‎‏‎‎‏‎‏‏‏‎‎‎‎‏‎‏‎‎‏‏‎‎‎‏‏‎‎‎‎‎‏‏‎Charged‎‏‎‎‏‎"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‏‎‏‏‏‏‎‎‏‏‏‎‏‏‎‏‏‏‎‎‏‎‏‏‎‎‎‏‏‎‏‏‎‏‏‎‎‏‏‏‎‎‏‏‏‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging wirelessly‎‏‎‎‏‎"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‎‎‏‏‏‎‎‎‏‏‎‎‎‎‎‏‎‎‎‎‏‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging‎‏‎‎‏‎"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‎‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‏‏‎‎‎‎‎‏‏‎‏‏‎‏‏‎‎‎‏‎‎‎‎‎‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging Dock‎‏‎‎‏‎"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‎‎‏‎‏‏‎‏‎‎‎‏‏‎‏‎‎‏‎‏‏‏‏‏‎‎‎‏‎‎‎‎‎‎‎‎‏‏‏‏‎‏‎‏‎‏‏‏‏‏‏‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging‎‏‎‎‏‎"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‎‎‏‏‏‎‎‎‎‎‎‏‎‏‎‎‎‎‎‎‎‏‎‏‎‏‏‎‏‎‏‏‏‎‎‏‎‎‎‏‏‏‏‏‎‏‎‎‎‎‏‎‎‎‏‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging rapidly‎‏‎‎‏‎"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‏‏‏‏‎‏‎‎‏‏‎‏‎‏‏‎‎‎‏‎‎‏‎‎‏‎‏‏‏‏‎‏‏‏‎‏‎‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging slowly‎‏‎‎‏‎"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‏‏‎‏‎‏‏‏‏‎‎‎‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‎‎‏‎‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‎‏‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging optimized to protect battery‎‏‎‎‏‎"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‎‏‎‎‏‎‏‎‏‎‎‎‎‏‏‎‏‎‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‏‎‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="PERCENTAGE">%s</xliff:g>‎‏‎‎‏‏‏‎ • Charging temporarily limited‎‏‎‎‏‎"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‎‎‎‎‎‎‎‎‏‎‏‏‏‎‏‎‏‎‎‎‏‏‎‏‎‏‏‎‏‏‎‎‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‏‏‏‎‏‎‎Press Menu to unlock.‎‏‎‎‏‎"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‎‏‎‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‎‏‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‎‎‎Network locked‎‏‎‎‏‎"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‎‎‎‎‎‎‏‏‎‏‏‎‏‏‎‏‏‎‎‎‏‏‏‎‏‏‏‏‎‎‎‏‎‎‎‎‎‎‎‎‏‏‏‏‎‎‏‎‎‏‎‏‎‎‎‎No SIM‎‏‎‎‏‎"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‏‎‎‏‏‎‎‎‎‏‎‏‎‎‎‎‏‎‎‎‎‎‎‏‏‎‏‎‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‎Add a SIM.‎‏‎‎‏‎"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‏‏‏‏‏‎‎‏‏‎‎‎‎‏‎‎‏‏‎‏‎‎‎‎‏‎‎‎‎‏‎‎‏‏‏‏‎‏‎‏‎‏‏‏‎‎‎‏‏‎‏‎‎‏‏‏‎‎‎The SIM is missing or not readable. Add a SIM.‎‏‎‎‏‎"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‎‎‎‏‏‎‎‏‎‏‎‏‏‎‏‎‎‎‎‎‎‏‎‎‏‏‎‎‏‏‎‎‏‎‏‎‏‏‎‎‏‎‏‏‎‎‎‎‏‎‏‎‎‎Unusable SIM.‎‏‎‎‏‎"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‏‎‏‏‏‏‎‏‎‎‏‏‏‎‏‏‏‏‎‎‏‎‎‏‏‏‎‏‏‏‎‎‎‏‎‏‎‏‎‏‏‏‎‎‎‏‎‎‏‏‏‎‏‎‎‎Your SIM has been permanently deactivated.‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎ Contact your wireless service provider for another SIM.‎‏‎‎‏‎"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‏‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‏‏‏‎‏‏‎‎‎‎‎‏‏‏‎‏‎‏‎‏‏‏‎‏‎‏‏‏‏‏‎‎‏‏‎‎SIM is locked.‎‏‎‎‏‎"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‎‏‏‏‏‎‎SIM is PUK-locked.‎‏‎‎‏‎"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‏‏‏‏‎‏‎‏‎‎‎‎‏‏‎‎‏‎‎‎‏‏‎‎‏‏‏‏‎‎‎‏‎‏‎‏‎‎‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎Unlocking SIM…‎‏‎‎‏‎"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‎‏‎‏‏‎‏‎‏‏‎‏‎‏‎‏‎‎‎‎‏‎‎‎‏‎‎‏‎‎‎‎‏‎‎‏‏‎‎‏‎‎‎‎‎‎‏‎‏‎‏‏‎No SIM card‎‏‎‎‏‎"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‏‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‏‎‎‏‏‏‏‎‎‏‎‎‏‎‏‏‎‏‏‏‎‎Insert a SIM card.‎‏‎‎‏‎"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‏‎‎‏‎‏‎‎‏‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‎‏‏‏‏‏‎‎‎‎‎‏‎‏‎‎The SIM card is missing or not readable. Insert a SIM card.‎‏‎‎‏‎"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‎‎‏‎‏‎‏‏‎‎‏‏‎‎‏‏‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‎‎‏‎‎Unusable SIM card.‎‏‎‎‏‎"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎‎‎‏‎‏‏‏‏‏‎‏‎‎‎‏‏‏‎‎‎‏‎‏‏‏‎‏‏‎‏‎‏‏‎‎‏‏‏‎‎Your SIM card has been permanently disabled.‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎ Contact your wireless service provider for another SIM card.‎‏‎‎‏‎"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‏‏‏‏‎‏‎‏‎‏‏‎‎‎‎‎‎‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‎‎SIM card is locked.‎‏‎‎‏‎"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‏‎‏‎‏‎‎‏‎‎‎‏‏‏‎‏‏‎‎‎‎‏‎‎‏‏‎‎‎‏‏‎‏‏‎‎SIM card is PUK-locked.‎‏‎‎‏‎"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‎‏‎‏‎‎‏‎‎‎‎‎‎‎‎‎‎‎‏‎‎‎‏‏‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎‏‏‏‎‏‏‏‎‏‎Unlocking SIM card…‎‏‎‎‏‎"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‏‏‏‏‎‎‏‏‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‏‏‎‏‎‎‎‏‎‎‎‏‏‎‏‎‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎PIN area‎‏‎‎‏‎"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‎‏‏‏‎‏‎‎‎‎‏‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‏‎‎‏‏‏‎‎‏‏‎‎‎‏‎‏‎‎‏‎‎‏‏‏‎‏‎‏‎‏‎‎Device password‎‏‎‎‏‎"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‏‏‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‏‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‏‎‎SIM PIN area‎‏‎‎‏‎"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‏‎‏‏‎‏‏‏‎‎‏‏‏‎‎‎‏‏‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎‎‎‏‏‎‏‏‎‏‏‏‏‏‎SIM \"‎‏‎‎‏‏‎<xliff:g id="CARRIER">%1$s</xliff:g>‎‏‎‎‏‏‏‎\" is now disabled. Enter PUK code to continue. Contact carrier for details.‎‏‎‎‏‎"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‏‏‎‎‎‏‎‏‏‏‎‎‏‎‎‎‎‎‏‎‏‎‎‏‎‏‎‎‏‏‏‏‎‎‎‏‏‏‏‏‏‏‎Enter desired PIN code‎‏‎‎‏‎"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‎‎‏‎‎‎‏‎‎‏‎‏‎‏‏‏‎‎‏‎‎‏‏‎‏‎‎‎‎‏‎‎‏‎‏‎‎‏‏‎‏‎‏‎‏‎‎‏‎‏‎‏‏‏‎‎‎Confirm desired PIN code‎‏‎‎‏‎"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‏‏‏‎‏‏‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‎‎Unlocking SIM…‎‏‎‎‏‎"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‏‎‏‎‏‎‏‏‏‎‎‎‏‎‏‎‎‏‎‏‏‎‏‏‎‎Unlocking SIM card…‎‏‎‎‏‎"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‎‏‎‏‎‏‎‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‎‎‏‏‎‎‎‏‏‎‏‎‏‎‎‏‎‎Type a PIN that is 4 to 8 numbers.‎‏‎‎‏‎"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‏‎‎‏‏‏‎‎‏‎‎‏‎‏‎‏‏‎‎‏‎‏‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎PUK code should be 8 numbers or more.‎‏‎‎‏‎"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‏‏‎‎‎‏‏‏‏‎‏‎‎‎‏‎‎‏‏‎‏‏‏‎‏‎‏‏‏‏‏‏‎‎‎‎‏‎‎‏‎‏‎‎‏‏‏‎‏‏‎You have incorrectly typed your PIN ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%1$d</xliff:g>‎‏‎‎‏‏‏‎ times. ‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎Try again in ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%2$d</xliff:g>‎‏‎‎‏‏‏‎ seconds.‎‏‎‎‏‎"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‏‎‏‎‎‏‏‎‏‎‎‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‏‎‎‏‏‏‏‎‏‎‎‏‏‎‎‎‏‎‏‎‏‎‏‎‏‎‏‏‎‎You have incorrectly typed your password ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%1$d</xliff:g>‎‏‎‎‏‏‏‎ times. ‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎Try again in ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%2$d</xliff:g>‎‏‎‎‏‏‏‎ seconds.‎‏‎‎‏‎"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‎‎‎‎‎‎‏‏‏‎‎‏‎‏‎‎‏‎‏‎‎‎‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‏‏‏‎‏‏‏‏‎‎‏‎‏‎‏‎‎‎‎‎You have incorrectly drawn your unlock pattern ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%1$d</xliff:g>‎‏‎‎‏‏‏‎ times. ‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎Try again in ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%2$d</xliff:g>‎‏‎‎‏‏‏‎ seconds.‎‏‎‎‏‎"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‏‎‏‏‎‏‏‏‏‎‏‏‏‎‏‏‎‎‏‎‏‎‎‎‏‏‎‏‏‏‎‏‎‎‎‏‎‏‎‏‎‏‏‎‎‏‎‏‎‎‎‏‎‏‎Incorrect SIM PIN code you must now contact your carrier to unlock your device.‎‏‎‎‏‎"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‏‏‏‎‏‎‎‎‎‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎‎‎‎‏‎‏‎Incorrect SIM PIN code, you have # remaining attempt before you must contact your carrier to unlock your device.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‏‏‏‎‏‎‎‎‎‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎‎‎‎‏‎‏‎Incorrect SIM PIN code, you have # remaining attempts. ‎‏‎‎‏‎}}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‏‎‎‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‏‎‎‎‏‎‎Incorrect SIM PIN code, you have ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempts.‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‏‎‎‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‏‎‎‎‏‎‎Incorrect SIM PIN code, you have ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempt before you must contact your carrier to unlock your device.‎‏‎‎‏‎</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‎‏‎‎‏‎‏‏‏‏‎‎‏‎‏‏‏‏‎‎‎‎‎‎‎‏‎‏‏‏‏‎‏‏‎‏‎‏‏‏‏‎‎‏‏‏‏‎‎‎‏‎‎‏‎SIM is unusable. Contact your carrier.‎‏‎‎‏‎"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‎‎‎‏‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‎‎‏‏‎Incorrect SIM PUK code, you have # remaining attempt before SIM becomes permanently unusable.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‎‎‎‏‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‎‎‏‏‎Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.‎‏‎‎‏‎}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‏‎‎‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‎‏‏‎‎Incorrect SIM PUK code, you have ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempts before SIM becomes permanently unusable.‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‏‎‎‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‎‏‏‎‎Incorrect SIM PUK code, you have ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempt before SIM becomes permanently unusable.‎‏‎‎‏‎</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‎‎‏‏‏‏‎‏‎‏‎‎‎‎‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‎‏‎‎‏‎‏‏‎‎‏‏‏‎‎‏‏‏‏‎‏‎‎‏‎SIM PIN operation failed!‎‏‎‎‏‎"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‎‎‎‏‎‎‏‏‎‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‏‎‎‏‎‏‎‏‎‏‏‎‏‏‎‎‏‏‏‏‎‎SIM PUK operation failed!‎‏‎‎‏‎"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎‏‏‎‎‎‎‎‏‎‎‏‏‎‎‎‎‏‏‎‏‎‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‏‎‎‎‏‎‎‎‏‎Switch input method‎‏‎‎‏‎"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎Pattern required after device restarts‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‎‎‎‎‎‏‎‎‎‏‎‎‎‏‎‏‎‏‏‏‎‎‎‎‎‏‎‏‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‎PIN required after device restarts‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‏‏‏‏‎‏‎‎‎‏‏‏‎‎‏‎‎‏‎‎‎‎‎‏‏‏‎‎‎‏‏‎‎‎‎‏‎‎Password required after device restarts‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‎‏‎‎‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‎For additional security, use pattern instead‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‏‏‎‎‎‎‎For additional security, use PIN instead‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‎‏‎‎For additional security, use password instead‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‏‏‎‎‏‎‏‏‎‎‏‎‎‎‏‎Pattern required for additional security‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‎‎‎‏‏‎‎‎‏‎‏‏‎‏‎‎‎‎‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‎‎‏‎‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎PIN required for additional security‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‏‎‏‏‎‎Password required for additional security‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎‎‏‏‎‏‏‎‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‎‏‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎‎‎‎‏‎‏‎Device locked by admin‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‏‏‎‎‎‎‎‏‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎Device was locked manually‎‏‎‎‏‎"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‎‎‎‎‏‏‏‏‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎Not recognized‎‏‎‎‏‎"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‏‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‏‎‏‏‏‎‎‏‏‏‎‏‎‎‎‏‏‏‎‎‎‎‎To use Face Unlock, turn on camera access in Settings‎‏‎‎‏‎"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‏‏‎‏‏‎‎‎‎‏‏‏‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‎‎‎‏‎Enter SIM PIN. You have # remaining attempt before you must contact your carrier to unlock your device.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‏‏‎‏‏‎‎‎‎‏‏‏‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‎‎‎‏‎Enter SIM PIN. You have # remaining attempts.‎‏‎‎‏‎}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‎‎‏‏‏‎‏‎‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact carrier for details.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‎‎‏‏‏‎‏‎‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact carrier for details.‎‏‎‎‏‎}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‎‎‏‏‎‏‏‎‎‎‏‎‏‎‎‏‏‎‎‎‎‎‎‎‏‎‎‎‏‎‏‎‎‎‏‎‎‏‏‏‎‏‎‏‎‏‎‎‎‏‏‎‎To use Face Unlock, turn on ‎‏‎‎‏‏‎"<b>"‎‏‎‎‏‏‏‎Camera access‎‏‎‎‏‏‎"</b>"‎‏‎‎‏‏‏‎ in Settings &gt; Privacy‎‏‎‎‏‎"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‎‏‏‏‎‎‎‎‎‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‎‎‎‏‏‏‎‏‏‏‎‏‎‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎Enter SIM PIN. You have ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempts.‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‎‏‏‏‎‎‎‎‎‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‎‎‎‏‏‏‎‏‏‏‎‏‎‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎Enter SIM PIN. You have ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempt before you must contact your carrier to unlock your device.‎‏‎‎‏‎</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‎‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‎‏‏‏‎SIM is now disabled. Enter PUK code to continue. You have ‎‏‎‎‏‏‎<xliff:g id="_NUMBER_1">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempts before SIM becomes permanently unusable. Contact carrier for details.‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‎‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‎‏‏‏‎SIM is now disabled. Enter PUK code to continue. You have ‎‏‎‎‏‏‎<xliff:g id="_NUMBER_0">%d</xliff:g>‎‏‎‎‏‏‏‎ remaining attempt before SIM becomes permanently unusable. Contact carrier for details.‎‏‎‎‏‎</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‏‎‎‎‎Default‎‏‎‎‏‎"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‏‏‎‎‎‏‎‏‏‏‎‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‏‎‏‎‏‏‏‏‎‏‎Bubble‎‏‎‎‏‎"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎Analog‎‏‎‎‏‎"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‏‏‏‎‎‎‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‏‏‎‏‏‏‏‎Unlock your device to continue‎‏‎‎‏‎"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
index e0aa9a8..d9adc7c 100644
--- a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Tarjeta no válida"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Cargada"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando de manera inalámbrica"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Conectado y cargando"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando rápidamente"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando lentamente"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carga optimizada para proteger la batería"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carga limitada temporalmente"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Presiona Menú para desbloquear."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Bloqueada para la red"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No hay ninguna tarjeta SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Introduce una tarjeta SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Falta la tarjeta SIM o no se puede leer. Introduce una tarjeta SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Tarjeta SIM inutilizable."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Tu tarjeta SIM se desactivó permanentemente.\n Ponte en contacto con tu proveedor de servicios inalámbricos para obtener otra tarjeta SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"La tarjeta SIM está bloqueada."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"La tarjeta SIM está bloqueada con el código PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Desbloqueando tarjeta SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Sin tarjeta SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Inserta una tarjeta SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Falta la tarjeta SIM o no se puede leer. Introduce una tarjeta SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Tarjeta SIM inutilizable"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Tu tarjeta SIM se inhabilitó de manera permanente.\n Comunícate con tu proveedor de servicios inalámbricos para obtener otra."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"La tarjeta SIM está bloqueada."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"La tarjeta SIM está bloqueada con el código PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Desbloqueando tarjeta SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Área de PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Contraseña del dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Área de PIN de la tarjeta SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Patrón incorrecto"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Contraseña incorrecta"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"PIN incorrecto"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Vuelve a intentarlo en # segundo.}many{Vuelve a intentarlo en # segundos.}other{Vuelve a intentarlo en # segundos.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Vuelve a intentarlo en # segundo.}other{Vuelve a intentarlo en # segundos.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Ingresa el PIN de la tarjeta SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Ingresa el PIN de la tarjeta SIM de \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Inhabilita la tarjeta eSIM para usar el dispositivo sin servicio móvil."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"La tarjeta SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" está inhabilitada. Para continuar, ingresa el código PUK. Para obtener más información, comunícate con el proveedor."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Ingresa el código PIN deseado"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirma el código PIN deseado"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Desbloqueando tarjeta SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Desbloqueando tarjeta SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Escribe un PIN que tenga entre 4 y 8 números."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"El código PUK debe tener al menos 8 números."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Escribiste tu PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> veces de manera incorrecta. \n\nVuelve a intentarlo en <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Escribiste tu contraseña <xliff:g id="NUMBER_0">%1$d</xliff:g> veces de manera incorrecta. \n\nVuelve a intentarlo en <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Dibujaste tu patrón de desbloqueo <xliff:g id="NUMBER_0">%1$d</xliff:g> veces de manera incorrecta. \n\nVuelve a intentarlo en <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"El código PIN de la tarjeta SIM es incorrecto. Debes comunicarte con tu proveedor para desbloquear el dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{El código PIN de la tarjeta SIM es incorrecto. Tienes # intento restante antes de que debas comunicarte con tu operador para desbloquear el dispositivo.}many{El código PIN de la tarjeta SIM es incorrecto. Tienes # intentos restantes. }other{El código PIN de la tarjeta SIM es incorrecto. Tienes # intentos restantes. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">El código PIN de la tarjeta SIM es incorrecto. Te quedan <xliff:g id="NUMBER_1">%d</xliff:g> intentos más.</item>
+      <item quantity="one">El código PIN de la tarjeta SIM es incorrecto. Te queda <xliff:g id="NUMBER_0">%d</xliff:g> intento antes de que debas comunicarte con tu proveedor para desbloquear el dispositivo.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"La tarjeta SIM no se puede usar. Comunícate con tu proveedor."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{El código PUK de la tarjeta SIM es incorrecto. Tienes # intento restante antes de que la tarjeta SIM quede inutilizable permanentemente.}many{El código PUK de la tarjeta SIM es incorrecto. Tienes # intentos restantes antes de que la tarjeta SIM quede inutilizable permanentemente.}other{El código PUK de la tarjeta SIM es incorrecto. Tienes # intentos restantes antes de que la tarjeta SIM quede inutilizable permanentemente.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">El código PUK de la tarjeta SIM es incorrecto. Tienes <xliff:g id="NUMBER_1">%d</xliff:g> intentos más antes de que la tarjeta SIM quede inutilizable de manera permanente.</item>
+      <item quantity="one">El código PUK de la tarjeta SIM es incorrecto. Tienes <xliff:g id="NUMBER_0">%d</xliff:g> intento antes de que la tarjeta SIM quede inutilizable de manera permanente.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Se produjo un error al desbloquear la tarjeta SIM con el PIN."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Se produjo un error al desbloquear la tarjeta SIM con el PUK."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Cambiar método de entrada"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Se requiere el patrón después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Se requiere el PIN después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Se requiere la contraseña después de reiniciar el dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para seguridad adicional, usa un patrón"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para seguridad adicional, usa un PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para seguridad adicional, usa una contraseña"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Se requiere el patrón por razones de seguridad"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Se requiere el PIN por razones de seguridad"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Se requiere la contraseña por razones de seguridad"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se bloqueó de forma manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoció"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Activa acceso a cámara en Config. y usa Desb. facial"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Ingresa el PIN de la tarjeta SIM. Tienes # intento restante antes de que debas comunicarte con tu operador para desbloquear el dispositivo.}many{Ingresa el PIN de la tarjeta SIM. Tienes # intentos restantes.}other{Ingresa el PIN de la tarjeta SIM. Tienes # intentos restantes.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Se inhabilitó la tarjeta SIM. Para continuar, ingresa el código PUK. Tienes # intento restante antes de que la SIM quede inutilizable permanentemente. Comunícate con tu operador para conocer más detalles.}many{Se inhabilitó la tarjeta SIM. Para continuar, ingresa el código PUK. Tienes # intentos restantes antes de que la SIM quede inutilizable permanentemente. Comunícate con tu operador para conocer más detalles.}other{Se inhabilitó la tarjeta SIM. Para continuar, ingresa el código PUK. Tienes # intentos restantes antes de que la SIM quede inutilizable permanentemente. Comunícate con tu operador para conocer más detalles.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para usar Desbloqueo facial, activa el "<b>"Acceso a la cámara"</b>" en Configuración y privacidad"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Ingresa el PIN de la SIM. Quedan <xliff:g id="NUMBER_1">%d</xliff:g> intentos más.</item>
+      <item quantity="one">Ingresa el PIN de la SIM. Queda <xliff:g id="NUMBER_0">%d</xliff:g> intento antes de que debas comunicarte con tu proveedor para desbloquear el dispositivo.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Se inhabilitó la SIM. Para continuar, ingresa el código PUK. Te quedan <xliff:g id="_NUMBER_1">%d</xliff:g> intentos más antes de que la SIM quede inutilizable permanentemente. Comunícate con tu proveedor para obtener más detalles.</item>
+      <item quantity="one">Se inhabilitó la SIM. Para continuar, ingresa el código PUK. Te queda <xliff:g id="_NUMBER_0">%d</xliff:g> intento más antes de que la SIM quede inutilizable permanentemente. Comunícate con tu proveedor para obtener más detalles.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloquea tu dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-es/strings.xml b/packages/SystemUI/res-keyguard/values-es/strings.xml
index fc65751..3702be2 100644
--- a/packages/SystemUI/res-keyguard/values-es/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-es/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Tarjeta no válida."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Cargado"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando sin cables"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Base de carga"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando rápidamente"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando lentamente"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carga optimizada para proteger la batería"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carga limitada temporalmente"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pulsa el menú para desbloquear la pantalla."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Bloqueada para la red"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"No hay ninguna SIM."</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Añade una SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Falta la SIM o no se puede leer. Añade una SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"No se puede usar la SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Tu SIM se ha desactivado de forma permanente.\n Para obtener otra SIM, ponte en contacto con tu proveedor de servicios inalámbricos."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"La SIM está bloqueada."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"La SIM está bloqueada con el código PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Desbloqueando SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Falta la tarjeta SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Inserta una tarjeta SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Falta la tarjeta SIM o no se puede leer. Inserta una tarjeta SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"La tarjeta SIM se ha inhabilitado."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"La tarjeta SIM se ha inhabilitado permanentemente.\n Para obtener otra, ponte en contacto con tu proveedor de servicios de telefonía."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"La tarjeta SIM está bloqueada."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"La tarjeta SIM está bloqueada con el código PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Desbloqueando la tarjeta SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Área de PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Contraseña del dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Área de PIN de la tarjeta SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Patrón incorrecto"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Contraseña incorrecta"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"PIN incorrecto"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Vuelve a intentarlo en # segundo.}many{Vuelve a intentarlo en # segundos.}other{Vuelve a intentarlo en # segundos.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Vuelve a intentarlo en # segundo.}other{Vuelve a intentarlo en # segundos.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Introduce el PIN de la tarjeta SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Introduce el PIN de la tarjeta SIM de <xliff:g id="CARRIER">%1$s</xliff:g>."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Inhabilita la tarjeta eSIM para usar el dispositivo sin servicio móvil."</string>
@@ -61,34 +61,45 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"La tarjeta SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" está inhabilitada. Para continuar, introduce el código PUK. Si quieres obtener más información, ponte en contacto con el operador."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Introduce el código PIN que quieras utilizar"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirma el código PIN que quieras utilizar"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Desbloqueando SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Desbloqueando la tarjeta SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Escribe un código PIN que tenga entre 4 y 8 dígitos."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"El código PUK debe tener 8 números como mínimo."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Has fallado <xliff:g id="NUMBER_0">%1$d</xliff:g> veces al escribir el PIN. \n\nVuelve a intentarlo dentro de <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Has fallado <xliff:g id="NUMBER_0">%1$d</xliff:g> veces al introducir la contraseña. \n\nVuelve a intentarlo dentro de <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Has fallado <xliff:g id="NUMBER_0">%1$d</xliff:g> veces al dibujar el patrón de desbloqueo. \n\nVuelve a intentarlo dentro de <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"El código PIN de la tarjeta SIM es incorrecto. Debes ponerte en contacto con tu operador para desbloquear el dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Código PIN de la SIM incorrecto. Te queda # intento antes de tener que ponerte en contacto con tu operador para desbloquear el dispositivo.}many{Código PIN de la SIM incorrecto. Te quedan # intentos. }other{Código PIN de la SIM incorrecto. Te quedan # intentos. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">El código PIN de la tarjeta SIM es incorrecto. Quedan <xliff:g id="NUMBER_1">%d</xliff:g> intentos.</item>
+      <item quantity="one">El código PIN de la tarjeta SIM es incorrecto. Queda <xliff:g id="NUMBER_0">%d</xliff:g> intento para tener que ponerte en contacto con tu operador para desbloquear el dispositivo.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"La tarjeta SIM no se puede utilizar. Ponte en contacto con tu operador."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Código PUK de la SIM incorrecto. Te queda # intento antes de que la SIM quede inservible permanentemente.}many{Código PUK de la SIM incorrecto. Te quedan # intentos antes de que la SIM quede inservible permanentemente.}other{Código PUK de la SIM incorrecto. Te quedan # intentos antes de que la SIM quede inservible permanentemente.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">El código PUK de la tarjeta SIM es incorrecto. Quedan <xliff:g id="NUMBER_1">%d</xliff:g> intentos paraa que la tarjeta SIM no se pueda utilizar de forma permanente.</item>
+      <item quantity="one">El código PUK de la tarjeta SIM es incorrecto. Queda <xliff:g id="NUMBER_0">%d</xliff:g> intento para que la tarjeta SIM no se pueda utilizar de forma permanente.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"No se ha podido desbloquear la tarjeta SIM con el código PIN."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"No se ha podido desbloquear la tarjeta SIM con el código PUK."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Cambiar método de introducción"</string>
-    <string name="airplane_mode" msgid="2528005343938497866">"Modo Avión"</string>
+    <string name="airplane_mode" msgid="2528005343938497866">"Modo avión"</string>
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Debes introducir el patrón después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Debes introducir el PIN después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Debes introducir la contraseña después de reiniciar el dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para mayor seguridad, usa el patrón"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para mayor seguridad, usa el PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para mayor seguridad, usa la contraseña"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Debes introducir el patrón como medida de seguridad adicional"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Debes introducir el PIN como medida de seguridad adicional"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Debes introducir la contraseña como medida de seguridad adicional"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se ha bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoce"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Desbloqueo facial: activa el acceso a la cámara"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Introduce el PIN de la SIM. Te queda # intento antes de tener que ponerte en contacto con tu operador para desbloquear el dispositivo.}many{Introduce el PIN de la SIM. Te quedan # intentos.}other{Introduce el PIN de la SIM. Te quedan # intentos.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{La SIM se ha inhabilitado. Introduce el código PUK para continuar. Te queda # intento antes de que la SIM quede inservible permanentemente. Ponte en contacto con tu operador para obtener más información.}many{La SIM se ha inhabilitado. Introduce el código PUK para continuar. Te quedan # intentos antes de que la SIM quede inservible permanentemente. Ponte en contacto con tu operador para obtener más información.}other{La SIM se ha inhabilitado. Introduce el código PUK para continuar. Te quedan # intentos antes de que la SIM quede inservible permanentemente. Ponte en contacto con tu operador para obtener más información.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para usar Desbloqueo Facial, habilita el "<b>"acceso a la cámara"</b>" en Ajustes y privacidad"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Introduce el PIN de la tarjeta SIM. Te quedan <xliff:g id="NUMBER_1">%d</xliff:g> intentos.</item>
+      <item quantity="one">Introduce el PIN de la tarjeta SIM. Te queda <xliff:g id="NUMBER_0">%d</xliff:g> intento para tener que ponerte en contacto con tu operador para desbloquear el dispositivo.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">La tarjeta SIM está inhabilitada. Introduce el código PUK para continuar. Te quedan <xliff:g id="_NUMBER_1">%d</xliff:g> intentos para que la tarjeta SIM quede inservible de forma permanente. Ponte en contacto con tu operador para obtener más información.</item>
+      <item quantity="one">La tarjeta SIM está inhabilitada. Introduce el código PUK para continuar. Te queda <xliff:g id="_NUMBER_0">%d</xliff:g> intento para que la tarjeta SIM quede inservible de forma permanente. Ponte en contacto con tu operador para obtener más información.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloquea tu dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-et/strings.xml b/packages/SystemUI/res-keyguard/values-et/strings.xml
index 86a8b11..9488054 100644
--- a/packages/SystemUI/res-keyguard/values-et/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-et/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Kehtetu kaart."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Laetud"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Juhtmeta laadimine"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laadimine"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laadimisdokk"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laadimine"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kiirlaadimine"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Aeglane laadimine"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laadimine on aku kaitsmiseks optimeeritud"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laadimine on ajutiselt piiratud"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Vajutage avamiseks menüüklahvi."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Võrk on lukus"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM-i pole"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Lisage SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM puudub või pole loetav. Lisage SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-i ei saa kasutada."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Teie SIM on jäädavalt inaktiveeritud.\n Teise SIM-i saamiseks võtke ühendust oma traadita side teenusepakkujaga."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM on lukustatud."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM on PUK-koodiga lukustatud."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-i avamine …"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM-kaarti pole"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Sisestage SIM-kaart."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-kaart puudub või on loetamatu. Sisestage SIM-kaart."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Kasutamiskõlbmatu SIM-kaart."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-kaart on jäädavalt keelatud.\n Uue SIM-kaardi saamiseks võtke ühendust oma traadita side teenusepakkujaga."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kaart on lukus."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kaart on PUK-koodiga lukus."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM-kaardi avamine …"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-koodi ala"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Seadme parool"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM-kaardi PIN-koodi ala"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-kaart „<xliff:g id="CARRIER">%1$s</xliff:g>” on nüüd keelatud. Jätkamiseks sisestage PUK-kood. Lisateabe saamiseks võtke ühendust operaatoriga."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Sisestage soovitud PIN-kood"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Kinnitage soovitud PIN-kood"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-i avamine …"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM-kaardi avamine …"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Sisestage 4–8-numbriline PIN-kood."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-koodi pikkus peab olema vähemalt kaheksa numbrit."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Olete PIN-koodi <xliff:g id="NUMBER_0">%1$d</xliff:g> korda valesti sisestanud. \n\nProovige <xliff:g id="NUMBER_1">%2$d</xliff:g> sekundi pärast uuesti."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Olete parooli <xliff:g id="NUMBER_0">%1$d</xliff:g> korda valesti sisestanud. \n\nProovige <xliff:g id="NUMBER_1">%2$d</xliff:g> sekundi pärast uuesti."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Olete oma avamismustrit <xliff:g id="NUMBER_0">%1$d</xliff:g> korda valesti joonistanud. \n\nProovige <xliff:g id="NUMBER_1">%2$d</xliff:g> sekundi pärast uuesti."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM-kaardi vale PIN-kood. Seadme avamiseks peate nüüd ühendust võtma oma operaatoriga."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM-i vale PIN-kood. Teil on veel # katse, enne kui peate seadme avamiseks operaatoriga ühendust võtma.}other{SIM-i vale PIN-kood. Teil on veel # katset. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM-kaardi vale PIN-kood. Teil on jäänud veel <xliff:g id="NUMBER_1">%d</xliff:g> katset.</item>
+      <item quantity="one">SIM-kaardi vale PIN-kood. Teil on jäänud veel <xliff:g id="NUMBER_0">%d</xliff:g> katse enne, kui peate seadme avamiseks operaatoriga ühendust võtma.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-kaart on kasutamiskõlbmatu. Võtke ühendust operaatoriga."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM-i vale PUK-kood. Teil on veel # katse, enne kui SIM püsivalt lukustatakse.}other{SIM-i vale PUK-kood. Teil on veel # katset, enne kui SIM püsivalt lukustatakse.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM-kaardi vale PUK-kood. Teil on jäänud veel <xliff:g id="NUMBER_1">%d</xliff:g> katset enne, kui SIM-kaart jäädavalt lukustatakse.</item>
+      <item quantity="one">SIM-kaardi vale PUK-kood. Teil on jäänud veel <xliff:g id="NUMBER_0">%d</xliff:g> katse enne, kui SIM-kaart jäädavalt lukustatakse.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM-kaardi PIN-koodi toiming ebaõnnestus."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM-kaardi PUK-koodi toiming ebaõnnestus."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Vaheta sisestusmeetodit"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pärast seadme taaskäivitamist tuleb sisestada muster"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pärast seadme taaskäivitamist tuleb sisestada PIN-kood"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pärast seadme taaskäivitamist tuleb sisestada parool"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Kasutage tugevama turvalisuse huvides hoopis mustrit"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Kasutage tugevama turvalisuse huvides hoopis PIN-koodi"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Kasutage tugevama turvalisuse huvides hoopis parooli"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Lisaturvalisuse huvides tuleb sisestada muster"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Lisaturvalisuse huvides tuleb sisestada PIN-kood"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lisaturvalisuse huvides tuleb sisestada parool"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administraator lukustas seadme"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Seade lukustati käsitsi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tuvastatud"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Näoga avamise kasutamiseks andke seadetes juurdepääs kaamerale"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Sisestage SIM-i PIN-kood. Teil on veel # katse, enne kui peate seadme avamiseks operaatoriga ühendust võtma.}other{Sisestage SIM-i PIN-kood. Teil on veel # katset.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM on nüüd keelatud. Jätkamiseks sisestage PUK-kood. Teil on veel # katse enne, kui SIM püsivalt lukustatakse. Lisateavet küsige operaatorilt.}other{SIM on nüüd keelatud. Jätkamiseks sisestage PUK-kood. Teil on veel # katset enne, kui SIM püsivalt lukustatakse. Lisateavet küsige operaatorilt.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Näoga avamise kasutamiseks lülitage menüüs Seaded &gt; Privaatsus sisse "<b>"juurdepääs kaamerale"</b>"."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Sisestage SIM-kaardi PIN-kood. Jäänud on <xliff:g id="NUMBER_1">%d</xliff:g> katset.</item>
+      <item quantity="one">Sisestage SIM-kaardi PIN-kood. Jäänud on <xliff:g id="NUMBER_0">%d</xliff:g> katse enne, kui peate seadme avamiseks ühendust võtma operaatoriga.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM-kaart on nüüd keelatud. Jätkamiseks sisestage PUK-kood. Teil on jäänud veel <xliff:g id="_NUMBER_1">%d</xliff:g> katset enne, kui SIM-kaart püsivalt lukustatakse. Lisateavet küsige operaatorilt.</item>
+      <item quantity="one">SIM-kaart on nüüd keelatud. Jätkamiseks sisestage PUK-kood. Teil on jäänud veel <xliff:g id="_NUMBER_0">%d</xliff:g> katse enne, kui SIM-kaart püsivalt lukustatakse. Lisateavet küsige operaatorilt.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Vaikenumbrilaud"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mull"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Jätkamiseks avage oma seade"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-eu/strings.xml b/packages/SystemUI/res-keyguard/values-eu/strings.xml
index b79ad39..41d467a 100644
--- a/packages/SystemUI/res-keyguard/values-eu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-eu/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Txartelak ez du balio."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Kargatuta"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hari gabe kargatzen"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kargatzen"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oinarrian kargatzen"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kargatzen"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Bizkor kargatzen"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mantso kargatzen"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kargatzea optimizatu da bateria ez kaltetzeko"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kargatzeko aukera mugatuta dago aldi baterako"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Desblokeatzeko, sakatu Menua."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Sarea blokeatuta dago"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Ez dago SIMik"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Gehitu SIM bat."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIMa falta da, edo ezin da irakurri. Gehitu SIM bat."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Ezin da erabili SIMa."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Betiko desaktibatu da SIMa.\n Jarri harremanetan operadorearekin beste SIM bat eskuratzeko."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIMa blokeatuta dago."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIMa PUKaren bidez desblokeatu behar da."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIMa desblokeatzen…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Ez dago SIM txartelik"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Sartu SIM txartela."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM txartela falta da edo ezin da irakurri. Sartu SIM txartel bat."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM txartela erabilgaitza da."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Betiko desgaitu zaizu SIM txartela.\n Beste SIM txartel bat lortzeko, jarri zerbitzu-hornitzailearekin harremanetan."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Blokeatuta dago SIM txartela."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"PUK bidez blokeatuta dago SIM txartela."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM txartela desblokeatzen…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN kodearen eremua"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Gailuaren pasahitza"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM txartelaren PIN kodearen eremua"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Desgaitu egin da \"<xliff:g id="CARRIER">%1$s</xliff:g>\" operadorearen SIM txartela. Aurrera egiteko, idatzi PUK kodea. Xehetasunak jakiteko, jarri operadorearekin harremanetan."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Idatzi erabili nahi duzun PIN kodea"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Berretsi erabili nahi duzun PIN kodea"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIMa desblokeatzen…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM txartela desblokeatzen…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Idatzi 4 eta 8 zenbaki bitarteko PIN bat."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kodeak 8 zenbaki izan behar ditu gutxienez."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"<xliff:g id="NUMBER_0">%1$d</xliff:g> aldiz idatzi duzu PINa, baina huts egin duzu denetan. \n\nSaiatu berriro <xliff:g id="NUMBER_1">%2$d</xliff:g> segundo barru."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"<xliff:g id="NUMBER_0">%1$d</xliff:g> aldiz idatzi duzu pasahitza, baina huts egin duzu denetan. \n\nSaiatu berriro <xliff:g id="NUMBER_1">%2$d</xliff:g> segundo barru."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"<xliff:g id="NUMBER_0">%1$d</xliff:g> aldiz marraztu duzu desblokeatzeko eredua, baina huts egin duzu denetan. \n\nSaiatu berriro <xliff:g id="NUMBER_1">%2$d</xliff:g> segundo barru."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIMaren PIN kodea ez da zuzena. Gailua desblokeatzeko, operadorearekin jarri beharko duzu harremanetan."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Oker idatzi duzu SIMaren PIN kodea. # saiakera geratzen zaizu gailua desblokeatzeko operadorearekin harremanetan jarri behar izan aurretik.}other{Oker idatzi duzu SIMaren PIN kodea. # saiakera geratzen zaizkizu gailua desblokeatzeko operadorearekin harremanetan jarri behar izan aurretik. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Ez da zuzena SIM txartelaren PIN kodea. <xliff:g id="NUMBER_1">%d</xliff:g> saiakera geratzen zaizkizu gailua desblokeatzeko.</item>
+      <item quantity="one">Ez da zuzena SIM txartelaren PIN kodea. <xliff:g id="NUMBER_0">%d</xliff:g> saiakera geratzen zaizu gailua desblokeatzeko.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM txartela erabilgaitza da. Jarri operadorearekin harremanetan."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Oker idatzi duzu SIMaren PUK kodea. # saiakera geratzen zaizu SIMa betiko ez-erabilgarri geratu aurretik.}other{Oker idatzi duzu SIMaren PUK kodea. # saiakera geratzen zaizkizu SIMa betiko ez-erabilgarri geratu aurretik.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Ez da zuzena SIM txartelaren PUK kodea. <xliff:g id="NUMBER_1">%d</xliff:g> saiakera geratzen zaizkizu SIM txartela betiko erabilgaitz geratu aurretik.</item>
+      <item quantity="one">Ez da zuzena SIM txartelaren PUK kodea. <xliff:g id="NUMBER_0">%d</xliff:g> saiakera geratzen zaizu SIM txartela betiko erabilgaitz geratu aurretik.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Huts egin du SIM txartelaren PIN kodearen eragiketak!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Huts egin du SIM txartelaren PUK kodearen eragiketak!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Aldatu idazketa-metodoa"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Eredua marraztu beharko duzu gailua berrabiarazten denean"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PINa idatzi beharko duzu gailua berrabiarazten denean"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pasahitza idatzi beharko duzu gailua berrabiarazten denean"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Babestuago egoteko, erabili eredua"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Babestuago egoteko, erabili PINa"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Babestuago egoteko, erabili pasahitza"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Eredua behar da gailua babestuago izateko"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PINa behar da gailua babestuago izateko"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pasahitza behar da gailua babestuago izateko"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratzaileak blokeatu egin du gailua"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Eskuz blokeatu da gailua"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ez da ezagutu"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Aurpegi bidezko desblokeoak kamera erabiltzeko baimena behar du"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Idatzi SIMaren PINa. # saiakera geratzen zaizu gailua desblokeatzeko operadorearekin harremanetan jarri behar izan aurretik.}other{Idatzi SIMaren PINa. # saiakera gelditzen zaizkizu.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Orain, SIMa desgaituta dago. Aurrera egiteko, idatzi PUK kodea. # saiakera geratzen zaizu SIMa betiko ez-erabilgarri geratu aurretik. Xehetasunak lortzeko, jarri operadorearekin harremanetan.}other{Orain, SIMa desgaituta dago. Aurrera egiteko, idatzi PUK kodea. # saiakera geratzen zaizkizu SIMa betiko ez-erabilgarri geratu aurretik. Xehetasunak lortzeko, jarri operadorearekin harremanetan.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Aurpegi bidez desblokeatzeko aukera erabiltzeko, aktibatu "<b>"kamera atzitzeko baimena"</b>" Ezarpenak &gt; Pribatutasuna atalean"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Idatzi SIMaren PINa. <xliff:g id="NUMBER_1">%d</xliff:g> saiakera geratzen zaizkizu.</item>
+      <item quantity="one">Idatzi SIMaren PINa. <xliff:g id="NUMBER_0">%d</xliff:g> saiakera geratzen zaizu; oker idatziz gero, operadoreari eskatu beharko diozu gailua desblokeatzeko.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Desgaitu egin da SIM txartela. Aurrera egiteko, idatzi PUK kodea. <xliff:g id="_NUMBER_1">%d</xliff:g> saiakera geratzen zaizkizu SIM txartela betiko erabilgaitz geratu aurretik. Xehetasunak lortzeko, jarri operadorearekin harremanetan.</item>
+      <item quantity="one">Desgaitu egin da SIM txartela. Aurrera egiteko, idatzi PUK kodea. <xliff:g id="_NUMBER_0">%d</xliff:g> saiakera geratzen zaizu SIM txartela betiko erabilgaitz geratu aurretik. Xehetasunak lortzeko, jarri operadorearekin harremanetan.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Lehenetsia"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Puxikak"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogikoa"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Aurrera egiteko, desblokeatu gailua"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fa/strings.xml b/packages/SystemUI/res-keyguard/values-fa/strings.xml
index 6faaed5..9d77608 100644
--- a/packages/SystemUI/res-keyguard/values-fa/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fa/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"کارت نامعتبر"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"شارژ کامل شد"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ بی‌سیم"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ شدن"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • پایه شارژ"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ شدن"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ سریع"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • آهسته‌آهسته شارژ می‌شود"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • برای محافظت از باتری، شارژ بهینه می‌شود"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • شارژ موقتاً محدود شده است"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"برای باز کردن قفل روی «منو» فشار دهید."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"شبکه قفل شد"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"سیم‌کارتی وجود ندارد"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"سیم‌کارت اضافه کنید."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"سیم‌کارت موجود نیست یا قابل‌خواندن نیست. سیم‌کارت اضافه کنید."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"سیم‌کارت قابل‌استفاده نیست."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"سیم‌کارت شما برای همیشه غیرفعال شده است.\n برای دریافت سیم‌کارتی دیگر، با رساننده خدمات بی‌سیم خود تماس بگیرید."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"سیم‌کارت قفل است."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"‏سیم‌کارت با کد PUK قفل شده است."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"درحال باز کردن قفل سیم‌کارت…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"سیم‌کارت موجود نیست"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"سیم‌کارت را وارد کنید."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"سیم‌کارت موجود نیست یا قابل خواندن نیست. یک سیم‌کارت وارد کنید."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"سیم‌کارت غیرقابل استفاده است."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"‏سیم‌کارت شما به‌طور دائم غیرفعال شده است. \nبرای داشتن سیم‌کارت دیگر با ارائه‎دهنده سرویس بی‎سیم خود تماس بگیرید."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"سیم‌کارت قفل شد."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"‏سیم‌کارت با PUK قفل شده است."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"درحال باز کردن قفل سیم‌کارت..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"قسمت پین"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"گذرواژه دستگاه"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"قسمت پین سیم‌کارت"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"‏اکنون سیم‌کارت «<xliff:g id="CARRIER">%1$s</xliff:g>» غیرفعال شده است. برای ادامه دادن، کد PUK را وارد کنید. برای اطلاع از جزئیات با شرکت مخابراتی تماس بگیرید."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"کد پین دلخواه را وارد کنید"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"تأیید کد پین دلخواه"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"درحال باز کردن قفل سیم‌کارت…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"درحال باز کردن قفل سیم‌کارت..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"یک پین ۴ تا ۸ رقمی را تایپ کنید."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"کد پین باید ۸ عدد یا بیشتر باشد."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"پین خود را <xliff:g id="NUMBER_0">%1$d</xliff:g> بار اشتباه تایپ کردید. \n\nپس از <xliff:g id="NUMBER_1">%2$d</xliff:g> ثانیه دوباره امتحان کنید."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"گذرواژه خود را <xliff:g id="NUMBER_0">%1$d</xliff:g> بار اشتباه تایپ کردید. \n\nپس از <xliff:g id="NUMBER_1">%2$d</xliff:g> ثانیه دوباره امتحان کنید."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"الگوی بازگشایی قفل را <xliff:g id="NUMBER_0">%1$d</xliff:g> بار اشتباه کشیده‌اید. \n\nلطفاً پس‌از <xliff:g id="NUMBER_1">%2$d</xliff:g> ثانیه دوباره امتحان کنید."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"کد پین سیم‌کارت اشتباه است، اکنون برای باز کردن قفل دستگاهتان باید با شرکت مخابراتی تماس بگیرید."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{کد پین سیم‌کارت اشتباه است. # فرصت دیگر باقی مانده است و پس‌از آن برای باز کردن قفل دستگاه باید با شرکت مخابراتی‌تان تماس بگیرید.}one{کد پین سیم‌کارت اشتباه است، # فرصت دیگر دارید. }other{کد پین سیم‌کارت اشتباه است، # فرصت دیگر دارید. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">کد پین سیم‌کارت اشتباه است، <xliff:g id="NUMBER_1">%d</xliff:g> بار دیگر می‌توانید تلاش کنید.</item>
+      <item quantity="other">کد پین سیم‌کارت اشتباه است، <xliff:g id="NUMBER_1">%d</xliff:g> بار دیگر می‌توانید تلاش کنید.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"سیم‌کارت غیرقابل استفاده است. با شرکت مخابراتی‌تان تماس بگیرید."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{‏کد PUK سیم‌کارت اشتباه است، # فرصت دیگر باقی مانده است و پس‌از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود.}one{‏کد PUK سیم‌کارت اشتباه است، # فرصت دیگر باقی مانده است و پس‌از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود.}other{‏کد PUK سیم‌کارت اشتباه است، # فرصت دیگر باقی مانده است و پس‌از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">‏کد PUK سیم‌کارت اشتباه است، <xliff:g id="NUMBER_1">%d</xliff:g> بار دیگر می‌توانید تلاش کنید و پس از آن سیم‌کارت برای همیشه غیرقابل استفاده می‌شود.</item>
+      <item quantity="other">‏کد PUK سیم‌کارت اشتباه است، <xliff:g id="NUMBER_1">%d</xliff:g> بار دیگر می‌توانید تلاش کنید و پس از آن سیم‌کارت برای همیشه غیرقابل استفاده می‌شود.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"عملیات پین سیم‌کارت ناموفق بود!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"‏عملیات PUK سیم‌کارت ناموفق بود!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"تغییر روش ورودی"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"بعد از بازنشانی دستگاه باید الگو وارد شود"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"بعد از بازنشانی دستگاه باید پین وارد شود"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"بعد از بازنشانی دستگاه باید گذرواژه وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"برای امنیت بیشتر، به‌جای آن از الگو استفاده کنید"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"برای امنیت بیشتر، به‌جای آن از پین استفاده کنید"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"برای امنیت بیشتر، به‌جای آن از گذرواژه استفاده کنید"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"برای ایمنی بیشتر باید الگو وارد شود"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"برای ایمنی بیشتر باید پین وارد شود"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"برای ایمنی بیشتر باید گذرواژه وارد شود"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"دستگاه توسط سرپرست سیستم قفل شده است"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"دستگاه به‌صورت دستی قفل شده است"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"شناسایی نشد"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"برای استفاده از قفل‌گشایی با چهره، دسترسی دوربین را در تنظیمات روشن کنید"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{پین سیم‌کارت را وارد کنید. # فرصت دیگر باقی مانده است و پس‌از آن برای باز کردن قفل دستگاه باید با شرکت مخابراتی‌تان تماس بگیرید.}one{پین سیم‌کارت را وارد کنید. # فرصت دیگر باقی مانده است.}other{پین سیم‌کارت را وارد کنید. # فرصت دیگر باقی مانده است.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{‏سیم‌کارت اکنون غیرفعال است. برای ادامه دادن، کد PUK را وارد کنید. # فرصت دیگر باقی مانده است و پس‌از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود. برای اطلاع از جزئیات، با شرکت مخابراتی تماس بگیرید.}one{‏سیم‌کارت اکنون غیرفعال است. برای ادامه دادن، کد PUK را وارد کنید. # فرصت دیگر باقی مانده است و پس‌از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود. برای اطلاع از جزئیات، با شرکت مخابراتی تماس بگیرید.}other{‏سیم‌کارت اکنون غیرفعال است. برای ادامه دادن، کد PUK را وارد کنید. # فرصت دیگر باقی مانده است و پس‌از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود. برای اطلاع از جزئیات، با شرکت مخابراتی تماس بگیرید.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"‏برای استفاده از «قفل‌گشایی با چهره»، "<b>"دسترسی به دوربین"</b>" را در «تنظیمات &gt; حریم‌خصوصی» روشن کنید"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">پین سیم‌کارت را وارد کنید. <xliff:g id="NUMBER_1">%d</xliff:g> تلاش دیگری باقی مانده است.</item>
+      <item quantity="other">پین سیم‌کارت را وارد کنید. <xliff:g id="NUMBER_1">%d</xliff:g> تلاش دیگری باقی مانده است.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">‏سیم‌کارت اکنون غیرفعال است. برای ادامه دادن کد PUK را وارد کنید. <xliff:g id="_NUMBER_1">%d</xliff:g> تلاش دیگر باقی مانده است و پس از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود. برای اطلاع از جزئیات با شرکت مخابراتی تماس بگیرید.</item>
+      <item quantity="other">‏سیم‌کارت اکنون غیرفعال است. برای ادامه دادن کد PUK را وارد کنید. <xliff:g id="_NUMBER_1">%d</xliff:g> تلاش دیگر باقی مانده است و پس از آن سیم‌کارت برای همیشه غیرقابل‌استفاده می‌شود. برای اطلاع از جزئیات با شرکت مخابراتی تماس بگیرید.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"پیش‌فرض"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"حباب"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"آنالوگ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"برای ادامه، قفل دستگاهتان را باز کنید"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fi/strings.xml b/packages/SystemUI/res-keyguard/values-fi/strings.xml
index 0c52507..2fbb8ab 100644
--- a/packages/SystemUI/res-keyguard/values-fi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fi/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Virheellinen kortti"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Ladattu"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladataan langattomasti"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladataan"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladataan telineellä"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladataan"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladataan nopeasti"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladataan hitaasti"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lataus optimoitu akun suojaamiseksi"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lataamista rajoitettu väliaikaisesti"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Poista lukitus painamalla Valikkoa."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Verkko lukittu"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Ei SIM-korttia"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Lisää SIM-kortti."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-korttia ei löydy tai ei voi lukea. Lisää SIM-kortti."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-korttia ei voi käyttää."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Sim-kortti on poistettu käytöstä pysyvästi.\n Ota yhteyttä langattoman palvelun tarjoajaan ja pyydä uusi SIM-kortti."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-kortti on lukittu."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-kortti on lukittu PUK-koodilla."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-kortin lukitusta avataan…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Ei SIM-korttia"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Aseta SIM-kortti."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-korttia ei löydy tai ei voi lukea. Aseta SIM-kortti."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM-kortti ei käytettävissä"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-kortti on poistettu pysyvästi käytöstä.\n Ota yhteyttä operaattoriisi ja hanki uusi SIM-kortti."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kortti on lukittu."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kortti on PUK-lukittu."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM-kortin lukitusta avataan…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-koodin alue"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Laitteen salasana"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM-kortin PIN-koodin alue"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Operaattorin <xliff:g id="CARRIER">%1$s</xliff:g> SIM-kortti on nyt lukittu. Jatka antamalla PUK-koodi. Saat lisätietoja operaattoriltasi."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Anna haluamasi PIN-koodi."</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Vahvista haluamasi PIN-koodi."</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-kortin lukitusta avataan…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM-kortin lukitusta avataan…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Anna 4–8-numeroinen PIN-koodi."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-koodissa tulee olla vähintään 8 numeroa."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Olet kirjoittanut PIN-koodin väärin <xliff:g id="NUMBER_0">%1$d</xliff:g> kertaa. \n\nYritä uudelleen <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunnin kuluttua."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Olet kirjoittanut salasanan väärin <xliff:g id="NUMBER_0">%1$d</xliff:g> kertaa. \n\nYritä uudelleen <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunnin kuluttua."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Olet piirtänyt lukituksenpoistokuvion väärin <xliff:g id="NUMBER_0">%1$d</xliff:g> kertaa. \n\nYritä uudelleen <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunnin kuluttua."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Virheellinen SIM-kortin PIN-koodi. Sinun on nyt otettava yhteys operaattoriin laitteen lukituksen avaamiseksi."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Virheellinen SIM-kortin PIN-koodi. Sinulla on # yritys jäljellä, ennen kuin sinun on otettava yhteys operaattoriin laitteen lukituksen avaamiseksi.}other{Virheellinen SIM-kortin PIN-koodi. Sinulla on # yritystä jäljellä. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Virheellinen SIM-kortin PIN-koodi. Sinulla on <xliff:g id="NUMBER_1">%d</xliff:g> yritystä jäljellä.</item>
+      <item quantity="one">Virheellinen SIM-kortin PIN-koodi. Sinulla on <xliff:g id="NUMBER_0">%d</xliff:g> yritys jäljellä, ennen kuin sinun on otettava yhteys operaattoriin laitteen lukituksen avaamiseksi.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-korttia ei voi käyttää. Ota yhteys operaattoriin."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Virheellinen SIM-kortin PUK-koodi. Sinulla on # yritys jäljellä, ennen kuin SIM-kortti poistuu käytöstä pysyvästi.}other{Virheellinen SIM-kortin PUK-koodi. Sinulla on # yritystä jäljellä, ennen kuin SIM-kortti poistuu käytöstä pysyvästi.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Virheellinen SIM-kortin PUK-koodi. Sinulla on <xliff:g id="NUMBER_1">%d</xliff:g> yritystä jäljellä, ennen kuin SIM-kortista tulee pysyvästi käyttökelvoton.</item>
+      <item quantity="one">Virheellinen SIM-kortin PUK-koodi. Sinulla on <xliff:g id="NUMBER_0">%d</xliff:g> yritys jäljellä, ennen kuin SIM-kortista tulee pysyvästi käyttökelvoton.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM-kortin PIN-toiminto epäonnistui."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM-kortin PUK-toiminto epäonnistui."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Vaihda syöttötapaa."</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kuvio vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-koodi vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Salasana vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Lisäsuojaa saat, kun käytät sen sijaan kuviota"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Lisäsuojaa saat, kun käytät sen sijaan PIN-koodia"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Lisäsuojaa saat, kun käytät sen sijaan salasanaa"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kuvio vaaditaan suojauksen parantamiseksi."</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-koodi vaaditaan suojauksen parantamiseksi."</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Salasana vaaditaan suojauksen parantamiseksi."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Järjestelmänvalvoja lukitsi laitteen."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Laite lukittiin manuaalisesti"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tunnistettu"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Kasvojentunnistusavaus: Asetukset &gt; pääsy kameraan"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Lisää SIM-kortin PIN-koodi. Sinulla on # yritys jäljellä, ennen kuin sinun on otettava yhteys operaattoriin laitteen lukituksen avaamiseksi.}other{Lisää SIM-kortin PIN-koodi. # yritystä jäljellä.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-kortti on nyt lukittu. Lisää PUK-koodi, niin voit jatkaa. Sinulla on # yritys jäljellä, ennen kuin SIM-kortti poistuu käytöstä pysyvästi. Pyydä lisätietoa operaattoriltasi.}other{SIM-kortti on nyt lukittu. Lisää PUK-koodi, niin voit jatkaa. Sinulla on # yritystä jäljellä, ennen kuin SIM-kortti poistuu käytöstä pysyvästi. Pyydä lisätietoa operaattoriltasi.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Jos haluat käyttää kasvojentunnistusavausta, valitse Asetukset &gt; Yksityisyys ja laita "<b>"Pääsy kameraan"</b>" päälle"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Anna SIM-kortin PIN-koodi. Sinulla on <xliff:g id="NUMBER_1">%d</xliff:g> yritystä jäljellä.</item>
+      <item quantity="one">Anna SIM-kortin PIN-koodi. <xliff:g id="NUMBER_0">%d</xliff:g> yrityksen jälkeen laite lukittuu, ja vain operaattori voi avata sen.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM-kortti on nyt lukittu. Anna PUK-koodi, niin voit jatkaa. Sinulla on <xliff:g id="_NUMBER_1">%d</xliff:g> yritystä jäljellä, ennen kuin SIM-kortti poistuu pysyvästi käytöstä. Pyydä lisätietoja operaattoriltasi.</item>
+      <item quantity="one">SIM-kortti on nyt lukittu. Anna PUK-koodi, niin voit jatkaa. Sinulla on <xliff:g id="_NUMBER_0">%d</xliff:g> yritys jäljellä, ennen kuin SIM-kortti poistuu pysyvästi käytöstä. Pyydä lisätietoja operaattoriltasi.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Oletus"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Kupla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoginen"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Jatka avaamalla laitteen lukitus"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
index 6ac8f4c..da81aa3 100644
--- a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Cette carte n\'est pas valide."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Chargé"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • En recharge sans fil"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge en cours…"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Station de recharge"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"En recharge : <xliff:g id="PERCENTAGE">%s</xliff:g>"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"En recharge rapide : <xliff:g id="PERCENTAGE">%s</xliff:g>"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"En recharge lente : <xliff:g id="PERCENTAGE">%s</xliff:g>"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge optimisée pour protéger la pile"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge temporairement limitée"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Appuyez sur la touche Menu pour déverrouiller l\'appareil."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Réseau verrouillé"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Aucune carte SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Ajouter une carte SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"La carte SIM est manquante ou illisible. Ajouter une carte SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"La carte SIM est inutilisable."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Votre carte SIM a été désactivée de manière permanente.\n Communiquez avec votre fournisseur de services sans fil pour obtenir une autre carte SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"La carte SIM est verrouillée."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"La carte SIM est verrouillée par clé PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Déverrouillage de la carte SIM en cours…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Aucune carte SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insérez une carte SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Carte SIM absente ou illisible. Veuillez insérer une carte SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Carte SIM inutilisable."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Votre carte SIM a été définitivement désactivée.\n Veuillez communiquer avec votre fournisseur de services pour en obtenir une autre."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"La carte SIM est verrouillée."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"La carte SIM est verrouillée par un code PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Déblocage de la carte SIM en cours…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Zone du NIP"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Mot de passe de l\'appareil"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Zone du NIP de la carte SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Schéma incorrect"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Mot de passe incorrect"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"NIP incorrect"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Réessayez dans # seconde.}one{Réessayez dans # seconde.}many{Réessayez dans # secondes.}other{Réessayez dans # secondes.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Réessayez dans # seconde.}one{Réessayez dans # seconde.}other{Réessayez dans # secondes.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Entrez le NIP de la carte SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Entrez le NIP de la carte SIM pour « <xliff:g id="CARRIER">%1$s</xliff:g> »."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Désactivez la carte eSIM pour utiliser l\'appareil sans service cellulaire."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Le carte SIM « <xliff:g id="CARRIER">%1$s</xliff:g> » est maintenant désactivée. Entrez le code PUK pour continuer. Pour obtenir plus de détails, communiquez avec votre fournisseur de services."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Entrez le NIP que vous souhaitez utiliser"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirmez le NIP que vous souhaitez utiliser"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Déverrouillage de la carte SIM en cours…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Déblocage de la carte SIM en cours…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Veuillez entrer un NIP comprenant entre quatre et huit chiffres."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Le code PUK doit contenir au moins 8 chiffres."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Vous avez entré un NIP incorrect à <xliff:g id="NUMBER_0">%1$d</xliff:g> reprises. \n\nVeuillez réessayer dans <xliff:g id="NUMBER_1">%2$d</xliff:g> secondes."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Vous avez entré un mot de passe incorrect à <xliff:g id="NUMBER_0">%1$d</xliff:g> reprises.\n\nVeuillez réessayer dans <xliff:g id="NUMBER_1">%2$d</xliff:g> secondes."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Vous avez dessiné un schéma de déverrouillage incorrect à <xliff:g id="NUMBER_0">%1$d</xliff:g> reprises.\n\nVeuillez réessayer dans <xliff:g id="NUMBER_1">%2$d</xliff:g> secondes."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"NIP de carte SIM incorrect. Vous devez maintenant communiquer avec votre fournisseur de services pour déverrouiller votre appareil."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{NIP de la carte SIM incorrect. Il vous reste # tentative. Après cela, vous devrez communiquer avec votre fournisseur de services pour déverrouiller votre appareil.}one{NIP de la carte SIM incorrect. Il vous reste # tentative. }many{NIP de la carte SIM incorrect. Il vous reste # de tentatives. }other{NIP de la carte SIM incorrect. Il vous reste # tentatives. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Le NIP de la carte SIM incorrect. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentative.</item>
+      <item quantity="other">Le NIP de la carte SIM incorrect. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentatives.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"La carte SIM est inutilisable. Communiquez avec votre fournisseur de services."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Code PUK de la carte SIM incorrect. Il vous reste # tentative avant que votre carte SIM devienne définitivement inutilisable.}one{Code PUK de la carte SIM incorrect. Il vous reste # tentative avant que votre carte SIM devienne définitivement inutilisable.}many{Code PUK de la carte SIM incorrect. Il vous reste # de tentatives avant que votre carte SIM devienne définitivement inutilisable.}other{Code PUK de la carte SIM incorrect. Il vous reste # tentatives avant que votre carte SIM devienne définitivement inutilisable.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Le code PUK de la carte SIM est incorrect. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentative avant que votre carte SIM devienne définitivement inutilisable.</item>
+      <item quantity="other">Le code PUK de la carte SIM est incorrect. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentatives avant que votre carte SIM devienne définitivement inutilisable.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Le déverrouillage par NIP de la carte SIM a échoué."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Le déverrouillage de la carte SIM par code PUK a échoué."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Changer de méthode d\'entrée"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Le schéma est exigé après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Le NIP est exigé après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Le mot de passe est exigé après le redémarrage de l\'appareil"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pour plus de sécurité, utilisez plutôt un schéma"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pour plus de sécurité, utilisez plutôt un NIP"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pour plus de sécurité, utilisez plutôt un mot de passe"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Le schéma est exigé pour plus de sécurité"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Le NIP est exigé pour plus de sécurité"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Le mot de passe est exigé pour plus de sécurité"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'appareil a été verrouillé par l\'administrateur"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"L\'appareil a été verrouillé manuellement"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Doigt non reconnu"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Déverr. rec. faciale : activez accès app. photo dans Param."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Entrez le NIP de la carte SIM. Il vous reste # tentative. Après cela, vous devrez communiquer avec votre fournisseur de services pour déverrouiller votre appareil.}one{Entrez le NIP de la carte SIM. Il vous reste # tentative.}many{Entrez le NIP de la carte SIM. Il vous reste # de tentatives.}other{Entrez le NIP de la carte SIM. Il vous reste # tentatives.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{La carte SIM est maintenant désactivée. Entrez le code PUK pour continuer. Il vous reste # tentative avant que votre carte SIM devienne définitivement inutilisable. Communiquez avec votre fournisseur de services pour en savoir plus.}one{La carte SIM est maintenant désactivée. Entrez le code PUK pour continuer. Il vous reste # tentative avant que votre carte SIM devienne définitivement inutilisable. Communiquez avec votre fournisseur de services pour en savoir plus.}many{La carte SIM est maintenant désactivée. Entrez le code PUK pour continuer. Il vous reste # de tentatives avant que votre carte SIM devienne définitivement inutilisable. Communiquez avec votre fournisseur de services pour en savoir plus.}other{La carte SIM est maintenant désactivée. Entrez le code PUK pour continuer. Il vous reste # tentatives avant que votre carte SIM devienne définitivement inutilisable. Communiquez avec votre fournisseur de services pour en savoir plus.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Pour utiliser le déverrouillage par reconnaissance faciale, activez l\'"<b>"accès à l\'appareil photo"</b>" dans Paramètres &gt; Confidentialité"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Entrez le NIP de votre carte SIM. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentative.</item>
+      <item quantity="other">Entrez le NIP de votre carte SIM. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentatives.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">La carte SIM est maintenant désactivée. Entrez le code PUK pour continuer. Il vous reste <xliff:g id="_NUMBER_1">%d</xliff:g> tentative avant que votre carte SIM devienne définitivement inutilisable. Pour obtenir plus de détails, communiquez avec votre fournisseur de services.</item>
+      <item quantity="other">La carte SIM est maintenant désactivée. Entrez le code PUK pour continuer. Il vous reste <xliff:g id="_NUMBER_1">%d</xliff:g> tentatives avant que votre carte SIM devienne définitivement inutilisable. Pour obtenir plus de détails, communiquez avec votre fournisseur de services.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Déverrouiller votre appareil pour continuer"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fr/strings.xml b/packages/SystemUI/res-keyguard/values-fr/strings.xml
index 8313e6a..b34ae8c 100644
--- a/packages/SystemUI/res-keyguard/values-fr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fr/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Carte non valide."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Chargé"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • En charge sans fil"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Station de charge"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge…"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge rapide…"</string>
-    <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge lente"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge optimisée pour protéger la batterie"</string>
+    <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge lente…"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Recharge momentanément limitée"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Appuyez sur \"Menu\" pour déverrouiller le clavier."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Réseau verrouillé"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Aucune SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Ajoutez une SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"La SIM est absente ou illisible. Ajoutez une SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM inutilisable."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Votre SIM a été désactivée définitivement.\n Contactez votre opérateur de téléphonie mobile pour en obtenir une autre."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM verrouillée."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM verrouillée par clé PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Déblocage de la SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Pas de carte SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insérez une carte SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Carte SIM absente ou illisible. Insérez une carte SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"La carte SIM est inutilisable."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Votre carte SIM a été définitivement désactivée.\nVeuillez contacter votre opérateur de téléphonie mobile pour en obtenir une autre."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"La carte SIM est verrouillée."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"La carte SIM est verrouillée par clé PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Déblocage de la carte SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Champ du code"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Mot de passe de l\'appareil"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Champ du code PIN de la carte SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Schéma incorrect"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Mot de passe incorrect"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"Code incorrect"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Réessayez dans # seconde.}one{Réessayez dans # seconde.}many{Réessayez dans # secondes.}other{Réessayez dans # secondes.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Réessayez dans # seconde.}one{Réessayez dans # seconde.}other{Réessayez dans # secondes.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Saisissez le code PIN de la carte SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Saisissez le code PIN de la carte SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Désactivez la carte eSIM pour utiliser l\'appareil sans service mobile."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"La carte SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" est maintenant désactivée. Pour continuer, saisissez la clé PUK. Contactez votre opérateur pour en savoir plus."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Saisissez le code PIN de votre choix"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirmez le code PIN souhaité"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Déblocage de la SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Déblocage de la carte SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Saisissez un code PIN comprenant 4 à 8 chiffres."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"La clé PUK doit contenir au moins 8 chiffres."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Vous avez saisi un code incorrect à <xliff:g id="NUMBER_0">%1$d</xliff:g> reprises.\n\nRéessayez dans <xliff:g id="NUMBER_1">%2$d</xliff:g> secondes."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Vous avez saisi un mot de passe incorrect à <xliff:g id="NUMBER_0">%1$d</xliff:g> reprises.\n\nRéessayez dans <xliff:g id="NUMBER_1">%2$d</xliff:g> secondes."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Vous avez dessiné un schéma de déverrouillage incorrect à <xliff:g id="NUMBER_0">%1$d</xliff:g> reprises.\n\nRéessayez dans <xliff:g id="NUMBER_1">%2$d</xliff:g> secondes."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Code PIN de la carte SIM incorrect. Vous devez désormais contacter votre opérateur pour déverrouiller votre appareil."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Code PIN SIM incorrect. Il vous reste # tentative avant de devoir contacter votre opérateur pour déverrouiller l\'appareil.}one{Code PIN SIM incorrect. Il vous reste # tentative. }many{Code PIN SIM incorrect. Il vous reste # tentatives. }other{Code PIN SIM incorrect. Il vous reste # tentatives. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Code PIN de la carte SIM incorrect. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentative.</item>
+      <item quantity="other">Code PIN de la carte SIM incorrect. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentatives.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"La carte SIM est inutilisable. Contactez votre opérateur."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Clé PUK de la SIM incorrecte. Il vous reste # tentative avant que la SIM ne devienne définitivement inutilisable.}one{Clé PUK de la SIM incorrecte. Il vous reste # tentative avant que la carte SIM ne devienne définitivement inutilisable.}many{Clé PUK de la SIM incorrecte. Il vous reste # tentatives avant que la carte SIM ne devienne définitivement inutilisable.}other{Clé PUK de la SIM incorrecte. Il vous reste # tentatives avant que la carte SIM ne devienne définitivement inutilisable.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Clé PUK de la carte SIM incorrecte. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentative avant que votre carte SIM ne devienne définitivement inutilisable.</item>
+      <item quantity="other">Clé PUK de la carte SIM incorrecte. Il vous reste <xliff:g id="NUMBER_1">%d</xliff:g> tentatives avant que votre carte SIM ne devienne définitivement inutilisable.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Échec du déverrouillage à l\'aide du code PIN de la carte SIM."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Échec du déverrouillage à l\'aide de la clé PUK de la carte SIM."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Changer le mode de saisie"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Veuillez dessiner le schéma après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Veuillez saisir le code après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Veuillez saisir le mot de passe après le redémarrage de l\'appareil"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pour plus de sécurité, utilisez plutôt un schéma"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pour plus de sécurité, utilisez plutôt un code"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pour plus de sécurité, utilisez plutôt un mot de passe"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Veuillez dessiner le schéma pour renforcer la sécurité"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Veuillez saisir le code pour renforcer la sécurité"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Veuillez saisir le mot de passe pour renforcer la sécurité"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Appareil verrouillé par l\'administrateur"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Appareil verrouillé manuellement"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non reconnu"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Pour le déverrouillage par reconnaissance faciale, activez l\'accès à l\'app. photo dans Paramètres"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Saisissez le code PIN SIM. Il vous reste # tentative avant de devoir contacter votre opérateur pour déverrouiller l\'appareil.}one{Saisissez le code PIN SIM. Il vous reste # tentative.}many{Saisissez le code PIN SIM. Il vous reste # tentatives.}other{Saisissez le code PIN SIM. Il vous reste # tentatives.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{La SIM est maintenant désactivée. Saisissez la clé PUK pour continuer. Il vous reste # tentative avant que la SIM ne devienne définitivement inutilisable. Pour plus d\'infos, contactez votre opérateur.}one{La SIM est maintenant désactivée. Saisissez la clé PUK pour continuer. Il vous reste # tentative avant que la SIM ne devienne définitivement inutilisable. Pour plus d\'infos, contactez votre opérateur.}many{La SIM est maintenant désactivée. Saisissez la clé PUK pour continuer. Il vous reste # tentatives avant que la SIM ne devienne définitivement inutilisable. Pour plus d\'infos, contactez votre opérateur.}other{La SIM est maintenant désactivée. Saisissez la clé PUK pour continuer. Il vous reste # tentatives avant que la SIM ne devienne définitivement inutilisable. Pour plus d\'infos, contactez votre opérateur.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Pour utiliser Face Unlock, activez "<b>"Accès à l\'appareil photo"</b>" dans Paramètres &gt; Confidentialité"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Saisissez le code de la carte SIM. <xliff:g id="NUMBER_1">%d</xliff:g> tentative restante.</item>
+      <item quantity="other">Saisissez le code de la carte SIM. <xliff:g id="NUMBER_1">%d</xliff:g> tentatives restantes.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">La carte SIM est maintenant désactivée. Saisissez le code PUK pour continuer. Il vous reste <xliff:g id="_NUMBER_1">%d</xliff:g> tentative avant que votre carte SIM ne devienne définitivement inutilisable. Pour de plus amples informations, veuillez contacter votre opérateur.</item>
+      <item quantity="other">La carte SIM est maintenant désactivée. Saisissez le code PUK pour continuer. Il vous reste <xliff:g id="_NUMBER_1">%d</xliff:g> tentatives avant que votre carte SIM ne devienne définitivement inutilisable. Pour de plus amples informations, veuillez contacter votre opérateur.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Déverrouillez votre appareil pour continuer"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-gl/strings.xml b/packages/SystemUI/res-keyguard/values-gl/strings.xml
index 5bed7198..b1c14fe5 100644
--- a/packages/SystemUI/res-keyguard/values-gl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-gl/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"A tarxeta non é válida."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Cargado"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando sen fíos"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Base de carga"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando rapidamente"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Cargando lentamente"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carga optimizada para protexer a batería"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carga limitada temporalmente"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Preme Menú para desbloquear."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Bloqueada pola rede"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Non hai ningunha SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Engade unha SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"A SIM falta ou non se pode ler. Engade unha."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"A SIM non se pode usar."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"A SIM desactivouse permanentemente.\n Ponte en contacto co teu fornecedor de servizos sen fíos para conseguir outra."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"A SIM está bloqueada."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"A SIM está bloqueada mediante PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Desbloqueando SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Sen tarxeta SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insire unha tarxeta SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Falta a tarxeta SIM ou non se pode ler. Insire unha tarxeta SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Tarxeta SIM inutilizable"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"A túa tarxeta SIM desactivouse permanentemente.\n Ponte en contacto co fornecedor de servizo sen fíos para conseguir outra tarxeta SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"A tarxeta SIM está bloqueada."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"A tarxeta SIM está bloqueada con código PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Desbloqueando tarxeta SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Área do PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Contrasinal do dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Área do PIN da tarxeta SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Agora a SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" está desactivada. Introduce o código PUK para continuar. Ponte en contacto co operador para obter máis información."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Introduce o código PIN desexado"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirma o código PIN desexado"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Desbloqueando SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Desbloqueando tarxeta SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Escribe un PIN que teña entre 4 e 8 números."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"O código PUK debe ter 8 números como mínimo."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Introduciches o PIN incorrectamente <xliff:g id="NUMBER_0">%1$d</xliff:g> veces. \n\nTéntao de novo en <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Introduciches o contrasinal incorrectamente <xliff:g id="NUMBER_0">%1$d</xliff:g> veces. \n\nTéntao de novo en <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Debuxaches incorrectamente o padrón de desbloqueo <xliff:g id="NUMBER_0">%1$d</xliff:g> veces. \n\nTéntao de novo en <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"O código PIN da SIM non é correcto. Agora debes contactar co operador para desbloquear o dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{O código PIN da SIM é incorrecto. Quédache # intento antes de que teñas que contactar co operador para desbloquear o dispositivo.}other{O código PIN da SIM é incorrecto. Quédanche # intentos. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">O código PIN da SIM é incorrecto. Quédanche <xliff:g id="NUMBER_1">%d</xliff:g> intentos.</item>
+      <item quantity="one">O código PIN da SIM é incorrecto. Quédache <xliff:g id="NUMBER_0">%d</xliff:g> intento antes de que teñas que contactar co operador para desbloquear o dispositivo.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"A SIM está inutilizable. Contacta co operador."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{O código PUK da SIM é incorrecto. Quédache # intento antes de que a SIM quede inutilizable para sempre.}other{O código PUK da SIM é incorrecto. Quédanche # intentos antes de que a SIM quede inutilizable para sempre.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">O código PUK da SIM é incorrecto. Quédanche <xliff:g id="NUMBER_1">%d</xliff:g> intentos antes de que a SIM quede inutilizable para sempre.</item>
+      <item quantity="one">O código PUK da SIM non é correcto. Quédache <xliff:g id="NUMBER_0">%d</xliff:g> intento antes de que a SIM quede inutilizable para sempre.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Produciuse un erro no funcionamento do PIN da SIM"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Produciuse un erro ao tentar desbloquear a tarxeta SIM co código PUK."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Cambia o método de introdución"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necesario o padrón despois do reinicio do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necesario o PIN despois do reinicio do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necesario o contrasinal despois do reinicio do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Utiliza un padrón para obter maior seguranza"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Utiliza un PIN para obter maior seguranza"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Utiliza un contrasinal para obter maior seguranza"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"É necesario o padrón para obter seguranza adicional"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"É necesario o PIN para obter seguranza adicional"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"É necesario o contrasinal para obter seguranza adicional"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"O administrador bloqueou o dispositivo"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo bloqueouse manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non se recoñeceu"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"O Desbloqueo facial necesita acceso á cámara"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Mete o PIN da SIM. Quédache # intento antes de que teñas que contactar co operador para desbloquear o dispositivo.}other{Mete o PIN da SIM. Quédanche # intentos.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{A SIM está desactivada. Mete o código PUK para continuar. Quédache # intento antes de que a SIM quede inutilizable para sempre. Contacta co operador para obter información.}other{A SIM está desactivada. Mete o código PUK para continuar. Quédanche # intentos antes de que a SIM quede inutilizable para sempre. Contacta co operador para obter información.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para usar o desbloqueo facial, activa "<b>"Acceso á cámara"</b>" en Configuración &gt; Privacidade"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Introduce o código PIN da SIM. Quédanche <xliff:g id="NUMBER_1">%d</xliff:g> intentos.</item>
+      <item quantity="one">Introduce o código PIN da SIM. Quédache <xliff:g id="NUMBER_0">%d</xliff:g> intento antes de que teñas que contactar co operador para desbloquear o dispositivo.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">A SIM está desactivada. Introduce o código PUK para continuar. Quédanche <xliff:g id="_NUMBER_1">%d</xliff:g> intentos antes de que a SIM quede inutilizable para sempre. Contacta co operador para obter información.</item>
+      <item quantity="one">A SIM está desactivada. Introduce o código PUK para continuar. Quédache <xliff:g id="_NUMBER_0">%d</xliff:g> intento antes de que a SIM quede inutilizable para sempre. Contacta co operador para obter información.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbulla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analóxico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloquea o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-gu/strings.xml b/packages/SystemUI/res-keyguard/values-gu/strings.xml
index fd93ba0..5dc4a1c 100644
--- a/packages/SystemUI/res-keyguard/values-gu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-gu/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"અમાન્ય કાર્ડ."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ચાર્જ થઈ ગયું"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • વાયરલેસથી ચાર્જિંગ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ચાર્જિંગ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ડૉકથી ચાર્જ થઈ રહ્યું છે"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ચાર્જિંગ"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ઝડપથી ચાર્જિંગ"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ધીમેથી ચાર્જિંગ"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • બૅટરીની સુરક્ષા કરવા માટે, ચાર્જિંગ ઑપ્ટિમાઇઝ કરવામાં આવ્યું છે"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ચાર્જ કરવાનું થોડા સમય માટે મર્યાદિત કરવામાં આવ્યું છે"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"અનલૉક કરવા માટે મેનૂ દબાવો."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"નેટવર્ક લૉક થયું"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"કોઈ સિમ કાર્ડ નથી"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"સિમ કાર્ડ ઉમેરો."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"સિમ કાર્ડ ખૂટે છે અથવા વાંચી શકાય એવું નથી. સિમ કાર્ડ ઉમેરો."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ઉપયોગમાં ન લઈ શકાતું સિમ કાર્ડ."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"તમારું સિમ કાર્ડ કાયમ માટે નિષ્ક્રિય કરવામાં આવ્યું છે.\n બીજા સિમ કાર્ડ માટે તમારા વાયરલેસ સેવા પ્રદાતાનો સંપર્ક કરો."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"સિમ કાર્ડ લૉક કરેલું છે."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"સિમ કાર્ડ PUK-લૉક કરેલું છે."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"સિમ કાર્ડ અનલૉક કરી રહ્યાં છીએ…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"કોઈ સિમ કાર્ડ નથી"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"એક સિમ કાર્ડ દાખલ કરો."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"સિમ કાર્ડ ખૂટે છે અથવા વાંચન યોગ્ય નથી. સિમ કાર્ડ દાખલ કરો."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"બિનઉપયોગી સિમ કાર્ડ."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"તમારો સિમ કાર્ડ કાયમી રૂપે અક્ષમ કરવામાં આવ્યો છે.\n બીજા સિમ કાર્ડ માટે તમારા વાયરલેસ સેવા પ્રદાતાનો સંપર્ક કરો."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"સિમ કાર્ડ લૉક કરેલ છે."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"સિમ કાર્ડ, PUK-લૉક કરેલ છે."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"સિમ કાર્ડ અનલૉક કરી રહ્યાં છીએ…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"પિન ક્ષેત્ર"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ઉપકરણનો પાસવર્ડ"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"સિમ પિન ક્ષેત્ર"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"સિમ \"<xliff:g id="CARRIER">%1$s</xliff:g>\" હમણાં અક્ષમ કરેલ છે. ચાલુ રાખવા માટે PUK કોડ દાખલ કરો. વિગતો માટે કૅરિઅરનો સંપર્ક કરો."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"જોઈતો પિન કોડ દાખલ કરો"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"જોઈતા પિન કોડની પુષ્ટિ કરો"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"સિમ કાર્ડ અનલૉક કરી રહ્યાં છીએ…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"સિમ કાર્ડ અનલૉક કરી રહ્યાં છીએ…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 થી 8 સંખ્યાનો હોય તેવો એક પિન લખો."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK કોડ 8 કે તેનાથી વધુ સંખ્યાનો હોવો જોઈએ."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"તમારો પિન તમે <xliff:g id="NUMBER_0">%1$d</xliff:g> વખત ખોટી રીતે લખ્યો છે. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> સેકન્ડમાં ફરીથી પ્રયાસ કરો."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"તમારો પાસવર્ડ તમે <xliff:g id="NUMBER_0">%1$d</xliff:g> વખત ખોટી રીતે લખ્યો છે. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> સેકંડમાં ફરીથી પ્રયાસ કરો."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"તમારી અનલૉક પૅટર્ન તમે <xliff:g id="NUMBER_0">%1$d</xliff:g> વખત ખોટી રીતે દોરી છે. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> સેકન્ડમાં ફરીથી પ્રયાસ કરો."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ખોટો સિમ પિન કોડ, તમારે હવે તમારું ઉપકરણ અનલૉક કરવા માટે તમારા કૅરીઅરનો સંપર્ક કરવો આવશ્યક છે."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{સિમ કાર્ડનો ખોટો પિન કોડ, તમને તમારું ડિવાઇસ અનલૉક કરવા માટે તમારા મોબાઇલ ઑપરેટરનો સંપર્ક કરવાની જરૂર પડે, તે પહેલાં તમારી પાસે # પ્રયાસ બાકી છે.}one{સિમ કાર્ડનો ખોટો પિન કોડ, તમારી પાસે # પ્રયાસ બાકી છે. }other{સિમ કાર્ડનો ખોટો પિન કોડ, તમારી પાસે # પ્રયાસ બાકી છે. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">ખોટો સિમ પિન કોડ, તમારી પાસે <xliff:g id="NUMBER_1">%d</xliff:g> પ્રયાસ બાકી છે.</item>
+      <item quantity="other">ખોટો સિમ પિન કોડ, તમારી પાસે <xliff:g id="NUMBER_1">%d</xliff:g> પ્રયાસ બાકી છે.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"સિમ અનુપયોગી છે. તમારા કૅરિઅરનો સંપર્ક કરો."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{સિમ કાર્ડનો ખોટો PUK કોડ, સિમ કાર્ડ કાયમ માટે બિનઉપયોગી બની જાય, એ પહેલાં તમારી પાસે # પ્રયાસ બાકી છે.}one{સિમ કાર્ડનો ખોટો PUK કોડ, સિમ કાર્ડ કાયમ માટે બિનઉપયોગી બની જાય, એ પહેલાં તમારી પાસે # પ્રયાસ બાકી છે.}other{સિમ કાર્ડનો ખોટો PUK કોડ, સિમ કાર્ડ કાયમ માટે બિનઉપયોગી બની જાય, એ પહેલાં તમારી પાસે # પ્રયાસ બાકી છે.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">ખોટો સિમ PUK કોડ, સિમ કાયમી રૂપે અનુપયોગી બની જાય તે પહેલા તમારી પાસે <xliff:g id="NUMBER_1">%d</xliff:g> પ્રયાસ બાકી છે.</item>
+      <item quantity="other">ખોટો સિમ PUK કોડ, સિમ કાયમી રૂપે અનુપયોગી બની જાય તે પહેલા તમારી પાસે <xliff:g id="NUMBER_1">%d</xliff:g> પ્રયાસ બાકી છે.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"સિમ પિન ઑપરેશન નિષ્ફળ થયું!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"સિમ PUK ઓપરેશન નિષ્ફળ થયું!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ઇનપુટ પદ્ધતિ સ્વિચ કરો"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પૅટર્ન જરૂરી છે"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પિન જરૂરી છે"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પાસવર્ડ જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"વધારાની સુરક્ષા માટે, તેના બદલે પૅટર્નનો ઉપયોગ કરો"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"વધારાની સુરક્ષા માટે, તેના બદલે પિનનો ઉપયોગ કરો"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"વધારાની સુરક્ષા માટે, તેના બદલે પાસવર્ડનો ઉપયોગ કરો"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"વધારાની સુરક્ષા માટે પૅટર્ન જરૂરી છે"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"વધારાની સુરક્ષા માટે પિન જરૂરી છે"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"વધારાની સુરક્ષા માટે પાસવર્ડ જરૂરી છે"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"વ્યવસ્થાપકે ઉપકરણ લૉક કરેલું છે"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ઉપકરણ મેન્યુઅલી લૉક કર્યું હતું"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ઓળખાયેલ નથી"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ફેસ અનલૉક વાપરવા સેટિંગમાં કૅમેરા ઍક્સેસ ચાલુ કરો"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{સિમ કાર્ડનો પિન દાખલ કરો. તમને તમારું ડિવાઇસ અનલૉક કરવા માટે તમારા મોબાઇલ ઑપરેટરનો સંપર્ક કરવાની જરૂર પડે, તે પહેલાં તમારી પાસે # પ્રયાસ બાકી છે.}one{સિમ કાર્ડનો પિન દાખલ કરો. તમારી પાસે # પ્રયાસ બાકી છે.}other{સિમ કાર્ડનો પિન દાખલ કરો. તમારી પાસે # પ્રયાસ બાકી છે.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{સિમ કાર્ડ બંધ કરેલું છે. ચાલુ રાખવા માટે PUK કોડ દાખલ કરો. સિમ કાર્ડ કાયમ માટે બિનઉપયોગી બની જાય, એ પહેલાં તમારી પાસે # પ્રયાસ બાકી છે. વિગતો માટે મોબાઇલ ઑપરેટરનો સંપર્ક કરો.}one{સિમ કાર્ડ બંધ કરેલું છે. ચાલુ રાખવા માટે PUK કોડ દાખલ કરો. સિમ કાર્ડ કાયમ માટે બિનઉપયોગી બની જાય, એ પહેલાં તમારી પાસે # પ્રયાસ બાકી છે. વિગતો માટે મોબાઇલ ઑપરેટરનો સંપર્ક કરો.}other{સિમ કાર્ડ બંધ કરેલું છે. ચાલુ રાખવા માટે PUK કોડ દાખલ કરો. સિમ કાર્ડ કાયમ માટે બિનઉપયોગી બની જાય, એ પહેલાં તમારી પાસે # પ્રયાસ બાકી છે. વિગતો માટે મોબાઇલ ઑપરેટરનો સંપર્ક કરો.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ફેસ અનલૉક સુવિધાનો ઉપયોગ કરવા માટે, સેટિંગ &gt; પ્રાઇવસીમાં જઈને "<b>"કૅમેરા ઍક્સેસ"</b>" ચાલુ કરો"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">સિમનો પિન દાખલ કરો, તમારી પાસે <xliff:g id="NUMBER_1">%d</xliff:g> પ્રયાસ બાકી છે.</item>
+      <item quantity="other">સિમનો પિન દાખલ કરો, તમારી પાસે <xliff:g id="NUMBER_1">%d</xliff:g> પ્રયાસો બાકી છે.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">સિમ હવે બંધ કરેલ છે. ચાલુ રાખવા માટે PUK કોડ દાખલ કરો. સિમ કાયમીરૂપે બિનઉપયોગી બની જાય એ પહેલાં તમારી પાસે <xliff:g id="_NUMBER_1">%d</xliff:g> પ્રયાસ બાકી છે. વિગતો માટે કૅરિઅરનો સંપર્ક કરો.</item>
+      <item quantity="other">સિમ હવે બંધ કરેલ છે. ચાલુ રાખવા માટે PUK કોડ દાખલ કરો. સિમ કાયમીરૂપે બિનઉપયોગી બની જાય એ પહેલાં તમારી પાસે <xliff:g id="_NUMBER_1">%d</xliff:g> પ્રયાસો બાકી છે. વિગતો માટે કૅરિઅરનો સંપર્ક કરો.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ડિફૉલ્ટ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"બબલ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"એનાલોગ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ચાલુ રાખવા માટે તમારા ડિવાઇસને અનલૉક કરો"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hi/strings.xml b/packages/SystemUI/res-keyguard/values-hi/strings.xml
index 459287d..69a9dc9 100644
--- a/packages/SystemUI/res-keyguard/values-hi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hi/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"गलत कार्ड."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"चार्ज हो गई है"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • वायरलेस तरीके से चार्ज हो रहा है"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्ज हो रहा है"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • डॉक पर चार्ज हो रहा है"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्ज हो रहा है"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • तेज़ चार्ज हो रहा है"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • धीरे चार्ज हो रहा है"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • बैटरी को नुकसान से बचाने के लिए, उसकी चार्जिंग को ऑप्टिमाइज़ किया गया"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • कुछ समय के लिए चार्जिंग रोक दी गई"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"लॉक खोलने के लिए मेन्यू दबाएं."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"नेटवर्क लॉक किया हुआ है"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"कोई सिम नहीं है"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"कोई सिम जोड़ें."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"सिम मौजूद नहीं है या उसे ऐक्सेस नहीं किया जा सकता. कोई सिम जोड़ें."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"सिम को हमेशा के लिए बंद कर दिया गया है."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"आपका सिम हमेशा के लिए बंद कर दिया गया है.\n दूसरा सिम पाने के लिए, वायरलेस सेवा देने वाली कंपनी से संपर्क करें."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"सिम लॉक है."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"सिम में PUK लॉक लगा है."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"सिम अनलॉक हो रहा है…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"कोई सिम कार्ड नहीं है"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM कार्ड लगाएं."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM कार्ड मौजूद नहीं है या उसे पढ़ा नहीं जा सकता है. कोई SIM कार्ड लगाएं."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"बेकार SIM कार्ड."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"आपका सिम कार्ड हमेशा के लिए बंद कर दिया गया है.\n दूसरे सिम कार्ड के लिए अपने वायरलेस सेवा देने वाले से संपर्क करें."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM कार्ड लॉक किया हुआ है."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM कार्ड को PUK के ज़रिए लॉक किया हुआ है."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM कार्ड अनलॉक हो रहा है…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"पिन क्षेत्र"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"डिवाइस का पासवर्ड"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM पिन क्षेत्र"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" का सिम अब काम नहीं करेगा. जारी रखने के लिए PUK कोड डालें. ज़्यादा जानकारी के लिए अपनी मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"मनचाहा पिन कोड डालें"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"मनचाहे पिन कोड की पुष्टि करें"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"सिम अनलॉक हो रहा है…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM कार्ड अनलॉक हो रहा है…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"कोई ऐसा पिन लिखें, जिसमें 4 से 8 अंक हों."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK कोड 8 या ज़्यादा संख्या वाला होना चाहिए."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"आप अपना पिन <xliff:g id="NUMBER_0">%1$d</xliff:g> बार गलत तरीके से लिख चुके हैं. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> सेकंड में फिर से कोशिश करें."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"आप अपना पासवर्ड <xliff:g id="NUMBER_0">%1$d</xliff:g> बार गलत तरीके से लिख चुके हैं. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> सेकंड में फिर से कोशिश करें."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"आपने अपने लॉक खोलने के पैटर्न को <xliff:g id="NUMBER_0">%1$d</xliff:g> बार गलत तरीके से ड्रॉ किया है. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> सेकंड में फिर से कोशिश करें."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"गलत SIM पिन कोड, अपने डिवाइस को अनलॉक करने के लिए अब आपको अपनी मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करना होगा."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{आपने सिम का गलत पिन कोड डाला है. आपके पास # मौका बचा है. अगर फिर भी डिवाइस अनलॉक नहीं होता है, तो आपको मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करना होगा.}one{आपने सिम का गलत पिन कोड डाला है. आपके पास # मौका बचा है. }other{आपने सिम का गलत पिन कोड डाला है. आपके पास # मौके बचे हैं. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">गलत सिम पिन कोड, आप <xliff:g id="NUMBER_1">%d</xliff:g> बार और कोशिश कर सकते हैं.</item>
+      <item quantity="other">गलत सिम पिन कोड, आप <xliff:g id="NUMBER_1">%d</xliff:g> बार और कोशिश कर सकते हैं.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM बेकार हो गया है. अपनी मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{आपने सिम का गलत PUK कोड डाला है. आपके पास # मौका बचा है. इसके बाद, सिम हमेशा के लिए बंद हो जाएगा.}one{आपने सिम का गलत PUK कोड डाला है. आपके पास # मौका बचा है. इसके बाद, सिम हमेशा के लिए बंद हो जाएगा.}other{आपने सिम का गलत PUK कोड डाला है. आपके पास # मौके बचे हैं. इसके बाद, सिम हमेशा के लिए बंद हो जाएगा.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">SIM PUK कोड गलत है, SIM के हमेशा के लिए बेकार हो जाने से पहले आप <xliff:g id="NUMBER_1">%d</xliff:g> बार और कोशिश कर सकते हैं.</item>
+      <item quantity="other">SIM PUK कोड गलत है, SIM के हमेशा के लिए बेकार हो जाने से पहले आप <xliff:g id="NUMBER_1">%d</xliff:g> बार और कोशिश कर सकते हैं.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM पिन की कार्यवाही विफल रही!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK की कार्यवाही विफल रही!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"इनपुट का तरीका बदलें"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिवाइस फिर से चालू होने के बाद पैटर्न ज़रूरी है"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिवाइस फिर से चालू होने के बाद पिन ज़रूरी है"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिवाइस फिर से चालू होने के बाद पासवर्ड ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ज़्यादा सुरक्षा के लिए, इसके बजाय पैटर्न का इस्तेमाल करें"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ज़्यादा सुरक्षा के लिए, इसके बजाय पिन का इस्तेमाल करें"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ज़्यादा सुरक्षा के लिए, इसके बजाय पासवर्ड का इस्तेमाल करें"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षा के लिए पैटर्न ज़रूरी है"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षा के लिए पिन ज़रूरी है"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षा के लिए पासवर्ड ज़रूरी है"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"व्यवस्थापक ने डिवाइस को लॉक किया है"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिवाइस को मैन्युअल रूप से लॉक किया गया था"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहचान नहीं हो पाई"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"\'फ़ेस अनलॉक\' इस्तेमाल करने के लिए, सेटिंग में जाकर कैमरे का ऐक्सेस चालू करें"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{सिम का पिन डालें. आपके पास # मौका बचा है. अगर फिर भी डिवाइस अनलॉक नहीं होता है, तो आपको मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करना होगा.}one{सिम का पिन डालें. आपके पास # मौका बचा है.}other{सिम का पिन डालें. आपके पास # मौके बचे हैं.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{सिम बंद कर दिया गया है. जारी रखने के लिए PUK कोड डालें. आपके पास # मौका बचा है. इसके बाद, सिम हमेशा के लिए बंद हो जाएगा. ज़्यादा जानकारी के लिए, मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें.}one{सिम बंद कर दिया गया है. जारी रखने के लिए PUK कोड डालें. आपके पास # मौका बचा है. इसके बाद, सिम हमेशा के लिए बंद हो जाएगा. ज़्यादा जानकारी के लिए, मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें.}other{सिम बंद कर दिया गया है. जारी रखने के लिए PUK कोड डालें. आपके पास # मौके बचे हैं. इसके बाद, सिम हमेशा के लिए बंद हो जाएगा. ज़्यादा जानकारी के लिए, मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"फ़ेस अनलॉक की सुविधा का इस्तेमाल करने के लिए, सेटिंग और निजता में जाकर, "<b>"कैमरे का ऐक्सेस"</b>" चालू करें"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">सिम का पिन डालें. आपके पास <xliff:g id="NUMBER_1">%d</xliff:g> मौके बचे हैं.</item>
+      <item quantity="other">सिम का पिन डालें. आपके पास <xliff:g id="NUMBER_1">%d</xliff:g> मौके बचे हैं.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">सिम बंद कर दिया गया है. जारी रखने के लिए PUK कोड डालें. आपके पास <xliff:g id="_NUMBER_1">%d</xliff:g> मौके बचे हैं, उसके बाद, सिम हमेशा के लिए काम करना बंद कर देगा. जानकारी के लिए, मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें.</item>
+      <item quantity="other">सिम बंद कर दिया गया है. जारी रखने के लिए PUK कोड डालें. आपके पास <xliff:g id="_NUMBER_1">%d</xliff:g> मौके बचे हैं, उसके बाद, सिम हमेशा के लिए काम करना बंद कर देगा. जानकारी के लिए, मोबाइल और इंटरनेट सेवा देने वाली कंपनी से संपर्क करें.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"डिफ़ॉल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"एनालॉग"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"जारी रखने के लिए डिवाइस अनलॉक करें"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hr/strings.xml b/packages/SystemUI/res-keyguard/values-hr/strings.xml
index f88408f..de660d8 100644
--- a/packages/SystemUI/res-keyguard/values-hr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hr/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Nevažeća kartica."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Napunjeno"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • bežično punjenje"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • punjenje"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Priključna stanica za punjenje"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • punjenje"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • brzo punjenje"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • sporo punjenje"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje se optimizira radi zaštite baterije"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Punjenje je privremeno ograničeno"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pritisnite Izbornik da biste otključali."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Mreža je zaključana"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nema SIM-a"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Dodajte SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM nedostaje ili nije čitljiv. Dodajte SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM je neupotrebljiv."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Vaš je SIM trajno deaktiviran.\n Obratite se svom davatelju bežičnih usluga da biste dobili drugi SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM je zaključan."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM je zaključan PUK-om."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Otključavanje SIM-a…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nema SIM kartice"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Umetnite SIM karticu."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM kartica nedostaje ili nije čitljiva. Umetnite SIM karticu."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM kartica nije upotrebljiva."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Vaša SIM kartica trajno je onemogućena.\n Zatražite drugu SIM karticu od svog davatelja bežičnih usluga."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kartica je zaključana."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kartica je zaključana PUK-om."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Otključavanje SIM kartice…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Područje PIN-a"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Zaporka uređaja"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Područje PIN-a za SIM"</string>
@@ -61,16 +61,24 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM mobilnog operatera \"<xliff:g id="CARRIER">%1$s</xliff:g>\" sada je onemogućen. Unesite PUK kôd da biste nastavili. Obratite se mobilnom operateru za više pojedinosti."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Unesite željeni PIN kôd"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potvrdite željeni PIN kôd"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Otključavanje SIM-a…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Otključavanje SIM kartice…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Unesite PIN koji ima od 4 do 8 brojeva."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kôd treba imati 8 brojeva ili više."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Netočno ste unijeli PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> put/a. \n\nPokušajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Netočno ste unijeli zaporku <xliff:g id="NUMBER_0">%1$d</xliff:g> put/a. \n\nPokušajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Netočno ste iscrtali uzorak za otključavanje <xliff:g id="NUMBER_0">%1$d</xliff:g> put/a. \n\nPokušajte ponovo za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Netočan PIN kôd SIM kartice; sada morate kontaktirati svog mobilnog operatera da bi otključao vaš uređaj."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{PIN kôd SIM-a nije točan. Imate još # pokušaj, a zatim ćete morati kontaktirati mobilnog operatera da bi otključao uređaj.}one{PIN kôd SIM-a nije točan. Imate još # pokušaj. }few{PIN kôd SIM-a nije točan. Imate još # pokušaja. }other{PIN kôd SIM-a nije točan. Imate još # pokušaja. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">PIN kôd SIM-a nije točan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj.</item>
+      <item quantity="few">PIN kôd SIM-a nije točan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+      <item quantity="other">PIN kôd SIM-a nije točan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kartica nije upotrebljiva. Kontaktirajte svog mobilnog operatera."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{PUK kôd SIM-a nije točan. Imate još # pokušaj prije nego što SIM kartica postane trajno neupotrebljiva.}one{PUK kôd SIM-a nije točan. Imate još # pokušaj prije nego što SIM kartica postane trajno neupotrebljiva.}few{PUK kôd SIM-a nije točan. Imate još # pokušaja prije nego što SIM kartica postane trajno neupotrebljiva.}other{PUK kôd SIM-a nije točan. Imate još # pokušaja prije nego što SIM kartica postane trajno neupotrebljiva.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">PUK kôd SIM-a nije točan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj, a ako ne uspijete, SIM će postati trajno neupotrebljiv.</item>
+      <item quantity="few">PUK kôd SIM-a nije točan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja, a ako ne uspijete, SIM će postati trajno neupotrebljiv.</item>
+      <item quantity="other">PUK kôd SIM-a nije točan. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja, a ako ne uspijete, SIM će postati trajno neupotrebljiv.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operacija PIN-a SIM kartice nije uspjela!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operacija PUK-a SIM kartice nije uspjela!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Promjena načina unosa"</string>
@@ -78,17 +86,24 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nakon ponovnog pokretanja uređaja morate unijeti uzorak"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nakon ponovnog pokretanja uređaja morate unijeti PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nakon ponovnog pokretanja uređaja morate unijeti zaporku"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Za dodatnu sigurnost upotrijebite uzorak"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Za dodatnu sigurnost upotrijebite PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Za dodatnu sigurnost upotrijebite zaporku"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Unesite uzorak radi dodatne sigurnosti"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Unesite PIN radi dodatne sigurnosti"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Unesite zaporku radi dodatne sigurnosti"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Za otključavanje licem uključite pristup kameri"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Unesite PIN za SIM. Imate još # pokušaj, a zatim ćete morati kontaktirati mobilnog operatera da bi otključao uređaj.}one{Unesite PIN za SIM. Imate još # pokušaj.}few{Unesite PIN za SIM. Imate još # pokušaja.}other{Unesite PIN za SIM. Imate još # pokušaja.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaj prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.}one{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaj prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.}few{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaja prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.}other{SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još # pokušaja prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Da biste koristili otključavanje licem, uključite opciju "<b>"Pristup kameri"</b>" u odjeljku Postavke &gt; Privatnost"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaj.</item>
+      <item quantity="few">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+      <item quantity="other">Unesite PIN za SIM. Imate još <xliff:g id="NUMBER_1">%d</xliff:g> pokušaja.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaj prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.</item>
+      <item quantity="few">SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaja prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.</item>
+      <item quantity="other">SIM je sada onemogućen. Unesite PUK kôd da biste nastavili. Imate još <xliff:g id="_NUMBER_1">%d</xliff:g> pokušaja prije nego što SIM kartica postane trajno neupotrebljiva. Više informacija zatražite od mobilnog operatera.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurić"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Otključajte uređaj da biste nastavili"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hu/strings.xml b/packages/SystemUI/res-keyguard/values-hu/strings.xml
index b126030..8854740 100644
--- a/packages/SystemUI/res-keyguard/values-hu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hu/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Érvénytelen kártya."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Feltöltve"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Vezeték nélküli töltés"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Töltés…"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Töltődokk"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Töltés"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Gyors töltés"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lassú töltés"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Optimalizált töltés az akkumulátor védelme érdekében"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Töltés ideiglenesen korlátozva"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"A feloldáshoz nyomja meg a Menü gombot."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Hálózat zárolva"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nincs SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Adjon hozzá egy SIM-et."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"A SIM hiányzik vagy nem olvasható. Adjon hozzá egy SIM-et."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Nem használható SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM véglegesen deaktiválva.\n Forduljon a vezeték nélküli szolgáltatójához másik SIM beszerzése érdekében."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Zárolt SIM."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"A SIM le van zárva PUK-kóddal."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM zárolásának feloldása…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nincs SIM-kártya"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Helyezzen be SIM-kártyát."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"A SIM-kártya hiányzik vagy nem olvasható. Helyezzen be SIM-kártyát."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"A SIM-kártya nem használható."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-kártyája véglegesen le van tiltva.\nMásik SIM-kártya beszerzése érdekében, kérjük, forduljon vezeték nélküli szolgáltatójához."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"A SIM-kártya zárolva van."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"A SIM-kártya PUK-kóddal van zárolva."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM-kártya zárolásának feloldása…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-kód területe"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Eszköz jelszava"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"A SIM-kártyához tartozó PIN-kód mezője"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"A(z) „<xliff:g id="CARRIER">%1$s</xliff:g>” SIM-kártya le van tiltva. A folytatáshoz adja meg a PUK-kódot. A részletekért vegye fel a kapcsolatot szolgáltatójával."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Adja meg a kívánt PIN-kódot"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Erősítse meg a kívánt PIN-kódot"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM zárolásának feloldása…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM-kártya zárolásának feloldása…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Írjon be egy 4-8 számjegyű PIN-kódot."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"A PUK-kódnak legalább nyolc számjegyből kell állnia."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"<xliff:g id="NUMBER_0">%1$d</xliff:g> alkalommal helytelenül adta meg a PIN-kódot.\n\nPróbálja újra <xliff:g id="NUMBER_1">%2$d</xliff:g> másodperc múlva."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"<xliff:g id="NUMBER_0">%1$d</xliff:g> alkalommal helytelenül adta meg a jelszót.\n\nPróbálja újra <xliff:g id="NUMBER_1">%2$d</xliff:g> másodperc múlva."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"<xliff:g id="NUMBER_0">%1$d</xliff:g> alkalommal rosszul rajzolta le a feloldási mintát.\n\nPróbálja újra <xliff:g id="NUMBER_1">%2$d</xliff:g> másodperc múlva."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Helytelen PIN-kód a SIM-kártyához. Az eszköz feloldása érdekében, kérjük, vegye fel a kapcsolatot szolgáltatójával."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Helytelen PIN-kód a SIM-kártyához; még # próbálkozása van, mielőtt fel kell vennie a kapcsolatot szolgáltatójával az eszköz feloldásához.}other{Helytelen PIN-kód a SIM-kártyához; még # próbálkozása van. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Helytelen PIN-kód a SIM-kártyához. Még <xliff:g id="NUMBER_1">%d</xliff:g> próbálkozása maradt.</item>
+      <item quantity="one">Helytelen PIN-kód a SIM-kártyához. Még <xliff:g id="NUMBER_0">%d</xliff:g> próbálkozása maradt, az eszköz feloldásához azt követően fel kell vennie a kapcsolatot szolgáltatójával.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"A SIM-kártya használhatatlan. Vegye fel a kapcsolatot szolgáltatójával."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Helytelen PUK-kód a SIM-kártyához. Még # próbálkozása maradt, mielőtt a SIM-kártya végleg használhatatlanná válik.}other{Helytelen PUK-kód a SIM-kártyához; még # próbálkozása van, mielőtt a SIM-kártya végleg használhatatlan lesz.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Helytelen PUK-kód a SIM-kártyához. Még <xliff:g id="NUMBER_1">%d</xliff:g> próbálkozása maradt, mielőtt a SIM-kártya végleg használhatatlanná válik.</item>
+      <item quantity="one">Helytelen PUK-kód a SIM-kártyához. Még <xliff:g id="NUMBER_0">%d</xliff:g> próbálkozása maradt, mielőtt a SIM-kártya végleg használhatatlanná válik.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"A SIM-kártya PIN-művelete sikertelen!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"A SIM-kártya PUK-művelete sikertelen!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Beviteli módszer váltása"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Az eszköz újraindítását követően meg kell adni a mintát"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Az eszköz újraindítását követően meg kell adni a PIN-kódot"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Az eszköz újraindítását követően meg kell adni a jelszót"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"A nagyobb biztonság érdekében használjon inkább mintát"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"A nagyobb biztonság érdekében használjon inkább PIN-kódot"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"A nagyobb biztonság érdekében használjon inkább jelszót"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"A nagyobb biztonság érdekében minta szükséges"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"A nagyobb biztonság érdekében PIN-kód szükséges"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A nagyobb biztonság érdekében jelszó szükséges"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"A rendszergazda zárolta az eszközt"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Az eszközt manuálisan lezárták"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nem ismerhető fel"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Arcalapú feloldáshoz Hozzáférés a kamerához szükséges"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Adja meg a SIM-kártya PIN-kódját. # próbálkozása maradt. Ha elfogynak a próbálkozási lehetőségek, az eszköz feloldásához fel kell vennie a kapcsolatot szolgáltatójával.}other{Adja meg a SIM-kártya PIN-kódját. # próbálkozási lehetősége maradt.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{A SIM-kártya le van tiltva. A folytatáshoz adja meg a PUK-kódot. Még # próbálkozása van, mielőtt végleg használhatatlanná válik a SIM-kártya. További információért forduljon a szolgáltatóhoz.}other{A SIM-kártya le van tiltva. A folytatáshoz adja meg a PUK-kódot. Még # próbálkozása van, mielőtt végleg használhatatlanná válik a SIM-kártya. További információért forduljon a szolgáltatóhoz.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Az Arcalapú feloldás funkció használatához kapcsolja be a "<b>"Hozzáférés a kamerához"</b>" beállítást a Beállítások &gt; Adatvédelem szakaszban."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Adja meg a SIM-kártya PIN-kódját. <xliff:g id="NUMBER_1">%d</xliff:g> próbálkozása maradt.</item>
+      <item quantity="one">Adja meg a SIM-kártya PIN-kódját. <xliff:g id="NUMBER_0">%d</xliff:g> próbálkozása maradt. Ha elfogynak a próbálkozási lehetőségek, az eszköz feloldásához fel kell vennie a kapcsolatot szolgáltatójával.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">A SIM-kártya le van tiltva. A folytatáshoz adja meg a PUK-kódot. Még <xliff:g id="_NUMBER_1">%d</xliff:g> próbálkozása van, mielőtt végleg használhatatlanná válik a SIM-kártya. További információért forduljon a szolgáltatóhoz.</item>
+      <item quantity="one">A SIM-kártya le van tiltva. A folytatáshoz adja meg a PUK-kódot. Még <xliff:g id="_NUMBER_0">%d</xliff:g> próbálkozása van, mielőtt végleg használhatatlanná válik a SIM-kártya. További információért forduljon a szolgáltatóhoz.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Alapértelmezett"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Buborék"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analóg"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"A folytatáshoz oldja fel az eszközét"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hy/strings.xml b/packages/SystemUI/res-keyguard/values-hy/strings.xml
index 0b0fc04..94fdedd 100644
--- a/packages/SystemUI/res-keyguard/values-hy/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hy/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Սխալ քարտ"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Լիցքավորված է"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Անլար լիցքավորում"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Լիցքավորում"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Լիցքավորում դոկ-կայանի միջոցով"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Լիցքավորում"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Արագ լիցքավորում"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Դանդաղ լիցքավորում"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Մարտկոցը պաշտպանելու համար լիցքավորումն օպտիմալացվել է"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Լիցքավորումը ժամանակավորապես սահմանափակված է"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Ապակողպելու համար սեղմեք Ընտրացանկը:"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Ցանցը կողպված է"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM քարտ չկա"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Ավելացրեք SIM քարտ։"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM քարտը բացակայում է կամ ընթեռնելի չէ։ Ավելացրեք SIM քարտ։"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Անվավեր SIM քարտ։"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Ձեր SIM քարտն ընդմիշտ ապակտիվացվել է։\n Նոր SIM քարտ ձեռք բերելու համար կապվեք ձեր բջջային օպերատորի հետ։"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM քարտը կողպված է։"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM քարտը կողպված է PUK կոդով։"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM քարտն ապակողպվում է…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM քարտ չկա"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Տեղադրեք SIM քարտ:"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM քարտը բացակայում է կամ ընթեռնելի չէ: Տեղադրեք SIM քարտ:"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Անպիտան SIM քարտ:"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Ձեր SIM քարտը ընդմիշտ արգելափակվել է:\nԿապվեք ձեր բջջային օպերատորի հետ՝ նոր SIM քարտ ձեռք բերելու համար:"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM քարտը կողպված է:"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM քարտը PUK-ով կողպված է:"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM քարտը ապակողպվում է…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN կոդի տարածք"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Սարքի գաղտնաբառ"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM քարտի PIN կոդի տարածք"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"«<xliff:g id="CARRIER">%1$s</xliff:g>» SIM քարտն այժմ անջատված է: Շարունակելու համար մուտքագրեք PUK կոդը: Մանրամասն տեղեկություններ ստանալու համար դիմեք օպերատորին:"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Մուտքագրեք ցանկալի PIN կոդը"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Հաստատեք ցանկալի PIN կոդը"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM քարտն ապակողպվում է…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM քարտն ապակողպվում է…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Մուտքագրեք 4-8 թվանշան պարունակող PIN կոդ։"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK կոդը պետք է առնվազն 8 թվանշան պարունակի։"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Դուք սխալ եք մուտքագրել ձեր PIN կոդը <xliff:g id="NUMBER_0">%1$d</xliff:g> անգամ: \n\nՓորձեք կրկին <xliff:g id="NUMBER_1">%2$d</xliff:g> վայրկյանից։"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Դուք սխալ եք մուտքագրել ձեր գաղտնաբառը <xliff:g id="NUMBER_0">%1$d</xliff:g> անգամ: \n\nՓորձեք կրկին <xliff:g id="NUMBER_1">%2$d</xliff:g> վայրկյանից:"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Դուք սխալ եք մուտքագրել ձեր ապակողպման նախշը <xliff:g id="NUMBER_0">%1$d</xliff:g> անգամ: \n\nՓորձեք կրկին <xliff:g id="NUMBER_1">%2$d</xliff:g> վայրկյանից։"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM PIN կոդը սխալ է։ Այժմ պետք է դիմեք ձեր օպերատորին՝ սարքն արգելահանելու համար:"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM քարտի PIN կոդը սխալ է։ Մնաց # փորձ, որից հետո պետք է դիմեք ձեր օպերատորին՝ սարքն ապակողպելու համար։}one{SIM քարտի PIN կոդը սխալ է։ Մնաց # փորձ։ }other{SIM քարտի PIN կոդը սխալ է։ Մնաց # փորձ։ }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">SIM PIN կոդը սխալ է: Մնաց <xliff:g id="NUMBER_1">%d</xliff:g> փորձ, որից հետո պետք է դիմեք ձեր օպերատորին՝ սարքն արգելահանելու համար:</item>
+      <item quantity="other">SIM PIN կոդը սխալ է: Մնաց <xliff:g id="NUMBER_1">%d</xliff:g> փորձ:</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-ը հնարավոր չէ օգտագործել: Դիմեք ձեր օպերատորին:"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM քարտի PUK կոդը սխալ է։ Մնաց # փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել։}one{SIM քարտի PUK կոդը սխալ է։ Մնաց # փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել։}other{SIM քարտի PUK կոդը սխալ է։ Մնաց # փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել։}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">SIM PUK կոդը սխալ է: Մնաց <xliff:g id="NUMBER_1">%d</xliff:g> փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել:</item>
+      <item quantity="other">SIM PUK կոդը սխալ է: Մնաց <xliff:g id="NUMBER_1">%d</xliff:g> փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել:</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN կոդի գործողությունը ձախողվեց:"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK կոդի գործողությունը ձախողվեց:"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Փոխել ներածման եղանակը"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել նախշը"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել PIN կոդը"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել գաղտնաբառը"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Լրացուցիչ անվտանգության համար օգտագործեք նախշ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Լրացուցիչ անվտանգության համար օգտագործեք PIN կոդ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Լրացուցիչ անվտանգության համար օգտագործեք գաղտնաբառ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել նախշը"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել PIN կոդը"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել գաղտնաբառը"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Սարքը կողպված է ադմինիստրատորի կողմից"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Սարքը կողպվել է ձեռքով"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Չհաջողվեց ճանաչել"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Դեմքով ապակողպման համար թույլատրեք տեսախցիկի օգտագործումը"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Մուտքագրեք SIM քարտի PIN կոդը։ Մնացել է # փորձ, որից հետո պետք է դիմեք ձեր օպերատորին՝ սարքն ապակողպելու համար։}one{Մուտքագրեք SIM քարտի PIN կոդը։ Մնացել է # փորձ։}other{Մուտքագրեք SIM քարտի PIN կոդը։ Մնացել է # փորձ։}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM քարտն այժմ անջատված է։ Շարունակելու համար մուտքագրեք PUK կոդը։ Մնացել է # փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել։ Մանրամասների համար դիմեք օպերատորին։}one{SIM քարտն այժմ անջատված է։ Շարունակելու համար մուտքագրեք PUK կոդը։ Մնացել է # փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել։ Մանրամասների համար դիմեք օպերատորին։}other{SIM քարտն այժմ անջատված է։ Շարունակելու համար մուտքագրեք PUK կոդը։ Մնացել է # փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել։ Մանրամասների համար դիմեք օպերատորին։}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Դեմքով ապակողպումն օգտագործելու համար անցեք Կարգավորումներ &gt; Գաղտնիություն և տրամադրեք "<b>"տեսախցիկն օգտագործելու թույլտվություն"</b>"։"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Մուտքագրեք SIM քարտի PIN կոդը: Մնացել է <xliff:g id="NUMBER_1">%d</xliff:g> փորձ:</item>
+      <item quantity="other">Մուտքագրեք SIM քարտի PIN կոդը: Մնացել է <xliff:g id="NUMBER_1">%d</xliff:g> փորձ:</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM քարտն անջատված է: Շարունակելու համար մուտքագրեք PUK կոդը: Մնացել է <xliff:g id="_NUMBER_1">%d</xliff:g> փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել: Մանրամասների համար դիմեք օպերատորին:</item>
+      <item quantity="other">SIM քարտն անջատված է: Շարունակելու համար մուտքագրեք PUK կոդը: Մնացել է <xliff:g id="_NUMBER_1">%d</xliff:g> փորձ, որից հետո SIM քարտն այլևս հնարավոր չի լինի օգտագործել: Մանրամասների համար դիմեք օպերատորին:</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Կանխադրված"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Պղպջակ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Անալոգային"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Շարունակելու համար ապակողպեք ձեր սարքը"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-in/strings.xml b/packages/SystemUI/res-keyguard/values-in/strings.xml
index 78dee0d..b16031a 100644
--- a/packages/SystemUI/res-keyguard/values-in/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-in/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Kartu Tidak Valid"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Terisi penuh"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengisi daya secara nirkabel"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengisi daya"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengisi Daya dengan Dok"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengisi daya"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengisi daya dengan cepat"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengisi daya dengan lambat"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Pengisian daya dioptimalkan untuk melindungi baterai"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Pengisian daya dibatasi sementara"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Tekan Menu untuk membuka kunci."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Jaringan terkunci"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Tidak ada SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Tambahkan SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM tidak ada atau tidak dapat dibaca. Tambahkan SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM tidak dapat digunakan."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM Anda telah dinonaktifkan secara permanen.\n Hubungi penyedia layanan nirkabel Anda untuk mendapatkan SIM lain."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM dikunci."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM dikunci PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Membuka kunci SIM …"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Tidak ada kartu SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Masukkan kartu SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Kartu SIM tidak ada atau tidak dapat dibaca. Masukkan kartu SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Kartu SIM tidak dapat digunakan."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Kartu SIM Anda telah dinonaktifkan secara permanen.\n Hubungi penyedia layanan nirkabel Anda untuk kartu SIM lain."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Kartu SIM terkunci."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Kartu SIM terkunci PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Membuka kunci kartu SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Bidang PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Sandi perangkat"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Bidang PIN SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" kini dinonaktifkan. Masukkan kode PUK untuk melanjutkan. Hubungi operator untuk mengetahui detailnya."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Masukkan kode PIN yang diinginkan"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Konfirmasi kode PIN yang diinginkan"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Membuka kunci SIM …"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Membuka kunci kartu SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Ketikkan PIN berupa 4 sampai 8 angka."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Kode PUK harus terdiri dari 8 angka atau lebih."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Anda telah <xliff:g id="NUMBER_0">%1$d</xliff:g> kali salah mengetik PIN. \n\nCoba lagi dalam <xliff:g id="NUMBER_1">%2$d</xliff:g> detik."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Anda telah <xliff:g id="NUMBER_0">%1$d</xliff:g> kali salah mengetik sandi. \n\nCoba lagi dalam <xliff:g id="NUMBER_1">%2$d</xliff:g> detik."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Anda telah <xliff:g id="NUMBER_0">%1$d</xliff:g> kali salah menggambar pola pembuka kunci. \n\nCoba lagi dalam <xliff:g id="NUMBER_1">%2$d</xliff:g> detik."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Kode PIN SIM salah. Hubungi operator untuk membuka kunci perangkat."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Kode PIN SIM salah. Tersisa # percobaan lagi sebelum Anda harus menghubungi operator untuk membuka kunci perangkat.}other{Kode PIN SIM salah, tersisa # percobaan lagi. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Kode PIN SIM salah, sisa, sisa <xliff:g id="NUMBER_1">%d</xliff:g> percobaan.</item>
+      <item quantity="one">Kode PIN SIM salah, sisa <xliff:g id="NUMBER_0">%d</xliff:g> percobaan sebelum Anda harus menghubungi operator untuk membuka kunci perangkat.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM tidak dapat digunakan. Hubungi operator Anda."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Kode PUK SIM salah, tersisa # percobaan lagi sebelum SIM tidak dapat digunakan secara permanen.}other{Kode PUK SIM salah, tersisa # percobaan lagi sebelum SIM tidak dapat digunakan secara permanen.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Kode PUK SIM salah, sisa <xliff:g id="NUMBER_1">%d</xliff:g> percobaan sebelum SIM tidak dapat digunakan selamanya.</item>
+      <item quantity="one">Kode PUK SIM salah, sisa <xliff:g id="NUMBER_0">%d</xliff:g> percobaan sebelum SIM tidak dapat digunakan selamanya.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operasi PIN SIM gagal!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operasi PUK SIM gagal!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Beralih metode input"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pola diperlukan setelah perangkat dimulai ulang"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah perangkat dimulai ulang"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Sandi diperlukan setelah perangkat dimulai ulang"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Untuk keamanan tambahan, gunakan pola"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Untuk keamanan tambahan, gunakan PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Untuk keamanan tambahan, gunakan sandi"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pola diperlukan untuk keamanan tambahan"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keamanan tambahan"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Sandi diperlukan untuk keamanan tambahan"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Perangkat dikunci oleh admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Perangkat dikunci secara manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Untuk pakai Buka dengan Wajah, beri akses kamera di Setelan"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Masukkan PIN SIM. Tersisa # percobaan lagi sebelum Anda harus menghubungi operator untuk membuka kunci perangkat.}other{Masukkan PIN SIM. Tersisa # percobaan lagi.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM kini dinonaktifkan. Masukkan kode PUK untuk melanjutkan. Tersisa # percobaan lagi sebelum SIM tidak dapat digunakan secara permanen. Hubungi operator untuk mengetahui detailnya.}other{SIM kini dinonaktifkan. Masukkan kode PUK untuk melanjutkan. Tersisa # percobaan lagi sebelum SIM tidak dapat digunakan secara permanen. Hubungi operator untuk mengetahui detailnya.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Untuk menggunakan Face Unlock, aktifkan "<b>"Akses kamera"</b>" di Setelan &gt; Privasi"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Masukkan PIN SIM. Tersisa <xliff:g id="NUMBER_1">%d</xliff:g> percobaan.</item>
+      <item quantity="one">Masukkan PIN SIM. Tersisa <xliff:g id="NUMBER_0">%d</xliff:g> percobaan sebelum Anda harus menghubungi operator untuk membuka kunci perangkat.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM kini dinonaktifkan. Masukkan kode PUK untuk melanjutkan. Tersisa <xliff:g id="_NUMBER_1">%d</xliff:g> percobaan sebelum SIM tidak dapat digunakan secara permanen. Hubungi operator untuk mengetahui detailnya.</item>
+      <item quantity="one">SIM kini dinonaktifkan. Masukkan kode PUK untuk melanjutkan. Tersisa <xliff:g id="_NUMBER_0">%d</xliff:g> percobaan sebelum SIM tidak dapat digunakan secara permanen. Hubungi operator untuk mengetahui detailnya.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Buka kunci perangkat untuk melanjutkan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-is/strings.xml b/packages/SystemUI/res-keyguard/values-is/strings.xml
index d758565..8f6b18b 100644
--- a/packages/SystemUI/res-keyguard/values-is/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-is/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ógilt kort."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Fullhlaðin"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Í þráðlausri hleðslu"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Í hleðslu"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hleður í dokku"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Í hleðslu"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hröð hleðsla"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hæg hleðsla"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hleðsla fínstillt til að vernda rafhlöðuna"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hleðsla takmörkuð tímabundið"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Ýttu á valmyndarhnappinn til að taka úr lás."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Net læst"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Ekkert SIM-kort"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Bæta við SIM-korti."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-kort vantar eða er ekki læsilegt. Bæta við SIM-korti."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Ónothæft SIM-kort."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM-kortið þitt var gert varanlega óvirkt.\n Hafðu samband við símafyrirtækið þitt til að fá nýtt SIM-kort."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-kort er læst."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-kort er læst með PUK-númeri."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Opnar SIM-kort…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Ekkert SIM-kort"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Settu SIM-kort í."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-kort vantar eða það er ekki læsilegt. Settu SIM-kort í."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Ónothæft SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-kortið hefur verið gert varanlega óvirkt.\n Hafðu samband við símafyrirtækið þitt til að fá annað SIM-kort."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kortið er læst."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kortið er PUK-læst."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Tekur SIM-kort úr lás…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-svæði"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Aðgangsorð tækis"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"PIN-svæði SIM-korts"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-kortið „<xliff:g id="CARRIER">%1$s</xliff:g>“ hefur verið gert óvirkt. Sláðu inn PUK-númerið til að halda áfram. Hafðu samband við símafyrirtækið til að fá frekari upplýsingar."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Sláðu inn nýtt PIN-númer"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Staðfestu nýja PIN-númerið"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Opnar SIM-kort…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Tekur SIM-kort úr lás…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Sláðu in PIN-númer sem er 4 til 8 tölustafir."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-númerið verður að vera 8 tölustafir eða lengra."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Þú hefur slegið inn rangt PIN-númer <xliff:g id="NUMBER_0">%1$d</xliff:g> sinnum. \n\nReyndu aftur eftir <xliff:g id="NUMBER_1">%2$d</xliff:g> sekúndur."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Þú hefur slegið inn rangt aðgangsorð <xliff:g id="NUMBER_0">%1$d</xliff:g> sinnum. \n\nReyndu aftur eftir <xliff:g id="NUMBER_1">%2$d</xliff:g> sekúndur."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Þú hefur teiknað rangt opnunarmynstur <xliff:g id="NUMBER_0">%1$d</xliff:g> sinnum. \n\nReyndu aftur eftir <xliff:g id="NUMBER_1">%2$d</xliff:g> sekúndur."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Rangt PIN-númer SIM-korts. Nú þarftu að hafa samband við símafyrirtækið til að opna fyrir tækið."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Rangt PIN-númer SIM-korts. Þú átt # tilraun eftir áður en þú þarft að hafa samband við símafyrirtækið þitt til að taka tækið úr lás.}one{Rangt PIN-númer SIM-korts. Þú átt # tilraun eftir. }other{Rangt PIN-númer SIM-korts. Þú átt # tilraunir eftir. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Rangt PIN-númer SIM-korts. Þú átt <xliff:g id="NUMBER_1">%d</xliff:g> tilraun eftir.</item>
+      <item quantity="other">Rangt PIN-númer SIM-korts. Þú átt <xliff:g id="NUMBER_1">%d</xliff:g> tilraunir eftir.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-kortið er ónothæft. Hafðu samband við símafyrirtækið þitt."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Rangt PUK-númer SIM-korts. Þú átt # tilraun eftir áður en SIM-kortið verður ónothæft til frambúðar.}one{Rangt PUK-númer SIM-korts. Þú átt # tilraun eftir áður en SIM-kortið verður ónothæft til frambúðar.}other{Rangt PUK-númer SIM-korts. Þú átt # tilraunir eftir áður en SIM-kortið verður ónothæft til frambúðar.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Rangt PUK-númer SIM-korts. Þú átt <xliff:g id="NUMBER_1">%d</xliff:g> tilraun eftir áður en SIM-kortið verður ónothæft til frambúðar.</item>
+      <item quantity="other">Rangt PUK-númer SIM-korts. Þú átt <xliff:g id="NUMBER_1">%d</xliff:g> tilraunir eftir áður en SIM-kortið verður ónothæft til frambúðar.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"PIN-aðgerð SIM-korts mistókst!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"PUK-aðgerð SIM-korts mistókst!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Skipta um innsláttaraðferð"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Mynsturs er krafist þegar tækið er endurræst"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-númers er krafist þegar tækið er endurræst"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Aðgangsorðs er krafist þegar tækið er endurræst"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Fyrir aukið öryggi skaltu nota mynstur í staðinn"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Fyrir aukið öryggi skaltu nota PIN-númer í staðinn"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Fyrir aukið öryggi skaltu nota aðgangsorð í staðinn"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mynsturs er krafist af öryggisástæðum"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-númers er krafist af öryggisástæðum"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Aðgangsorðs er krafist af öryggisástæðum"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Kerfisstjóri læsti tæki"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Tækinu var læst handvirkt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Þekktist ekki"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Kveiktu á myndavélaaðgangi í stillingum til að nota andlitskenni"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Sláðu inn PIN-númer SIM-korts. Þú átt # tilraun eftir áður en þú þarft að hafa samband við símafyrirtækið þitt til að taka tækið úr lás.}one{Sláðu inn PIN-númer SIM-kortsins. Þú átt # tilraun eftir.}other{Sláðu inn PIN-númer SIM-kortsins. Þú átt # tilraunir eftir.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-kortið er nú óvirkt. Sláðu inn PUK-númer til að halda áfram. Þú átt # tilraun eftir þar til SIM-kortið verður ónothæft til frambúðar. Hafðu samband við símafyrirtækið til að fá upplýsingar.}one{SIM-kortið er nú óvirkt. Sláðu inn PUK-númer til að halda áfram. Þú átt # tilraun eftir þar til SIM-kortið verður ónothæft til frambúðar. Hafðu samband við símafyrirtækið til að fá upplýsingar.}other{SIM-kortið er nú óvirkt. Sláðu inn PUK-númer til að halda áfram. Þú átt # tilraunir eftir þar til SIM-kortið verður ónothæft til frambúðar. Hafðu samband við símafyrirtækið til að fá upplýsingar.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Þú verður að kveikja á "<b>"aðgangi að myndavél"</b>" í „Stillingar &gt; persónuvernd“ til að nota andlitskenni"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Sláðu inn PIN-númer SIM-korts. Þú átt <xliff:g id="NUMBER_1">%d</xliff:g> tilraun eftir.</item>
+      <item quantity="other">Sláðu inn PIN-númer SIM-korts. Þú átt <xliff:g id="NUMBER_1">%d</xliff:g> tilraunir eftir.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM-kortið er nú óvirkt. Sláðu inn PUK-númer til að halda áfram. Það er <xliff:g id="_NUMBER_1">%d</xliff:g> tilraun eftir þar til SIM-kortið verður ónothæft til frambúðar. Hafðu samband við símafyrirtækið til að fá upplýsingar.</item>
+      <item quantity="other">SIM-kortið er nú óvirkt. Sláðu inn PUK-númer til að halda áfram. Það eru <xliff:g id="_NUMBER_1">%d</xliff:g> tilraunir eftir þar til SIM-kortið verður ónothæft til frambúðar. Hafðu samband við símafyrirtækið til að fá upplýsingar.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Sjálfgefið"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Blaðra"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Með vísum"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Taktu tækið úr lás til að halda áfram"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-it/strings.xml b/packages/SystemUI/res-keyguard/values-it/strings.xml
index 3b8e42c..f912a28 100644
--- a/packages/SystemUI/res-keyguard/values-it/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-it/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Scheda non valida."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Carico"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • In carica wireless"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • In carica"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • In carica nel dock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • In carica"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ricarica veloce"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ricarica lenta"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ricarica ottimizzata per proteggere la batteria"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ricarica momentaneamente limitata"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Premi Menu per sbloccare."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rete bloccata"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nessuna SIM presente"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Aggiungi una SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM mancante o non leggibile. Aggiungi una SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM inutilizzabile."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"La SIM è stata disattivata definitivamente.\n Contatta il tuo fornitore di servizi wireless per richiedere un\'altra SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"La SIM è bloccata."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"La SIM è bloccata tramite PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Sblocco della SIM in corso…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nessuna SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Inserisci una scheda SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Scheda SIM mancante o non leggibile. Inserisci una scheda SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Scheda SIM inutilizzabile."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"La scheda SIM è stata disattivata definitivamente.\n Contatta il fornitore del tuo servizio wireless per ricevere un\'altra scheda SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"La SIM è bloccata."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"La SIM è bloccata tramite PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Sblocco SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Area PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Password del dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Area PIN SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Sequenza errata"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Password errata"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"PIN errato"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Riprova fra # secondo.}many{Riprova fra # secondi.}other{Riprova fra # secondi.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Riprova fra # secondo.}other{Riprova fra # secondi.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Inserisci il PIN della SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Inserisci il PIN della SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Disattiva la eSIM per usare il dispositivo senza servizio dati mobile."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"La SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" non è attiva al momento. Inserisci il codice PUK per continuare. Contatta l\'operatore per avere informazioni dettagliate."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Inserisci il codice PIN desiderato"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Conferma il codice PIN desiderato"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Sblocco della SIM in corso…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Sblocco SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Il PIN deve essere di 4-8 numeri."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Il codice PUK dovrebbe avere almeno otto numeri."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Hai digitato il tuo PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> volte in modo errato. \n\nRiprova tra <xliff:g id="NUMBER_1">%2$d</xliff:g> secondi."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Hai digitato la tua password <xliff:g id="NUMBER_0">%1$d</xliff:g> volte in modo errato. \n\nRiprova tra <xliff:g id="NUMBER_1">%2$d</xliff:g> secondi."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"<xliff:g id="NUMBER_0">%1$d</xliff:g> tentativi errati di inserimento della sequenza di sblocco. \n\nRiprova tra <xliff:g id="NUMBER_1">%2$d</xliff:g> secondi."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Codice PIN della SIM errato. Devi contattare l\'operatore per sbloccare il dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Codice PIN della SIM errato. Hai ancora # tentativo a disposizione, dopodiché dovrai contattare l\'operatore per sbloccare il dispositivo.}many{Codice PIN della SIM errato. Hai ancora # tentativi a disposizione. }other{Codice PIN della SIM errato. Hai ancora # tentativi a disposizione. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Codice PIN della SIM errato. Hai ancora <xliff:g id="NUMBER_1">%d</xliff:g> tentativi a disposizione.</item>
+      <item quantity="one">Codice PIN della SIM errato. Hai ancora <xliff:g id="NUMBER_0">%d</xliff:g> tentativo a disposizione, dopodiché dovrai contattare l\'operatore per sbloccare il dispositivo.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM inutilizzabile. Contatta il tuo operatore."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Codice PUK della SIM errato. Hai ancora # tentativo a disposizione prima che la SIM diventi definitivamente inutilizzabile.}many{Codice PUK della SIM errato. Hai ancora # tentativi a disposizione prima che la SIM diventi definitivamente inutilizzabile.}other{Codice PUK della SIM errato. Hai ancora # tentativi a disposizione prima che la SIM diventi definitivamente inutilizzabile.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Codice PUK della SIM errato. Hai ancora <xliff:g id="NUMBER_1">%d</xliff:g> tentativi a disposizione prima che la SIM diventi definitivamente inutilizzabile.</item>
+      <item quantity="one">Codice PUK della SIM errato. Hai ancora <xliff:g id="NUMBER_0">%d</xliff:g> tentativo a disposizione prima che la SIM diventi definitivamente inutilizzabile.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operazione con PIN della SIM non riuscita."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operazione con PUK della SIM non riuscita."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Cambia metodo di immissione"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Sequenza obbligatoria dopo il riavvio del dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN obbligatorio dopo il riavvio del dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password obbligatoria dopo il riavvio del dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Per maggior sicurezza, usa invece la sequenza"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Per maggior sicurezza, usa invece il PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Per maggior sicurezza, usa invece la password"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Sequenza obbligatoria per maggiore sicurezza"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN obbligatorio per maggiore sicurezza"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password obbligatoria per maggiore sicurezza"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloccato dall\'amministratore"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Il dispositivo è stato bloccato manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non riconosciuto"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Sblocco con volto richiede l\'accesso alla fotocamera"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Inserisci il codice PIN della SIM. Hai ancora # tentativo a disposizione, dopodiché dovrai contattare l\'operatore per sbloccare il dispositivo.}many{Inserisci il PIN della SIM. Hai a disposizione ancora # tentativi.}other{Inserisci il PIN della SIM. Hai a disposizione ancora # tentativi.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{La scheda SIM è ora disattivata. Inserisci il codice PUK per continuare. Hai ancora # tentativo a disposizione prima che la SIM diventi definitivamente inutilizzabile. Per informazioni dettagliate, contatta l\'operatore.}many{La scheda SIM è ora disattivata. Inserisci il codice PUK per continuare. Hai ancora # tentativi a disposizione prima che la SIM diventi definitivamente inutilizzabile. Per informazioni dettagliate, contatta l\'operatore.}other{La scheda SIM è ora disattivata. Inserisci il codice PUK per continuare. Hai ancora # tentativi a disposizione prima che la SIM diventi definitivamente inutilizzabile. Per informazioni dettagliate, contatta l\'operatore.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Per utilizzare lo sblocco con il volto, attiva "<b>"l\'accesso alla fotocamera"</b>" in Impostazioni &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Inserisci il codice PIN della SIM. Hai ancora <xliff:g id="NUMBER_1">%d</xliff:g> tentativi a disposizione.</item>
+      <item quantity="one">Inserisci il codice PIN della SIM. Hai ancora <xliff:g id="NUMBER_0">%d</xliff:g> tentativo a disposizione, dopodiché dovrai contattare l\'operatore per sbloccare il dispositivo.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">La scheda SIM è ora disattivata. Inserisci il codice PUK per continuare. Hai ancora <xliff:g id="_NUMBER_1">%d</xliff:g> tentativi a disposizione prima che la SIM diventi definitivamente inutilizzabile. Per informazioni dettagliate, contatta l\'operatore.</item>
+      <item quantity="one">La scheda SIM è ora disattivata. Inserisci il codice PUK per continuare. Hai ancora <xliff:g id="_NUMBER_0">%d</xliff:g> tentativo a disposizione prima che la SIM diventi definitivamente inutilizzabile. Per informazioni dettagliate, contatta l\'operatore.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predefinito"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Sblocca il dispositivo per continuare"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-iw/strings.xml b/packages/SystemUI/res-keyguard/values-iw/strings.xml
index bdc867b..ce43b15 100644
--- a/packages/SystemUI/res-keyguard/values-iw/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-iw/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"כרטיס לא חוקי."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"הסוללה טעונה"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • בטעינה אלחוטית"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • בטעינה"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • אביזר העגינה בטעינה"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • בטעינה"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • בטעינה מהירה"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • בטעינה איטית"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • הטעינה עברה אופטימיזציה כדי להגן על הסוללה"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • הטעינה מוגבלת באופן זמני"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"יש ללחוץ על \'תפריט\' כדי לבטל את הנעילה."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"הרשת נעולה"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"‏אין כרטיס SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"‏הוספת כרטיס SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"‏כרטיס ה-SIM חסר או שלא ניתן לקרוא אותו. הוספת כרטיס SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"‏לא ניתן להשתמש בכרטיס ה-SIM הזה."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"‏כרטיס ה-SIM שלך הושבת באופן סופי.\n עליך לפנות לספק השירות האלחוטי שלך לקבלת כרטיס SIM אחר."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"‏כרטיס ה-SIM נעול."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"‏כרטיס ה-SIM נעול באמצעות PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"‏מתבצע ביטול נעילה של כרטיס ה-SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"‏אין כרטיס SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"‏יש להכניס כרטיס SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"‏כרטיס ה-SIM חסר או שלא ניתן לקרוא אותו. יש להכניס כרטיס SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"‏לא ניתן להשתמש בכרטיס SIM זה."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"‏כרטיס ה-SIM שלך הושבת באופן סופי.\nיש לפנות לספק השירות האלחוטי שלך לקבלת כרטיס SIM אחר."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"‏כרטיס ה-SIM נעול."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"‏כרטיס ה-SIM נעול באמצעות PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"‏בתהליך ביטול נעילה של כרטיס ה-SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"אזור של קוד האימות"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"סיסמת המכשיר"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"‏אזור לקוד האימות של כרטיס ה-SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"קו ביטול נעילה שגוי"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"סיסמה שגויה"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"קוד האימות שגוי"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{אפשר לנסות שוב בעוד שנייה אחת.}one{אפשר לנסות שוב בעוד # שניות.}two{אפשר לנסות שוב בעוד # שניות.}other{אפשר לנסות שוב בעוד # שניות.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{אפשר לנסות שוב בעוד שנייה אחת.}two{אפשר לנסות שוב בעוד # שניות.}many{אפשר לנסות שוב בעוד # שניות.}other{אפשר לנסות שוב בעוד # שניות.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"‏יש להזין את קוד האימות של כרטיס ה-SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"‏יש להזין את קוד האימות של כרטיס ה-SIM של <xliff:g id="CARRIER">%1$s</xliff:g>."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"‏<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> יש להשבית את כרטיס ה-eSIM כדי להשתמש במכשיר ללא שירות סלולרי."</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"‏ה-SIM של \"<xliff:g id="CARRIER">%1$s</xliff:g>\" מושבת עכשיו. צריך להזין קוד PUK כדי להמשיך. לפרטים, יש לפנות אל הספק."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"יש להזין את קוד האימות הרצוי"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"צריך לאשר את קוד האימות הרצוי"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"‏מתבצע ביטול נעילה של כרטיס ה-SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"‏מתבצע ביטול נעילה של כרטיס ה-SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"יש להקליד קוד אימות שאורכו 4 עד 8 ספרות."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"‏קוד PUK צריך להיות בן 8 ספרות או יותר."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"הקלדת קוד גישה שגוי <xliff:g id="NUMBER_0">%1$d</xliff:g> פעמים. \n\nיש לנסות שוב בעוד <xliff:g id="NUMBER_1">%2$d</xliff:g> שניות."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"הקלדת סיסמה שגויה <xliff:g id="NUMBER_0">%1$d</xliff:g> פעמים. \n\nאפשר לנסות שוב בעוד <xliff:g id="NUMBER_1">%2$d</xliff:g> שניות."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"שרטטת קו ביטול נעילה שגוי <xliff:g id="NUMBER_0">%1$d</xliff:g> פעמים. \n\nאפשר לנסות שוב בעוד <xliff:g id="NUMBER_1">%2$d</xliff:g> שניות."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"‏קוד האימות של כרטיס ה-SIM שגוי. יש ליצור קשר עם הספק כדי לבטל את נעילת המכשיר."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{‏קוד האימות של כרטיס ה-SIM שגוי. נשאר לך עוד ניסיון אחד (#) לפני שיהיה צורך ליצור קשר עם הספק כדי לבטל את נעילת המכשיר.}one{‏קוד האימות של כרטיס ה-SIM שגוי. נשארו לך עוד # ניסיונות. }two{‏קוד האימות של כרטיס ה-SIM שגוי. נשארו לך עוד # ניסיונות. }other{‏קוד האימות של כרטיס ה-SIM שגוי. נשארו לך עוד # ניסיונות. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="two">‏קוד האימות של כרטיס ה-SIM שגוי. נותרו לך עוד <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות.</item>
+      <item quantity="many">‏קוד האימות של כרטיס ה-SIM שגוי. נותרו לך עוד <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות.</item>
+      <item quantity="other">‏קוד האימות של כרטיס ה-SIM שגוי. נותרו לך עוד <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות.</item>
+      <item quantity="one">‏קוד האימות של כרטיס ה-SIM שגוי. נותר לך עוד ניסיון <xliff:g id="NUMBER_0">%d</xliff:g> לפני שיהיה עליך ליצור קשר עם הספק כדי לבטל את נעילת המכשיר.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"‏לא ניתן להשתמש בכרטיס ה-SIM. יש ליצור קשר עם הספק."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{‏קוד ה-PUK של כרטיס ה-SIM שגוי. נשאר לך עוד ניסיון אחד (#) לפני שכרטיס ה-SIM יינעל לתמיד.}one{‏קוד ה-PUK של כרטיס ה-SIM שגוי. נשארו לך עוד # ניסיונות לפני שכרטיס ה-SIM יינעל לתמיד.}two{‏קוד ה-PUK של כרטיס ה-SIM שגוי. נשארו לך עוד # ניסיונות לפני שכרטיס ה-SIM יינעל לתמיד.}other{‏קוד ה-PUK של כרטיס ה-SIM שגוי. נשארו לך עוד # ניסיונות לפני שכרטיס ה-SIM יינעל לתמיד.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="two">‏קוד ה-PUK של כרטיס ה-SIM שגוי. נותרו לך עוד <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות לפני שכרטיס ה-SIM יינעל לצמיתות.</item>
+      <item quantity="many">‏קוד ה-PUK של כרטיס ה-SIM שגוי. נותרו לך עוד <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות לפני שכרטיס ה-SIM יינעל לצמיתות.</item>
+      <item quantity="other">‏קוד ה-PUK של כרטיס ה-SIM שגוי. נותרו לך עוד <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות לפני שכרטיס ה-SIM יינעל לצמיתות.</item>
+      <item quantity="one">‏קוד ה-PUK של כרטיס ה-SIM שגוי. נותר לך ניסיון <xliff:g id="NUMBER_0">%d</xliff:g> נוסף לפני שכרטיס ה-SIM יינעל לצמיתות.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"‏נכשלה פעולת קוד הגישה של כרטיס ה-SIM"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"‏הניסיון לביטול הנעילה של כרטיס ה-SIM באמצעות קוד PUK נכשל!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"החלפת שיטת קלט"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"יש להזין את קו ביטול הנעילה לאחר הפעלה מחדש של המכשיר"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"צריך להזין קוד אימות לאחר הפעלה מחדש של המכשיר"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"יש להזין סיסמה לאחר הפעלה מחדש של המכשיר"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקו ביטול נעילה במקום זאת"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקוד אימות במקום זאת"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"כדי להגביר את רמת האבטחה, כדאי להשתמש בסיסמה במקום זאת"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"יש להזין את קו ביטול הנעילה כדי להגביר את רמת האבטחה"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"יש להזין קוד אימות כדי להגביר את רמת האבטחה"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"יש להזין סיסמה כדי להגביר את רמת האבטחה"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"המנהל של המכשיר נהל אותו"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"המכשיר ננעל באופן ידני"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"לא זוהתה"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"לזיהוי הפנים יש להפעיל את הגישה למצלמה בהגדרות"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{‏יש להזין את קוד האימות של כרטיס ה-SIM. נשאר לך עוד ניסיון אחד (#) לפני שיהיה צורך ליצור קשר עם הספק כדי לבטל את נעילת המכשיר.}one{‏יש להזין את קוד האימות של כרטיס ה-SIM. נשארו לך עוד # ניסיונות.}two{‏יש להזין את קוד האימות של כרטיס ה-SIM. נשארו לך עוד # ניסיונות.}other{‏יש להזין את קוד האימות של כרטיס ה-SIM. נשארו לך עוד # ניסיונות.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נשאר לך עוד ניסיון אחד (#) לפני שכרטיס ה-SIM יינעל לתמיד. למידע נוסף, ניתן לפנות לספק.}one{‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נשארו לך עוד # ניסיונות לפני שכרטיס ה-SIM יינעל לתמיד. למידע נוסף, ניתן לפנות לספק.}two{‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נשארו לך עוד # ניסיונות לפני שכרטיס ה-SIM יינעל לתמיד. למידע נוסף, ניתן לפנות לספק.}other{‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נשארו לך עוד # ניסיונות לפני שכרטיס ה-SIM יינעל לתמיד. למידע נוסף, ניתן לפנות לספק.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"‏כדי להשתמש בתכונה \'פתיחה ע\"י זיהוי הפנים\', יש להפעיל את ה"<b>"גישה למצלמה"</b>" בהגדרות &gt; פרטיות"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="two">‏יש להזין קוד אימות של כרטיס SIM. נותרו לך <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות נוספים.</item>
+      <item quantity="many">‏יש להזין קוד אימות של כרטיס SIM. נותרו לך <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות נוספים.</item>
+      <item quantity="other">‏יש להזין קוד אימות של כרטיס SIM. נותרו לך <xliff:g id="NUMBER_1">%d</xliff:g> ניסיונות נוספים.</item>
+      <item quantity="one">‏יש להזין קוד אימות של כרטיס SIM. נותר לך ניסיון נוסף (<xliff:g id="NUMBER_0">%d</xliff:g>) לפני שיהיה צורך ליצור קשר עם הספק כדי לבטל את נעילת המכשיר.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="two">‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נותרו לך <xliff:g id="_NUMBER_1">%d</xliff:g> ניסיונות נוספים לפני שכרטיס ה-SIM יינעל באופן סופי. למידע נוסף, ניתן לפנות לספק שלך.</item>
+      <item quantity="many">‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נותרו לך <xliff:g id="_NUMBER_1">%d</xliff:g> ניסיונות נוספים לפני שכרטיס ה-SIM יינעל באופן סופי. למידע נוסף, ניתן לפנות לספק שלך.</item>
+      <item quantity="other">‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נותרו לך <xliff:g id="_NUMBER_1">%d</xliff:g> ניסיונות נוספים לפני שכרטיס ה-SIM יינעל באופן סופי. למידע נוסף, ניתן לפנות לספק שלך.</item>
+      <item quantity="one">‏כרטיס ה-SIM מושבת כעת. יש להזין קוד PUK כדי להמשיך. נותר לך ניסיון אחד (<xliff:g id="_NUMBER_0">%d</xliff:g>) נוסף לפני שכרטיס ה-SIM יינעל באופן סופי. למידע נוסף, ניתן לפנות לספק שלך.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ברירת מחדל"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"בועה"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"אנלוגי"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"צריך לבטל את הנעילה של המכשיר כדי להמשיך"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ja/strings.xml b/packages/SystemUI/res-keyguard/values-ja/strings.xml
index c4c370d..07b3c87 100644
--- a/packages/SystemUI/res-keyguard/values-ja/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ja/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"無効なカードです。"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"充電が完了しました"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ワイヤレス充電中"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電中"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ホルダーで充電中"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電中"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 急速充電中"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 低速充電中"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電最適化済み(バッテリーを保護)"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電を一時的に制限しています"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"メニューからロックを解除できます。"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ネットワークがロックされました"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM がありません"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM を追加してください。"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM が見つからないか読み取れません。SIM を追加してください。"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM が使用できません。"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM が完全に無効になっています。\n ワイヤレス サービス プロバイダにお問い合わせのうえ、新しい SIM を入手してください。"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM がロックされています。"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM が PUK でロックされました。"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM ロックを解除しています…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM カードなし"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM カードを挿入してください。"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM カードが見つからないか読み取れません。SIM カードを挿入してください。"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM カードは使用できません。"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"お使いの SIM カードは完全に無効となっています。\nワイヤレス サービス プロバイダに問い合わせて新しい SIM カードを入手してください。"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM カードはロックされています。"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM カードは PUK でロックされています。"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM カードのロックを解除しています…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN エリア"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"デバイスのパスワード"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN エリア"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM「<xliff:g id="CARRIER">%1$s</xliff:g>」が無効になりました。続行するには PUK コードを入力してください。詳しくは携帯通信会社にお問い合わせください。"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"必要な PIN コードを入力してください"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"必要な PIN コードを確認してください"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM ロックを解除しています…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM カードのロックを解除しています…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"PIN は 4~8 桁の数字で入力してください。"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK コードは 8 桁以下の数字で入力してください。"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN の入力を <xliff:g id="NUMBER_0">%1$d</xliff:g> 回間違えました。\n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後にもう一度お試しください。"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"パスワードの入力を <xliff:g id="NUMBER_0">%1$d</xliff:g> 回間違えました。\n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後にもう一度お試しください。"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"ロック解除パターンの入力を <xliff:g id="NUMBER_0">%1$d</xliff:g> 回間違えました。\n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後にもう一度お試しください。"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM PIN コードが無効です。お使いのデバイスをロック解除するには携帯通信会社にお問い合わせいただく必要があります。"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM PIN コードが無効です。入力できるのはあと # 回です。この回数を超えると、お使いのデバイスをロック解除するのに携帯通信会社にお問い合わせいただく必要があります。}other{SIM PIN コードが無効です。入力できるのはあと # 回です。}}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM PIN コードが無効です。入力できるのはあと <xliff:g id="NUMBER_1">%d</xliff:g> 回です。</item>
+      <item quantity="one">SIM PIN コードが無効です。入力できるのはあと <xliff:g id="NUMBER_0">%d</xliff:g> 回です。この回数を超えると、お使いのデバイスをロック解除するのに携帯通信会社にお問い合わせいただく必要があります。</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM は使用できません。携帯通信会社にお問い合わせください。"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM PUK コードが無効です。入力できるのはあと # 回です。この回数を超えると SIM は完全に使用できなくなります。}other{SIM PUK コードが無効です。入力できるのはあと # 回です。この回数を超えると SIM は完全に使用できなくなります。}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM PUK コードが無効です。入力できるのはあと <xliff:g id="NUMBER_1">%d</xliff:g> 回です。この回数を超えると SIM は完全に使用できなくなります。</item>
+      <item quantity="one">SIM PUK コードが無効です。入力できるのはあと <xliff:g id="NUMBER_0">%d</xliff:g> 回です。この回数を超えると SIM は完全に使用できなくなります。</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN 操作に失敗しました。"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK 操作に失敗しました。"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"入力方法の切り替え"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"デバイスの再起動後はパターンの入力が必要となります"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"デバイスの再起動後は PIN の入力が必要となります"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"デバイスの再起動後はパスワードの入力が必要となります"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"セキュリティを強化するには代わりにパターンを使用してください"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"セキュリティを強化するには代わりに PIN を使用してください"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"セキュリティを強化するには代わりにパスワードを使用してください"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"追加の確認のためパターンが必要です"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"追加の確認のため PIN が必要です"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"追加の確認のためパスワードが必要です"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"デバイスは管理者によりロックされています"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"デバイスは手動でロックされました"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"認識されませんでした"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"顔認証の使用: 設定でカメラアクセスを有効にしてください"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PIN を入力してください。入力できるのはあと # 回です。この回数を超えると、お使いのデバイスをロック解除するのに携帯通信会社にお問い合わせいただく必要があります。}other{SIM PIN を入力してください。入力できるのはあと # 回です。}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM が無効になりました。続行するには PUK コードを入力してください。入力できるのはあと # 回です。この回数を超えると SIM は完全に使用できなくなります。詳しくは携帯通信会社にお問い合わせください。}other{SIM が無効になりました。続行するには PUK コードを入力してください。入力できるのはあと # 回です。この回数を超えると SIM は完全に使用できなくなります。詳しくは携帯通信会社にお問い合わせください。}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"顔認証を使用するには、[設定] &gt; [プライバシー] で"<b>"カメラへのアクセス"</b>"を有効にしてください"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN を入力してください。入力できるのはあと <xliff:g id="NUMBER_1">%d</xliff:g> 回です。</item>
+      <item quantity="one">SIM PIN を入力してください。入力できるのはあと <xliff:g id="NUMBER_0">%d</xliff:g> 回です。この回数を超えた場合は、携帯通信会社にお問い合わせください。</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM が無効になりました。続行するには PUK コードを入力してください。入力できるのはあと <xliff:g id="_NUMBER_1">%d</xliff:g> 回です。この回数を超えると SIM は完全に使用できなくなります。詳しくは携帯通信会社にお問い合わせください。</item>
+      <item quantity="one">SIM が無効になりました。続行するには PUK コードを入力してください。入力できるのはあと <xliff:g id="_NUMBER_0">%d</xliff:g> 回です。この回数を超えると SIM は完全に使用できなくなります。詳しくは携帯通信会社にお問い合わせください。</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"デフォルト"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"バブル"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"アナログ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"続行するにはデバイスのロックを解除してください"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ka/strings.xml b/packages/SystemUI/res-keyguard/values-ka/strings.xml
index 38e5420..f2341c6 100644
--- a/packages/SystemUI/res-keyguard/values-ka/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ka/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ბარათი არასწორია."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"დატენილია"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • იტენება უსადენოდ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • იტენება"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • დამტენი სამაგრი"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • იტენება"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • სწრაფად იტენება"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ნელა იტენება"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • დატენვა ოპტიმიზირებულია ბატარეის დასაცავად"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • დატენვა დროებით შეზღუდულია"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"განსაბლოკად დააჭირეთ მენიუს."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ქსელი ჩაკეტილია"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM არ არის"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM-ის დამატება."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM აკლია ან არ იკითხება. SIM-ის დამატება."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"გამოუყენებელი SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"თქვენი SIM სამუდამოდ გამორთულია.\n დაუკავშირდით თქვენს უკაბელო სერვისის პროვაიდერს სხვა SIM ბარათისთვის."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-ბარათი ჩაკეტილია."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM დაბლოკილია PUK-ით."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-ის განბლოკვა…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM ბარ. არაა"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ჩადეთ SIM ბარათი."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM ბარათი არ არის ან არ იკითხება. ჩადეთ SIM ბარათი."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM ბარათი გამოუსადეგარია."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"თქვენი SIM ბარათი სამუდამოდ გამოუსადეგარი გახდა.\n დაუკავშირდით თქვენი უსადენო სერვისის პროვაიდერს სხვა SIM ბარათის მისაღებად."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM ბარათი ჩაკეტილია."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM ბარათი ჩაკეტილია PUK-კოდით."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"მიმდინარეობს SIM ბარათის განბლოკვა…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-კოდის არე"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"მოწყობილობის პაროლი"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM ბარათის PIN-კოდის არე"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM ბარათი (<xliff:g id="CARRIER">%1$s</xliff:g>) ახლა დეაქტივირებულია. გასაგრძელებლად შეიყვანეთ PUK-კოდი. დეტალური ინფორმაციისთვის დაუკავშირდით თქვენს ოპერატორს."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"შეიყვანეთ სასურველი PIN-კოდი"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"დაადასტურეთ სასურველი PIN-კოდი"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-ის განბლოკვა…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"მიმდინარეობს SIM ბარათის განბლოკვა…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"აკრიფეთ 4-8 ციფრისგან შემდგარი PIN-კოდი."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-კოდი 8 ან მეტი ციფრისგან უნდა შედგებოდეს."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"თქვენ არასწორად შეიყვანეთ PIN-კოდი <xliff:g id="NUMBER_0">%1$d</xliff:g>-ჯერ. \n\nცადეთ ხელახლა <xliff:g id="NUMBER_1">%2$d</xliff:g> წამში."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"თქვენ არასწორად აკრიფეთ პაროლი <xliff:g id="NUMBER_0">%1$d</xliff:g>-ჯერ. \n\nცადეთ ხელახლა <xliff:g id="NUMBER_1">%2$d</xliff:g> წამში."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"თქვენ არასწორად დახატეთ განბლოკვის ნიმუში <xliff:g id="NUMBER_0">%1$d</xliff:g>-ჯერ. \n\nცადეთ ხელახლა <xliff:g id="NUMBER_1">%2$d</xliff:g> წამში."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM ბარათის PIN-კოდი არასწორია. ახლა თქვენი მოწყობილობის განსაბლოკად თქვენს ოპერატორთან დაკავშირება მოგიწევთ."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM-ის PIN კოდი არასწორია. თქვენ დაგრჩათ # მცდელობა, რის შემდეგაც მოწყობილობის განსაბლოკად ოპერატორთან დაკავშირება მოგიწევთ.}other{SIM ბარათის PIN-კოდი არასწორია. თქვენ დაგრჩათ # მცდელობა. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM ბარათის PIN-კოდი არასწორია. თქვენ დაგრჩათ <xliff:g id="NUMBER_1">%d</xliff:g> მცდელობა.</item>
+      <item quantity="one">SIM ბარათის PIN-კოდი არასწორია. თქვენ დაგრჩათ <xliff:g id="NUMBER_0">%d</xliff:g> მცდელობა, რომლის შემდეგაც თქვენი მოწყობილობის განსაბლოკად თქვენს ოპერატორთან დაკავშირება მოგიწევთ.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM ბარათი გამოუსადეგარია. დაუკავშირდით თქვენს ოპერატორს."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM ბარათის PUK-კოდი არასწორია. თქვენ დაგრჩათ # მცდელობა, რის შემდეგაც თქვენი SIM სამუდამოდ გამოუსადეგარი გახდება.}other{არასწორი SIM ბარათის PUK-კოდი. თქვენ დაგრჩათ # მცდელობა, სანამ SIM ბარათი სამუდამოდ გამოუსადეგარი გახდება.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM ბარათის PUK-კოდი არასწორია. თქვენ დაგრჩათ <xliff:g id="NUMBER_1">%d</xliff:g> მცდელობა, რომელთა შემდეგაც თქვენი SIM სამუდამოდ გამოუსადეგარი გახდება.</item>
+      <item quantity="one">SIM ბარათის PUK-კოდი არასწორია. თქვენ დაგრჩათ <xliff:g id="NUMBER_0">%d</xliff:g> მცდელობა, რომლის შემდეგაც თქვენი SIM სამუდამოდ გამოუსადეგარი გახდება.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM ბარათის PIN-კოდით განბლოკვა ვერ მოხერხდა!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM ბარათის PUK-კოდით განბლოკვა ვერ მოხერხდა!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"შეყვანის მეთოდის გადართვა"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა ნიმუშის დახატვა"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა PIN-კოდის შეყვანა"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა პაროლის შეყვანა"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"დამატებითი უსაფრთხოებისთვის გამოიყენეთ ნიმუში"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"დამატებითი უსაფრთხოებისთვის გამოიყენეთ PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"დამატებითი უსაფრთხოებისთვის გამოიყენეთ პაროლი"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"დამატებითი უსაფრთხოებისთვის საჭიროა ნიმუშის დახატვა"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"დამატებითი უსაფრთხოებისთვის საჭიროა PIN-კოდის შეყვანა"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"დამატებითი უსაფრთხოებისთვის საჭიროა პაროლის შეყვანა"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"მოწყობილობა ჩაკეტილია ადმინისტრატორის მიერ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"მოწყობილობა ხელით ჩაიკეტა"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"არ არის ამოცნობილი"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"სახით განბლოკვით სარგებლობისთვის, ჩართეთ კამერაზე წვდომა პარამეტრებში"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{შეიყვანეთ SIM ბარათის PIN-კოდი. თქვენ დაგრჩათ # მცდელობა, რის შემდეგაც მოწყობილობის განსაბლოკად დაგჭირდებათ თქვენს ოპერატორთან დაკავშირება.}other{შეიყვანეთ SIM ბარათის PIN-კოდი. თქვენ დაგრჩათ # მცდელობა.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM ბარათი ახლა გათიშულია. გასაგრძელებლად შეიყვანეთ PUK-კოდი. თქვენ დაგრჩათ # მცდელობა, სანამ SIM სამუდამოდ გამოუსადეგარი გახდება. დეტალური ინფორმაციისთვის დაუკავშირდით თქვენს ოპერატორს.}other{SIM ბარათი ახლა გათიშულია. გასაგრძელებლად შეიყვანეთ PUK-კოდი. თქვენ დაგრჩათ # მცდელობა, სანამ SIM სამუდამოდ გამოუსადეგარი გახდება. დეტალური ინფორმაციისთვის დაუკავშირდით თქვენს ოპერატორს.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"იმისთვის, რომ სახით განბლოკვით ისარგებლოთ, ჩართეთ "<b>"კამერაზე წვდომა"</b>" პარამეტრებსა და კონფიდენციალურობაში"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">შეიყვანეთ SIM ბარათის PIN-კოდი. თქვენ დაგრჩათ <xliff:g id="NUMBER_1">%d</xliff:g> მცდელობა.</item>
+      <item quantity="one">შეიყვანეთ SIM ბარათის PIN-კოდი. თქვენ დაგრჩათ <xliff:g id="NUMBER_0">%d</xliff:g> მცდელობა, რომლის შემდეგაც მოწყობილობის განსაბლოკად დაგჭირდებათ თქვენს ოპერატორთან დაკავშირება.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM ბარათი ახლა დეაქტივირებულია. გასაგრძელებლად შეიყვანეთ PUK-კოდი. თქვენ დაგრჩათ <xliff:g id="_NUMBER_1">%d</xliff:g> მცდელობა, სანამ SIM სამუდამოდ გამოუსადეგარი გახდება. დეტალური ინფორმაციისთვის დაუკავშირდით თქვენს ოპერატორს.</item>
+      <item quantity="one">SIM ბარათი ახლა დეაქტივირებულია. გასაგრძელებლად შეიყვანეთ PUK-კოდი. თქვენ დაგრჩათ <xliff:g id="_NUMBER_0">%d</xliff:g> მცდელობა, სანამ SIM სამუდამოდ გამოუსადეგარი გახდება. დეტალური ინფორმაციისთვის დაუკავშირდით თქვენს ოპერატორს.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ნაგულისხმევი"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ბუშტი"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ანალოგური"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"გასაგრძელებლად განბლოკეთ თქვენი მოწყობილობა"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-kk/strings.xml b/packages/SystemUI/res-keyguard/values-kk/strings.xml
index 2057b3e..20bba0f 100644
--- a/packages/SystemUI/res-keyguard/values-kk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-kk/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Жарамсыз карта."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Зарядталды"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Сымсыз зарядталуда"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядталып жатыр."</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Қондыру станциясында зарядталуда"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядталуда"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Жылдам зарядталуда"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Баяу зарядталуда"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Батареяны қорғау үшін, зарядтау оңтайландырылды"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядтау уақытша шектелген"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Ашу үшін \"Мәзір\" пернесін басыңыз."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Желі құлыптаулы"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM картасы жоқ."</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM картасын қосыңыз."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM картасы жоқ немесе оқылмай тұр. SIM картасын қосыңыз."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM картасын пайдалану мүмкін емес."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM картаңыз біржола өшірілді.\n Сымсыз байланыс провайдеріне хабарласып, басқа SIM картасын алыңыз."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM картасы құлыпталған."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM картасы PUK кодымен құлыпталды."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM картасының құлпы ашылып жатыр…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM картасы салынбаған"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM картасын енгізіңіз."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM картасы жоқ немесе оқылмайды. SIM картасын салыңыз."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM картасы қолданыстан шыққан."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM картаңыз біржола өшірілді. \n Сымсыз байланыс провайдеріне хабарласып, басқа SIM картасын алыңыз."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM картасы құлыпталған."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM картасы PUK кодымен құлыпталған."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM картасының құлпын ашуда…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN аумағы"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Құрылғы құпия сөзі"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN аумағы"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" SIM картасы қазір өшірулі. Жалғастыру үшін PUK кодын енгізіңіз. Мәліметтер алу үшін операторға хабарласыңыз."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Қажетті PIN кодын енгізіңіз"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Қажетті PIN кодын растаңыз"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM картасының құлпы ашылып жатыр…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM картасының құлпын ашуда…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4-8 саннан тұратын PIN кодын енгізіңіз."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK коды 8 не одан көп саннан тұруы қажет."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN коды <xliff:g id="NUMBER_0">%1$d</xliff:g> рет қате енгізілді. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секундтан кейін әркетті қайталаңыз."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Құпия сөз <xliff:g id="NUMBER_0">%1$d</xliff:g> рет қате енгізілді. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секундтан кейін әрекетті қайталаңыз."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Құлыпты ашу өрнегі <xliff:g id="NUMBER_0">%1$d</xliff:g> рет қате енгізілді. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секундтан кейін әрекетті қайталаңыз."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM PIN коды дұрыс емес, операторға хабарласып, құрылғының құлпын ашуды сұраңыз."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM картасының PIN коды дұрыс емес. # мүмкіндігіңіз қалды. Одан кейін құрылғы құлпын ашу үшін операторға хабарласуға тура келеді.}other{SIM картасының PIN коды дұрыс емес. # әрекет қалды. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM PIN коды дұрыс емес. <xliff:g id="NUMBER_1">%d</xliff:g> әрекет қалды.</item>
+      <item quantity="one">SIM PIN коды дұрыс емес. <xliff:g id="NUMBER_0">%d</xliff:g> әрекет қалды. Одан кейін құрылғы құлпын ашу үшін операторға хабарласуға тура келеді.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM қолданыстан шыққан. Оператормен хабарласыңыз."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM картасының PUK коды дұрыс емес, # әрекеттен кейін SIM картасы біржола құлыпталады.}other{SIM картасының PUK коды дұрыс емес, # әрекеттен кейін SIM картасы біржола құлыпталады.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM PUK коды дұрыс емес. <xliff:g id="NUMBER_1">%d</xliff:g> әрекет қалды. Одан кейін SIM біржола қолданыстан шығады.</item>
+      <item quantity="one">SIM PUK коды дұрыс емес. <xliff:g id="NUMBER_0">%d</xliff:g> әрекет қалды. Одан кейін SIM біржола қолданыстан шығады.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN кодымен құлпы ашылмады!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK кодымен құлпы ашылмады!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Енгізу әдісін ауыстыру"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Құрылғы қайта іске қосылғаннан кейін, өрнекті енгізу қажет"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Құрылғы қайта іске қосылғаннан кейін, PIN кодын енгізу қажет"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Құрылғы қайта іске қосылғаннан кейін, құпия сөзді енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Қосымша қауіпсіздік үшін өрнекті пайдаланыңыз."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Қосымша қауіпсіздік үшін PIN кодын пайдаланыңыз."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Қосымша қауіпсіздік үшін құпия сөзді пайдаланыңыз."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Қауіпсіздікті күшейту үшін өрнекті енгізу қажет"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Қауіпсіздікті күшейту үшін PIN кодын енгізу қажет"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Қауіпсіздікті күшейту үшін құпия сөзді енгізу қажет"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Құрылғыны әкімші құлыптаған"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Құрылғы қолмен құлыпталды"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Танылмады"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Face Unlock функциясын пайдалану үшін параметрлерден камераны пайдалану рұқсатын қосыңыз."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM картасының PIN кодын енгізіңіз. # мүмкіндік қалды, одан кейін оператордан SIM картасының құлпын ашуды сұрауға тура келеді.}other{SIM картасының PIN кодын енгізіңіз. # әрекет қалды.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM картасы өшірілді. Жалғастыру үшін PUK кодын енгізіңіз. # мүмкіндік қалды, одан кейін SIM картасы біржола құлыпталады. Толығырақ мәліметті оператордан алыңыз.}other{SIM картасы өшірілді. Жалғастыру үшін PUK кодын енгізіңіз. # мүмкіндік қалды, одан кейін SIM картасы біржола құлыпталады. Толығырақ мәліметті оператордан алыңыз.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Face Unlock функциясын пайдалану үшін \"Параметрлер &gt; Құпиялылық\" бөлімінен "<b>"Камераны пайдалану рұқсатын"</b>" қосыңыз."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN кодын енгізіңіз. <xliff:g id="NUMBER_1">%d</xliff:g> мүмкіндік қалды, одан кейін оператордан SIM картасының құлпын ашуды сұрауға тура келеді.</item>
+      <item quantity="one">SIM PIN кодын енгізіңіз. <xliff:g id="NUMBER_0">%d</xliff:g> мүмкіндік қалды, одан кейін оператордан SIM картасының құлпын ашуды сұрауға тура келеді.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM картасы өшірілді. Жалғастыру үшін PUK кодын енгізіңіз. <xliff:g id="_NUMBER_1">%d</xliff:g> мүмкіндік қалды, одан кейін SIM картасы біржола құлыпталады. Толығырақ мәліметті оператордан алыңыз.</item>
+      <item quantity="one">SIM картасы өшірілді. Жалғастыру үшін PUK кодын енгізіңіз. <xliff:g id="_NUMBER_0">%d</xliff:g> мүмкіндік қалды, одан кейін SIM картасы біржола құлыпталады. Толығырақ мәліметті оператордан алыңыз.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Әдепкі"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Көпіршік"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогтық"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Жалғастыру үшін құрылғының құлпын ашыңыз"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-km/strings.xml b/packages/SystemUI/res-keyguard/values-km/strings.xml
index 2e27833..236c318 100644
--- a/packages/SystemUI/res-keyguard/values-km/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-km/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"បណ្ណមិនត្រឹមត្រូវទេ។"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"បាន​សាក​ថ្មពេញ"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • កំពុងសាកថ្ម​ឥតខ្សែ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • កំពុង​សាកថ្ម"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ឧបករណ៍ភ្ជាប់សាកថ្ម"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • កំពុង​សាកថ្ម"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • កំពុង​សាកថ្មយ៉ាង​ឆាប់រហ័ស"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • កំពុង​សាកថ្មយឺត"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • បានបង្កើនប្រសិទ្ធភាពនៃការសាក ដើម្បីការពារថ្ម"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • បានដាក់កំហិតលើ​ការសាកថ្មជា​បណ្ដោះអាសន្ន"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"ចុចម៉ឺនុយ ​ដើម្បី​ដោះ​សោ។"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"បណ្ដាញ​ជាប់​សោ"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"គ្មានស៊ីមទេ"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"បញ្ចូល​ស៊ីម។"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"បាត់ស៊ីម ឬមិនអាចអានស៊ីមបាន។ បញ្ចូល​ស៊ីម។"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ស៊ីមមិនអាចប្រើបាន។"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ស៊ីមរបស់អ្នកត្រូវបានបិទដំណើរការជាអចិន្ត្រៃយ៍។\n ទាក់ទងទៅក្រុមហ៊ុនផ្ដល់សេវាឥតខ្សែរបស់អ្នក ដើម្បីទទួលបានស៊ីមមួយទៀត។"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"ស៊ីម​ត្រូវបាន​ចាក់សោ។"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"ស៊ីមត្រូវបានចាក់សោដោយ PUK។"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"កំពុងដោះសោស៊ីម…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"គ្មាន​ស៊ីម​កាត​ទេ"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ស៊ក​បញ្ចូល​ស៊ីម​កាត​។"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ស៊ីមកាត​បាន​បាត់ ឬ​មិន​អាច​អាន​បាន។ សូម​ស៊ក​បញ្ចូល​ស៊ីម​កាត។"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"ស៊ី​ម​កាត​មិន​អាច​ប្រើ​បាន​ទេ។"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ស៊ីម​កាត​របស់​អ្នក​ត្រូវ​បាន​បិទដំណើរការ​ជា​អចិន្ត្រៃយ៍។\n សូម​ទាក់ទង​ទៅក្រុមហ៊ុន​ផ្ដល់​សេវាកម្ម​ទំនាក់ទំនង​ឥត​ខ្សែ ដើម្បី​ទទួល​បាន​ស៊ីម​កាត​ផ្សេង។"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"ស៊ីមកាត​ជាប់​សោ។"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ស៊ីម​កាត​ជាប់​កូដ​ PUK ។"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"កំពុង​ដោះ​សោ​ស៊ីមកាត..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"ប្រអប់​បំពេញ​កូដ PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ពាក្យសម្ងាត់​ឧបករណ៍"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"ប្រអប់​បំពេញ​កូដ PIN របស់​ស៊ីម"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ឥឡូវ​នេះ​ ស៊ីម \"<xliff:g id="CARRIER">%1$s</xliff:g>\" ត្រូវ​បាន​បិទ​ដំណើរការហើយ។ បញ្ចូល​កូដ PUK ដើម្បី​បន្ត។ សូម​ទាក់ទង​ទៅក្រុមហ៊ុន​បម្រើ​សេវា​ទូរសព្ទ​របស់​អ្នក ដើម្បី​ទទួល​បាន​ព័ត៌មាន​លម្អិត។"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"បញ្ចូល​កូដ PIN ដែល​ចង់​បាន"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"បញ្ជាក់​កូដ PIN ដែល​ចង់​បាន"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"កំពុងដោះសោស៊ីម…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"កំពុង​ដោះ​សោ​ស៊ីមកាត..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"វាយ​បញ្ចូល​កូដ PIN ​ចន្លោះពី 4 ទៅ 8 ខ្ទង់"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"កូដ PUK គួរ​តែ​មាន​លេខ 8 ខ្ទង់ ឬ​ច្រើន​ជាង​នេះ។"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"អ្នក​បាន​វាយ​បញ្ចូល​កូដ PIN របស់​អ្នក​មិន​ត្រឹមត្រូវ​ចំនួន <xliff:g id="NUMBER_0">%1$d</xliff:g> ដង​ហើយ។ \n\nសូម​ព្យាយាម​ម្ដង​ទៀត​ក្នុង​រយៈ​ពេល <xliff:g id="NUMBER_1">%2$d</xliff:g> វិនាទី​ទៀត។"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"អ្នក​បាន​វាយ​បញ្ចូល​ពាក្យ​សម្ងាត់​របស់​អ្នក​មិន​ត្រឹមត្រូវ​ចំនួន <xliff:g id="NUMBER_0">%1$d</xliff:g> ដង​ហើយ។ \n\nសូម​ព្យាយាម​ម្ដង​ទៀត​ក្នុង​រយៈ​ពេល <xliff:g id="NUMBER_1">%2$d</xliff:g> វិនាទី​ទៀត។"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"អ្នក​បាន​គូរ​លំនាំ​ដោះ​សោ​របស់​អ្នក​មិន​ត្រឹមត្រូវ​ចំនួន <xliff:g id="NUMBER_0">%1$d</xliff:g> ដង​ហើយ។ \n\nសូមព្យាយាម​ម្ដង​ទៀត​ក្នុង​រយៈ​ពេល <xliff:g id="NUMBER_1">%2$d</xliff:g> វិនាទី​ទៀត។"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"កូដ PIN របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ទេ អ្នក​ត្រូវ​ទាក់ទង​ទៅក្រុមហ៊ុន​បម្រើ​សេវា​ទូរសព្ទ​របស់​អ្នក​ឥឡូវ​នេះ ដើម្បី​ដោះ​សោ​ឧបករណ៍​របស់​អ្នក។"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{កូដ PIN របស់ស៊ីម​មិនត្រឹមត្រូវទេ អ្នកអាច​ព្យាយាម​បញ្ចូលបាន # ដងទៀត មុនពេលដែល​ត្រូវទាក់ទង​ទៅក្រុមហ៊ុន​សេវាទូរសព្ទ​របស់អ្នក ដើម្បី​ដោះសោ​ឧបករណ៍​របស់អ្នក។}other{លេខ​កូដ​ PIN របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ អ្នកនៅ​សល់​ការ​ព្យាយាម # ដង​ទៀត។ }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">កូដ PIN របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ទេ អ្នក​អាច​ព្យាយាម​បាន <xliff:g id="NUMBER_1">%d</xliff:g> ដងទៀត។</item>
+      <item quantity="one">កូដ PIN របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ទេ ប្រសិន​បើ​អ្នក​បញ្ចូល​កូដខុស <xliff:g id="NUMBER_0">%d</xliff:g> ដងទៀត អ្នក​ត្រូវ​ទាក់ទង​ទៅ​ក្រុមហ៊ុន​បម្រើ​សេវាទូរសព្ទ​របស់អ្នក ដើម្បី​ដោះសោ​ឧបករណ៍របស់អ្នក។</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ស៊ីម​មិន​អាច​ប្រើ​បាន​ទេ។ សូម​ទាក់ទង​ទៅក្រុមហ៊ុន​បម្រើ​សេវា​ទូរសព្ទ​របស់​អ្នក។"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{កូដ PUK របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ទេ អ្នក​នៅ​សល់​ការ​ព្យាយាម # ដង​ទៀត​ មុន​ពេល​​ស៊ីម​មិន​អាច​ប្រើ​បាន​ជាអចិន្ត្រៃយ៍។}other{លេខ​កូដ PUK ស៊ីម​មិន​ត្រឹមត្រូវ អ្នកនៅ​សល់​កា​រព្យាយាម # ដង​ទៀត មុន​ពេល​ស៊ីម​មិន​អាច​ប្រើបាន​ជាអចិន្ត្រៃយ៍។}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">កូដ PUK របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ទេ ប្រសិន​បើ​អ្នក​បញ្ចូល​កូដខុស <xliff:g id="NUMBER_1">%d</xliff:g> ដងទៀត ស៊ីមនឹង​មិន​អាច​ប្រើ​បាន​ជា​អចិន្ត្រៃយ៍។</item>
+      <item quantity="one">កូដ PUK របស់​ស៊ីម​មិន​ត្រឹមត្រូវ​ទេ ប្រសិន​បើ​អ្នក​បញ្ចូល​កូដខុស <xliff:g id="NUMBER_0">%d</xliff:g> ដងទៀត ស៊ីមនឹង​មិន​អាច​ប្រើ​បាន​ជា​អចិន្ត្រៃយ៍។</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"មិន​អាច​ដោះ​សោ​ដោយ​ប្រើកូដ​ PIN របស់​ស៊ីម​បានទេ!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"មិន​អាច​ដោះ​សោ​ដោយ​ប្រើកូដ​ PUK របស់​ស៊ីម​បានទេ!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ប្ដូរ​វិធី​បញ្ចូល"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"តម្រូវឲ្យប្រើលំនាំ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"តម្រូវឲ្យបញ្ចូលកូដ PIN បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ដើម្បីសុវត្ថិភាពបន្ថែម សូមប្រើលំនាំជំនួសវិញ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ដើម្បីសុវត្ថិភាពបន្ថែម សូមប្រើកូដ PIN ជំនួសវិញ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ដើម្បីសុវត្ថិភាពបន្ថែម សូមប្រើពាក្យសម្ងាត់ជំនួសវិញ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"តម្រូវឲ្យប្រើលំនាំ ដើម្បីទទួលបានសវុត្ថិភាពបន្ថែម"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"តម្រូវឲ្យបញ្ចូលកូដ PIN ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ឧបករណ៍​ត្រូវបាន​ចាក់សោ​ដោយអ្នក​គ្រប់គ្រង"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ឧបករណ៍ត្រូវបានចាក់សោដោយអ្នកប្រើផ្ទាល់"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"មិនអាចសម្គាល់បានទេ"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ដើម្បីដោះសោតាមទម្រង់មុខ សូមបើកសិទ្ធិចូលប្រើកាមេរ៉ានៅក្នុងការកំណត់"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{បញ្ចូលកូដ PIN របស់ស៊ីម។ អ្នក​នៅសល់​ការព្យាយាម # ដង​ទៀត មុន​ពេល​ដែលអ្នក​ត្រូវទាក់ទង​ទៅ​ក្រុមហ៊ុន​សេវា​ទូរសព្ទ​របស់អ្នក​ដើម្បី​ដោះសោ​ឧបករណ៍​របស់អ្នក។}other{បញ្ចូល​កូដ PIN របស់​ស៊ីម។ អ្នកនៅ​សល់​ការ​ព្យាយាម ​# ដង​ទៀត។}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ឥឡូវនេះស៊ីមត្រូវបានបិទ។ សូមបញ្ចូលកូដ PUK ដើម្បីបន្ត។ អ្នកនៅសល់ការព្យាយាម # ដងទៀតមុនពេលស៊ីមមិនអាចប្រើបានជាអចិន្ត្រៃយ៍។ ទាក់ទង​ទៅ​ក្រុមហ៊ុន​សេវា​ទូរសព្ទ​សម្រាប់ព័ត៌មានលម្អិត។}other{ឥឡូវនេះស៊ីមត្រូវបានបិទ។ សូមបញ្ចូលកូដ PUK ដើម្បីបន្ត។ អ្នកនៅសល់ការព្យាយាម # ដងទៀត​មុនពេល​ស៊ីម​មិនអាច​ប្រើបាន​ជា​អចិន្ត្រៃយ៍។ ទាក់ទង​ទៅ​ក្រុមហ៊ុន​សេវា​ទូរសព្ទ​សម្រាប់ព័ត៌មានលម្អិត។}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ដើម្បីប្រើមុខងារដោះសោតាមទម្រង់មុខ សូមបើក"<b>"ការចូលប្រើកាមេរ៉ា"</b>"នៅក្នុងការកំណត់ &gt; ឯកជនភាព"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">បញ្ចូល​កូដ PIN របស់ស៊ីម។ អ្នកនៅ​សល់ការ​ព្យាយាម <xliff:g id="NUMBER_1">%d</xliff:g> ដងទៀត។</item>
+      <item quantity="one">បញ្ចូលកូដ PIN របស់ស៊ីម។ អ្នក​នៅសល់​ការព្យាយាម <xliff:g id="NUMBER_0">%d</xliff:g> ដង​ទៀត មុន​ពេល​ដែលអ្នក​ត្រូវទាក់ទង​ទៅ​ក្រុមហ៊ុន​សេវា​ទូរសព្ទ​របស់អ្នក​ដើម្បី​ដោះសោ​ឧបករណ៍​របស់អ្នក។</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">ឥឡូវនេះស៊ីមត្រូវបានបិទ។ សូមបញ្ចូលកូដ PUK ដើម្បីបន្ត។ អ្នកនៅសល់ការព្យាយាម <xliff:g id="_NUMBER_1">%d</xliff:g> ដងទៀត​មុនពេល​ស៊ីម​មិនអាច​ប្រើបាន​ជា​អចិន្ត្រៃយ៍។ ទាក់ទង​ទៅ​ក្រុមហ៊ុន​សេវា​ទូរសព្ទ​សម្រាប់ព័ត៌មានលម្អិត។</item>
+      <item quantity="one">ឥឡូវនេះស៊ីមត្រូវបានបិទ។ សូមបញ្ចូលកូដ PUK ដើម្បីបន្ត។ អ្នកនៅសល់ការព្យាយាម <xliff:g id="_NUMBER_0">%d</xliff:g> ដងទៀតមុនពេលស៊ីមមិនអាចប្រើបានជាអចិន្ត្រៃយ៍។ ទាក់ទង​ទៅ​ក្រុមហ៊ុន​សេវា​ទូរសព្ទ​សម្រាប់​ព័ត៌មាន​លម្អិត។</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"លំនាំដើម"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ពពុះ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"អាណាឡូក"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ដោះសោឧបករណ៍របស់អ្នកដើម្បីបន្ត"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-kn/strings.xml b/packages/SystemUI/res-keyguard/values-kn/strings.xml
index 9ed5912..716cbf6 100644
--- a/packages/SystemUI/res-keyguard/values-kn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-kn/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ಅಮಾನ್ಯ ಕಾರ್ಡ್."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ಚಾರ್ಜ್ ಆಗಿದೆ"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ವೈರ್‌ಲೆಸ್ ಆಗಿ ಚಾರ್ಜ್ ಆಗುತ್ತಿದೆ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ಚಾರ್ಜ್ ಆಗುತ್ತಿದೆ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ಚಾರ್ಜಿಂಗ್ ಡಾಕ್"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ಚಾರ್ಜ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ವೇಗವಾಗಿ ಚಾರ್ಜ್‌ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ನಿಧಾನವಾಗಿ ಚಾರ್ಜ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ಬ್ಯಾಟರಿಯನ್ನು ರಕ್ಷಿಸಲು ಚಾರ್ಜಿಂಗ್ ಅನ್ನು ಆಪ್ಟಿಮೈಸ್ ಮಾಡಲಾಗಿದೆ"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ಚಾರ್ಜಿಂಗ್ ಅನ್ನು ತಾತ್ಕಾಲಿಕವಾಗಿ ಸೀಮಿತಗೊಳಿಸಲಾಗಿದೆ"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"ಅನ್‌ಲಾಕ್ ಮಾಡಲು ಮೆನು ಒತ್ತಿರಿ."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ನೆಟ್‌ವರ್ಕ್ ಲಾಕ್ ಆಗಿದೆ"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM ಇಲ್ಲ"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM ಅನ್ನು ಸೇರಿಸಿ."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM ಕಾಣೆಯಾಗಿದೆ ಅಥವಾ ರೀಡ್ ಆಗುತ್ತಿಲ್ಲ. SIM ಅನ್ನು ಸೇರಿಸಿ."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM ನಿಷ್ಪ್ರಯೋಜಕವಾಗಿದೆ."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ನಿಮ್ಮ SIM ಅನ್ನು ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ.\n ಬೇರೊಂದು SIM ಗಾಗಿ ನಿಮ್ಮ ವೈರ್‌ಲೆಸ್ ಸೇವಾ ಪೂರೈಕೆದಾರರನ್ನು ಸಂಪರ್ಕಿಸಿ."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM ಲಾಕ್ ಆಗಿದೆ."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM PUK ಲಾಕ್ ಆಗಿದೆ."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM ಅನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡಲಾಗುತ್ತಿದೆ…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"ಸಿಮ್‌ ಕಾರ್ಡ್ ಇಲ್ಲ"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ಸಿಮ್‌ ಕಾರ್ಡ್ ಸೇರಿಸಿ."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ಸಿಮ್‌ ಕಾರ್ಡ್ ಇಲ್ಲ ಅಥವಾ ಗುರುತಿಸಲು ಅಸಾಧ್ಯ. ಒಂದು ಸಿಮ್‌ ಕಾರ್ಡ್ ಸೇರಿಸಿ."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"ನಿಷ್ಪ್ರಯೋಜಕ ಸಿಮ್‌ ಕಾರ್ಡ್."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ನಿಮ್ಮ ಸಿಮ್‌ ಕಾರ್ಡ್ ಅನ್ನು ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ.\n ಮತ್ತೊಂದು ಸಿಮ್‌ ಕಾರ್ಡ್‌ಗಾಗಿ ನಿಮ್ಮ ವೈರ್‌ಲೆಸ್ ಸೇವೆ ಪೂರೈಕೆದಾರರನ್ನು ಸಂಪರ್ಕಿಸಿ."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"ಸಿಮ್‌ ಕಾರ್ಡ್ ಲಾಕ್ ಆಗಿದೆ."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ಸಿಮ್‌ ಕಾರ್ಡ್ PUK-ಲಾಕ್ ಆಗಿದೆ."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"ಸಿಮ್‌ ಕಾರ್ಡ್ ಅನ್‌ಲಾಕ್ ಮಾಡಲಾಗುತ್ತಿದೆ…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"ಪಿನ್ ಪ್ರದೇಶ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ಸಾಧನದ ಪಾಸ್‌ವರ್ಡ್‌"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"ಸಿಮ್ ಪಿನ್ ಪ್ರದೇಶ"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ಈಗ \"<xliff:g id="CARRIER">%1$s</xliff:g>\" ಸಿಮ್ ನಿಷ್ಕ್ರಿಯಗೊಂಡಿದೆ. ಮುಂದುವರೆಯಲು PUK ಕೋಡ್ ನಮೂದಿಸಿ. ಮಾಹಿತಿಗಾಗಿ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"ಬಯಸಿರುವ ಪಿನ್‌ ಕೋಡ್ ನಮೂದಿಸಿ"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"ಬಯಸಿರುವ ಪಿನ್‌ ಕೋಡ್ ದೃಢೀಕರಿಸಿ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM ಅನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡಲಾಗುತ್ತಿದೆ…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"ಸಿಮ್‌ ಕಾರ್ಡ್ ಅನ್‌ಲಾಕ್ ಮಾಡಲಾಗುತ್ತಿದೆ…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 ರಿಂದ 8 ಸಂಖ್ಯೆಗಳಿರುವ ಪಿನ್‌ ಟೈಪ್ ಮಾಡಿ."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK ಕೋಡ್ 8 ಅಥವಾ ಹೆಚ್ಚು ಸಂಖ್ಯೆಗಳನ್ನು ಹೊಂದಿರಬೇಕು."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"ನಿಮ್ಮ ಪಿನ್‌ ಅನ್ನು ನೀವು <xliff:g id="NUMBER_0">%1$d</xliff:g> ಬಾರಿ ತಪ್ಪಾಗಿ ಟೈಪ್ ಮಾಡಿದ್ದೀರಿ. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ಸೆಕೆಂಡುಗಳಲ್ಲಿ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"ನಿಮ್ಮ ಪಾಸ್‍‍ವರ್ಡ್ ಅನ್ನು ನೀವು <xliff:g id="NUMBER_0">%1$d</xliff:g> ಬಾರಿ ತಪ್ಪಾಗಿ ನಮೂದಿಸಿದ್ದೀರಿ. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> ಸೆಕೆಂಡುಗಳಲ್ಲಿ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"ನಿಮ್ಮ ಅನ್‍‍ಲಾಕ್ ಪ್ಯಾಟರ್ನ್‌ ಅನ್ನು <xliff:g id="NUMBER_0">%1$d</xliff:g> ಬಾರಿ ತಪ್ಪಾಗಿ ಎಳೆದಿದ್ದೀರಿ. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ಸೆಕೆಂಡುಗಳಲ್ಲಿ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ಸಿಮ್‌ ಪಿನ್‌ ಕೋಡ್‌ ತಪ್ಪಾಗಿದೆ, ನಿಮ್ಮ ಸಾಧನವನ್ನು ಅನ್‌ಲಾಕ್‌ ಮಾಡಲು ನೀವು ಈ ಕೂಡಲೇ ನಿಮ್ಮ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಬೇಕು."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM ಪಿನ್ ಕೋಡ್ ತಪ್ಪಾಗಿದೆ, ನಿಮ್ಮ ಸಾಧನವನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡುವುದಕ್ಕಾಗಿ ನಿಮ್ಮ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನ ಬಾಕಿ ಉಳಿದಿದೆ.}one{SIM ಪಿನ್ ಕೋಡ್ ತಪ್ಪಾಗಿದೆ, ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ. }other{SIM ಪಿನ್ ಕೋಡ್ ತಪ್ಪಾಗಿದೆ, ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">ಸಿಮ್‌ ಪಿನ್ ಕೋಡ್‌ ತಪ್ಪಾಗಿದೆ, ನಿಮಗೆ <xliff:g id="NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.</item>
+      <item quantity="other">ಸಿಮ್‌ ಪಿನ್ ಕೋಡ್‌ ತಪ್ಪಾಗಿದೆ, ನಿಮಗೆ <xliff:g id="NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ಸಿಮ್‌ ನಿಷ್ಪ್ರಯೋಜಕವಾಗಿದೆ. ನಿಮ್ಮ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM PUK ಕೋಡ್ ತಪ್ಪಾಗಿದೆ, SIM ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನ ಬಾಕಿ ಉಳಿದಿದೆ.}one{SIM PUK ಕೋಡ್ ತಪ್ಪಾಗಿದೆ, SIM ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.}other{SIM PUK ಕೋಡ್ ತಪ್ಪಾಗಿದೆ, SIM ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">ಸಿಮ್‌ PUK ಕೋಡ್‌ ತಪ್ಪಾಗಿದೆ, <xliff:g id="NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳ ನಂತರ ಸಿಮ್‌ ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುತ್ತದೆ.</item>
+      <item quantity="other">ಸಿಮ್‌ PUK ಕೋಡ್‌ ತಪ್ಪಾಗಿದೆ, <xliff:g id="NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳ ನಂತರ ಸಿಮ್‌ ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುತ್ತದೆ.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"ಸಿಮ್‌ ಪಿನ್‌ ಕಾರ್ಯಾಚರಣೆ ವಿಫಲಗೊಂಡಿದೆ!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"ಸಿಮ್‌ PUK ಕಾರ್ಯಾಚರಣೆ ವಿಫಲಗೊಂಡಿದೆ!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ಇನ್‌ಪುಟ್‌‌ ವಿಧಾನ ಬದಲಿಸಿ"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಿನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ, ಬದಲಿಗೆ ಪ್ಯಾಟರ್ನ್ ಅನ್ನು ಬಳಸಿ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ, ಬದಲಿಗೆ ಪಿನ್ ಬಳಸಿ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ, ಬದಲಿಗೆ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬಳಸಿ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿದೆ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪಿನ್ ಅಗತ್ಯವಿದೆ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿದೆ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ನಿರ್ವಾಹಕರು ಸಾಧನವನ್ನು ಲಾಕ್ ಮಾಡಿದ್ದಾರೆ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ಸಾಧನವನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಲಾಕ್‌ ಮಾಡಲಾಗಿದೆ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ಗುರುತಿಸಲಾಗಿಲ್ಲ"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ಫೇಸ್ ಅನ್‌ಲಾಕ್ ಬಳಸಲು, ಸೆಟ್ಟಿಂಗ್ಸ್‌ನಲ್ಲಿ ಕ್ಯಾಮರಾ ಪ್ರವೇಶ ಆನ್ ಮಾಡಿ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM ಪಿನ್ ಅನ್ನು ನಮೂದಿಸಿ, ನಿಮ್ಮ ಸಾಧನವನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡುವುದಕ್ಕಾಗಿ ನಿಮ್ಮ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನ ಬಾಕಿ ಉಳಿದಿದೆ.}one{SIM ಪಿನ್ ಅನ್ನು ನಮೂದಿಸಿ. ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.}other{SIM ಪಿನ್ ಅನ್ನು ನಮೂದಿಸಿ. ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM ಅನ್ನು ಈಗ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಮುಂದುವರಿಸಲು PUK ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ. SIM ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನ ಬಾಕಿ ಉಳಿದಿದೆ. ವಿವರಗಳಿಗಾಗಿ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ.}one{SIM ಅನ್ನು ಈಗ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಮುಂದುವರಿಸಲು PUK ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ. SIM ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ. ವಿವರಗಳಿಗಾಗಿ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ.}other{SIM ಅನ್ನು ಈಗ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಮುಂದುವರಿಸಲು PUK ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ. SIM ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ # ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ. ವಿವರಗಳಿಗಾಗಿ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ಫೇಸ್ ಅನ್‌ಲಾಕ್ ಬಳಸಲು, ಸೆಟ್ಟಿಂಗ್‌ಗಳು &gt; ಗೌಪ್ಯತೆ ಎಂಬಲ್ಲಿ "<b>"ಕ್ಯಾಮರಾ ಪ್ರವೇಶವನ್ನು"</b>" ಆನ್ ಮಾಡಿ"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">ಸಿಮ್ ಪಿನ್ ನಮೂದಿಸಿ. ನಿಮ್ಮಲ್ಲಿ <xliff:g id="NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.</item>
+      <item quantity="other">ಸಿಮ್ ಪಿನ್ ನಮೂದಿಸಿ. ನಿಮ್ಮಲ್ಲಿ <xliff:g id="NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">ಸಿಮ್ ಅನ್ನು ಈಗ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಮುಂದುವರಿಸಲು PUK ಕೋಡ್ ನಮೂದಿಸಿ. ಸಿಮ್ ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ <xliff:g id="_NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ. ವಿವರಗಳಿಗಾಗಿ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ.</item>
+      <item quantity="other">ಸಿಮ್ ಅನ್ನು ಈಗ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಮುಂದುವರಿಸಲು PUK ಕೋಡ್ ನಮೂದಿಸಿ. ಸಿಮ್ ಶಾಶ್ವತವಾಗಿ ನಿಷ್ಪ್ರಯೋಜಕವಾಗುವ ಮುನ್ನ ನಿಮ್ಮಲ್ಲಿ <xliff:g id="_NUMBER_1">%d</xliff:g> ಪ್ರಯತ್ನಗಳು ಬಾಕಿ ಉಳಿದಿವೆ. ವಿವರಗಳಿಗಾಗಿ ವಾಹಕವನ್ನು ಸಂಪರ್ಕಿಸಿ.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ಡೀಫಾಲ್ಟ್"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ಬಬಲ್"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ಅನಲಾಗ್"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ಮುಂದುವರಿಸಲು, ನಿಮ್ಮ ಸಾಧನವನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡಿ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ko/strings.xml b/packages/SystemUI/res-keyguard/values-ko/strings.xml
index 0019cd2..e698017 100644
--- a/packages/SystemUI/res-keyguard/values-ko/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ko/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"유효하지 않은 카드"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"충전됨"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 무선 충전 중"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 충전 중"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 충전 도크"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 충전 중"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 고속 충전 중"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 저속 충전 중"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 배터리 보호를 위해 충전 최적화됨"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 충전이 일시적으로 제한됨"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"잠금 해제하려면 메뉴를 누르세요."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"네트워크 잠김"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM 없음"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM을 추가하세요."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM이 없거나 SIM을 읽을 수 없습니다. SIM을 추가하세요."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM을 사용할 수 없습니다."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM이 영구적으로 비활성화되었습니다.\n 다른 SIM을 사용하려면 무선 서비스 제공업체에 문의하시기 바랍니다."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM이 잠김 상태입니다."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM이 PUK 잠김 상태입니다."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM 잠금 해제 중…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM 카드 없음"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM 카드를 삽입하세요."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM 카드가 없거나 읽을 수 없는 상태입니다. SIM 카드를 삽입하세요."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"사용할 수 없는 SIM 카드입니다."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM 카드가 영구적으로 사용 중지되었습니다.\n무선 서비스 제공업체에 문의하여 다른 SIM 카드를 받으시기 바랍니다."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM 카드가 잠겨 있습니다."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM 카드가 PUK로 잠겨 있습니다."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM 카드 잠금 해제 중..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN 영역"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"기기 비밀번호"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN 영역"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \'<xliff:g id="CARRIER">%1$s</xliff:g>\'이(가) 사용 중지되었습니다. 계속하려면 PUK 코드를 입력하세요. 자세한 내용은 이동통신사에 문의하시기 바랍니다."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"원하는 PIN 코드 입력"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"원하는 PIN 코드 확인"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM 잠금 해제 중…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM 카드 잠금 해제 중..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4~8자리 숫자로 된 PIN을 입력하세요."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK 코드는 8자리 이상의 숫자여야 합니다."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN을 <xliff:g id="NUMBER_0">%1$d</xliff:g>번 잘못 입력했습니다. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>초 후에 다시 시도하세요."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"비밀번호를 <xliff:g id="NUMBER_0">%1$d</xliff:g>번 잘못 입력했습니다. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>초 후에 다시 시도하세요."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"잠금해제 패턴을 <xliff:g id="NUMBER_0">%1$d</xliff:g>번 잘못 그렸습니다. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>초 후에 다시 시도하세요."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"잘못된 SIM PIN코드입니다. 이동통신사에 문의하여 기기를 잠금 해제해야 합니다."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM의 PIN 코드가 잘못되었습니다. #회 이상 입력에 실패할 경우 이동통신사에 문의하여 기기를 잠금 해제해야 합니다.}other{SIM의 PIN 코드가 잘못되었습니다. 남은 시도 횟수는 #회입니다. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">잘못된 SIM PIN 코드입니다. 입력을 <xliff:g id="NUMBER_1">%d</xliff:g>번 더 시도할 수 있습니다.</item>
+      <item quantity="one">잘못된 SIM PIN 코드입니다. 입력에 <xliff:g id="NUMBER_0">%d</xliff:g>번 더 실패할 경우 이동통신사에 문의하여 기기를 잠금 해제해야 합니다.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM을 사용할 수 없습니다. 이동통신사에 문의하세요."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM의 PUK 코드가 잘못되었습니다. #회 이상 입력에 실패할 경우 SIM을 완전히 사용할 수 없게 됩니다.}other{SIM의 PUK 코드가 잘못되었습니다. #회 이상 입력에 실패할 경우 SIM을 완전히 사용할 수 없게 됩니다.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">잘못된 SIM PUK 코드입니다. 입력에 <xliff:g id="NUMBER_1">%d</xliff:g>번 더 실패할 경우 SIM이 영구적으로 사용 중지됩니다.</item>
+      <item quantity="one">잘못된 SIM PUK 코드입니다. 입력에 <xliff:g id="NUMBER_0">%d</xliff:g>번 더 실패할 경우 SIM이 영구적으로 사용 중지됩니다.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN 작업이 실패했습니다."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK 작업이 실패했습니다."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"입력 방법 전환"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"기기가 다시 시작되면 패턴이 필요합니다."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"기기가 다시 시작되면 PIN이 필요합니다."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"기기가 다시 시작되면 비밀번호가 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"보안 강화를 위해 대신 패턴 사용"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"보안 강화를 위해 대신 PIN 사용"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"보안 강화를 위해 대신 비밀번호 사용"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"보안 강화를 위해 패턴이 필요합니다."</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"보안 강화를 위해 PIN이 필요합니다."</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"보안 강화를 위해 비밀번호가 필요합니다."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"관리자가 기기를 잠갔습니다."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"기기가 수동으로 잠금 설정되었습니다."</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"인식할 수 없음"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"얼굴 인식 잠금 해제를 사용하려면 설정에서 카메라 액세스를 사용 설정하세요."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM의 PIN을 입력하세요. #회 이상 입력에 실패할 경우 이동통신사에 문의하여 기기를 잠금 해제해야 합니다.}other{SIM의 PIN을 입력하세요. 남은 시도 횟수는 #회입니다.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM이 사용 중지되었습니다. 계속하려면 PUK 코드를 입력하세요. #회 이상 입력에 실패할 경우 SIM을 완전히 사용할 수 없게 됩니다. 자세한 내용은 이동통신사에 문의하세요.}other{SIM이 사용 중지되었습니다. 계속하려면 PUK 코드를 입력하세요. #회 이상 입력에 실패할 경우 SIM을 완전히 사용할 수 없게 됩니다. 자세한 내용은 이동통신사에 문의하세요.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"얼굴 인식 잠금 해제를 사용하려면 설정 &gt; 개인 정보 보호에서 "<b>"카메라 액세스"</b>"를 사용 설정하세요."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN을 입력하세요. 입력은 <xliff:g id="NUMBER_1">%d</xliff:g>번 더 시도할 수 있습니다.</item>
+      <item quantity="one">SIM PIN을 입력하세요. 입력에 <xliff:g id="NUMBER_0">%d</xliff:g>번 더 실패하면 이동통신사에 문의하여 기기를 잠금 해제해야 합니다.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM이 사용 중지되었습니다. 계속하려면 PUK 코드를 입력하세요. <xliff:g id="_NUMBER_1">%d</xliff:g>번 더 실패하면 SIM을 완전히 사용할 수 없게 됩니다. 자세한 내용은 이동통신사에 문의하세요.</item>
+      <item quantity="one">SIM이 사용 중지되었습니다. 계속하려면 PUK 코드를 입력하세요. <xliff:g id="_NUMBER_0">%d</xliff:g>번 더 실패하면 SIM을 완전히 사용할 수 없게 됩니다. 자세한 내용은 이동통신사에 문의하세요.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"기본"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"버블"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"아날로그"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"기기를 잠금 해제하여 계속"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ky/strings.xml b/packages/SystemUI/res-keyguard/values-ky/strings.xml
index da646f1..1df29e1b 100644
--- a/packages/SystemUI/res-keyguard/values-ky/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ky/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"SIM-карта жараксыз."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Кубатталды"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зымсыз кубатталууда"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Кубатталууда"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Кубаттоо догу"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Кубатталууда"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Тез кубатталууда"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Жай кубатталууда"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Батареяны коргоо үчүн кубаттоо процесси оптималдаштырылды"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Кубаттоо убактылуу чектелген"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Кулпуну ачуу үчүн Менюну басыңыз."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Тармак кулпуланган"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM карта жок"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM карта кошуңуз."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM карта жок же окулбайт. SIM карта кошуңуз."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Жараксыз SIM карта."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM картаңыз биротоло өчүрүлдү.\n Башка SIM карта алуу үчүн зымсыз кызмат көрсөтүүчүгө кайрылыңыз."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM карта кулпуланган."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM карта PUK менен кулпуланган."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM картанын кулпусу ачылууда…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM карта жок"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM-карта салыңыз."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-карта жок же ал окулбай калган. SIM-карта салыңыз."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Жараксыз SIM-карта."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM картаңыз биротоло өчүрүлдү.\n Башка SIM-карта алыш үчүн зымсыз кызмат көрсөтүүчүгө кайрылыңыз."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-карта кулпуланган."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-карта PUK-код менен кулпуланган."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM-карта бөгөттөн чыгарылууда…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-коддун аймагы"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Түзмөктүн сырсөзү"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM-картанын PIN-кодунун аймагы"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Эми SIM-картанын \"<xliff:g id="CARRIER">%1$s</xliff:g>\" байланыш оператору өчүрүлдү. Улантуу үчүн PUK-кодду киргизиңиз. Анын чоо-жайын билүү үчүн байланыш операторуна кайрылыңыз."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Сиз каалаган PIN-кодду териңиз"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Сиз каалаган PIN-кодду ырастаңыз"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM картанын кулпусу ачылууда…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM-карта бөгөттөн чыгарылууда…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4–8 сандан турган PIN-кодду териңиз."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-код 8 же андан көп сандан турушу керек."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN-кодуңузду <xliff:g id="NUMBER_0">%1$d</xliff:g> жолу туура эмес тердиңиз. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секунддан кийин дагы аракет кылып көрүңүз."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Сырсөзүңүздү <xliff:g id="NUMBER_0">%1$d</xliff:g> жолу туура эмес тердиңиз. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секунддан кийин дагы аракет кылып көрүңүз."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Түзмөктү ачуучу графикалык  ачкычты <xliff:g id="NUMBER_0">%1$d</xliff:g> жолу туура эмес тарттыңыз. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секунддан кийин дагы аракет кылып көрүңүз."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM-картанын PIN-коду туура эмес. Эми түзмөктү бөгөттөн чыгаруу үчүн байланыш операторуңузга кайрылышыңыз керек."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM-картанын PIN коду туура эмес киргизилди. # аракет калды. Болбосо, түзмөктү бөгөттөн чыгаруу үчүн операторуңузга кайрылышыңыз керек болот.}other{SIM-картанын PIN коду туура эмес киргизилди. # аракет калды. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM-картанын PIN-коду туура эмес, сизде <xliff:g id="NUMBER_1">%d</xliff:g> аракет калды.</item>
+      <item quantity="one">SIM-картанын PIN-коду туура эмес, сизде <xliff:g id="NUMBER_0">%d</xliff:g> аракет калды. Болбосо, түзмөктү бөгөттөн чыгаруу үчүн байланыш операторуңузга кайрылышыңыз керек.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-карта жараксыз. Байланыш операторуңузга кайрылыңыз."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM-картанын PUK коду туура эмес киргизилди. SIM-картанын биротоло жарактан чыгаарына # аракет калды.}other{SIM-картанын PUK коду туура эмес киргизилди. SIM-картанын биротоло жарактан чыгаарына # аракет калды.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM-картанын PUK-коду туура эмес, SIM-картанын биротоло жарактан чыгаарына <xliff:g id="NUMBER_1">%d</xliff:g> аракет калды.</item>
+      <item quantity="one">SIM-картанын PUK-коду туура эмес, SIM-картанын биротоло жарактан чыгаарына <xliff:g id="NUMBER_0">%d</xliff:g> аракет калды.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM-картанын PIN-кодун ачуу кыйрады!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM-картанын PUK-кодун ачуу кыйрады!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Киргизүү ыкмасын өзгөртүү"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Түзмөк кайра күйгүзүлгөндөн кийин графикалык ачкычты тартуу талап кылынат"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Түзмөк кайра күйгүзүлгөндөн кийин PIN-кодду киргизүү талап кылынат"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Түзмөк кайра күйгүзүлгөндөн кийин сырсөздү киргизүү талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Кошумча коопсуздук үчүн анын ордуна графикалык ачкычты колдонуңуз"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Кошумча коопсуздук үчүн анын ордуна PIN кодду колдонуңуз"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Кошумча коопсуздук үчүн анын ордуна сырсөздү колдонуңуз"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Коопсуздукту бекемдөө үчүн графикалык ачкыч талап кылынат"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Коопсуздукту бекемдөө үчүн PIN-код талап кылынат"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Коопсуздукту бекемдөө үчүн сырсөз талап кылынат"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Түзмөктү администратор кулпулап койгон"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Түзмөк кол менен кулпуланды"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таанылган жок"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Жөндөөлөрдөн камерага уруксат беришиңиз керек"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM-картанын PIN кодун киргизиңиз. Сизде # аракет калды, андан кийин түзмөктү бөгөттөн чыгаруу үчүн байланыш операторуна кайрылышыңыз керек болот.}other{SIM-картанын PIN кодун киргизиңиз. # аракет калды.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-карта азыр жарактан чыкты. Улантуу үчүн PUK кодду киргизиңиз. SIM-картанын биротоло жарактан чыгаарына # аракет калды. Чоо-жайын билүү үчүн байланыш операторуна кайрылыңыз.}other{SIM-карта азыр жарактан чыкты. Улантуу үчүн PUK кодду киргизиңиз. SIM-картанын биротоло жарактан чыгарына # аракет калды. Чоо-жайын билүү үчүн байланыш операторуна кайрылыңыз.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Жүзүнөн таанып ачуу функциясын колдонуу үчүн Жөндөөлөр &gt; Купуялык бөлүмүнө өтүп, "<b>"Камераны колдонууну"</b>" күйгүзүңүз"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM-картанын PIN кодун киргизиңиз. Сизде <xliff:g id="NUMBER_1">%d</xliff:g> аракет калды.</item>
+      <item quantity="one">SIM-картанын PIN кодун киргизиңиз. Сизде <xliff:g id="NUMBER_0">%d</xliff:g> аракет калды, андан кийин түзмөктү бөгөттөн чыгаруу үчүн байланыш операторуна кайрылышыңыз керек болот.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM-карта азыр жарактан чыкты. Улантуу үчүн PUK-кодду киргизиңиз. SIM-картанын биротоло жарактан чыгарына <xliff:g id="_NUMBER_1">%d</xliff:g> аракет калды. Чоо-жайын билүү үчүн байланыш операторуна кайрылыңыз.</item>
+      <item quantity="one">SIM-карта азыр жарактан чыкты. Улантуу үчүн PUK-кодду киргизиңиз. SIM-картанын биротоло жарактан чыгаарына <xliff:g id="_NUMBER_0">%d</xliff:g> аракет калды. Чоо-жайын билүү үчүн байланыш операторуна кайрылыңыз.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Демейки"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Көбүк"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналог"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Улантуу үчүн түзмөгүңүздүн кулпусун ачыңыз"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lo/strings.xml b/packages/SystemUI/res-keyguard/values-lo/strings.xml
index fe1b6c6..a01cd7c 100644
--- a/packages/SystemUI/res-keyguard/values-lo/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lo/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ບັດບໍ່ຖືກຕ້ອງ."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ສາກເຕັມແລ້ວ."</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ກຳ​ລັງ​ສາກ​ໄຟໄຮ້​ສາຍ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ກຳລັງສາກໄຟ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ກຳລັງສາກໄຟຜ່ານດັອກ"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ກຳລັງສາກ"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ກຳລັງສາກແບບດ່ວນ"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ກຳລັງສາກແບບຊ້າ"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ການສາກຖືກປັບໃຫ້ເໝາະສົມເພື່ອປົກປ້ອງແບັດເຕີຣີ"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ຈຳກັດການສາກໄຟຊົ່ວຄາວແລ້ວ"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"ກົດ \"ເມນູ\" ເພື່ອປົດລັອກ."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ເຄືອຂ່າຍຖືກລັອກ"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"ບໍ່ມີຊິມ"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"ເພີ່ມຊິມ."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"ບໍ່ມີຊິມ ຫຼື ອ່ານຊິມບໍ່ໄດ້. ເພີ່ມຊິມ."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ຊິມໃຊ້ບໍ່ໄດ້."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ຊິມຂອງທ່ານຖືກປິດໃຊ້ຢ່າງຖາວອນແລ້ວ.\n ຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການໂທລະສັບໄຮ້ສາຍຂອງທ່ານເພື່ອຂໍຊິມໃໝ່."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"ຊິມຖືກລັອກຢູ່."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"ຊິມຖືກລັອກດ້ວຍ PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"ກຳລັງປົດລັອກຊິມ…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"ບໍ່ມີຊິມກາດ"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ໃສ່ຊິມກາດ."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ບໍ່ພົບເຫັນຊິມກາດ ຫຼືບໍ່ສາມາດອ່ານຊິມກາດໄດ້. ກະລຸນາໃສ່ຊິມກາດໃໝ່."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM card ບໍ່ສາມາດໃຊ້ໄດ້."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ຊິມກາດຂອງທ່ານຖືກປິດການນຳໃຊ້ຢ່າງຖາວອນແລ້ວ.\n ກະລຸນາຕິດຕໍ່ຜູ່ໃຫ້ບໍລິການໂທລະສັບຂອງທ່ານ ເພື່ອຂໍເອົາຊິມກາດໃໝ່."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM card ຖືກລັອກ."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ຊິມກາດຖືກລັອກດ້ວຍລະຫັດ PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"ປົດລັອກ SIM card..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"ພື້ນທີ່ PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ລະຫັດຜ່ານອຸປະກອນ"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"ພື້ນທີ່ PIN ຂອງ SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ດຽວນີ້ \"<xliff:g id="CARRIER">%1$s</xliff:g>\" SIM ປິດນຳໃຊ້. ປ້ອນລະຫັດ PUK ເພື່ອສືບຕໍ່. ຕິດຕໍ່ບໍລິສັດໃຫ້ບໍລິການ ເພື່ອຂໍລາຍລະອຽດ."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"ໃສ່ລະຫັດ PIN ທີ່ຕ້ອງການ."</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"ຢືນຢັນລະຫັດ PIN ທີ່ຕ້ອງການ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"ກຳລັງປົດລັອກຊິມ…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"ປົດລັອກ SIM card..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"ພິມລະຫັດ PIN ທີ່ມີ 4 ຫາ 8 ໂຕເລກ."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"ລະຫັດ PUK ຄວນມີຢ່າງໜ້ອຍ 8 ໂຕເລກ."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"ທ່ານພິມລະຫັດ PIN ຂອງທ່ານຜິດ <xliff:g id="NUMBER_0">%1$d</xliff:g> ເທື່ອແລ້ວ. \n\nກະລຸນາລອງໃໝ່ໃນອີກ <xliff:g id="NUMBER_1">%2$d</xliff:g> ວິນາທີ."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"ທ່ານພິມລະຫັດຜ່ານຜິດ <xliff:g id="NUMBER_0">%1$d</xliff:g> ເທື່ອແລ້ວ. \n\nໃຫ້ລອງໃໝ່ອີກຄັ້ງໃນອີກ <xliff:g id="NUMBER_1">%2$d</xliff:g> ວິນາທີ."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"ທ່ານແຕ້ມຮູບແບບປົດລັອກບໍ່ຖືກ <xliff:g id="NUMBER_0">%1$d</xliff:g> ເທື່ອແລ້ວ. \n\nລອງໃໝ່ໃນອີກ <xliff:g id="NUMBER_1">%2$d</xliff:g> ວິນາທີ."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ລະຫັດ PIN ຂອງ SIM ບໍ່ຖືກຕ້ອງທ່ານຕ້ອງຕິດຕໍ່ຫາຜູ່ໃຫ້ບໍລິການ ເພື່ອປົດລັອກອຸປະກອນຂອງທ່ານ."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{ລະຫັດ PIN ຂອງຊິມບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອກ່ອນທີ່ທ່ານຈະຕ້ອງຕິດຕໍ່ຫາຜູ້ໃຫ້ບໍລິການຂອງທ່ານເພື່ອປົດລັອກອຸປະກອນທ່ານ.}other{ລະຫັດ PIN ຂອງຊິມບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອ. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">ລະຫັດ SIM PIN ບໍ່ຖືກຕ້ອງ, ທ່ານຍັງພະຍາຍາມໄດ້ອີກ <xliff:g id="NUMBER_1">%d</xliff:g> ຄັ້ງ.</item>
+      <item quantity="one">ລະຫັດ PIN ຂອງ SIM ບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="NUMBER_0">%d</xliff:g> ເທື່ອກ່ອນທີ່ທ່ານຈະຕ້ອງຕິດຕໍ່ຫາຜູ່ໃຫ້ບໍລິການຂອງທ່ານ ເພື່ອປົດລັອກອຸປະກອນຂອງທ່ານ.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM ໃຊ້ບໍ່ໄດ້ແລ້ວ. ກະລຸນາຕິດຕໍ່ຫາຜູ່ໃຫ້ບໍລິການຂອງທ່ານ."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{ລະຫັດ PUK ຂອງຊິມບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອກ່ອນທີ່ຊິມຂອງທ່ານຈະບໍ່ສາມາດໃຊ້ໄດ້ຖາວອນ.}other{ລະຫັດ PUK ຂອງຊິມບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອກ່ອນທີ່ຊິມຂອງທ່ານຈະບໍ່ສາມາດໃຊ້ໄດ້ຖາວອນ.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">ລະຫັດ PUK ຂອງ SIM ບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="NUMBER_1">%d</xliff:g> ເທື່ອກ່ອນທີ່ SIM ຂອງທ່ານຈະໃຊ້ບໍ່ໄດ້ຢ່າງຖາວອນ.</item>
+      <item quantity="one">ລະຫັດ PUK ຂອງ SIM ບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="NUMBER_0">%d</xliff:g> ເທື່ອກ່ອນທີ່ SIM ຂອງທ່ານຈະໃຊ້ບໍ່ໄດ້ຢ່າງຖາວອນ.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"PIN ຂອງ SIM ເຮັດວຽກລົ້ມເຫຼວ!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"PUK ຂອງ SIM ເຮັດວຽກລົ້ມເຫຼວ!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ສະລັບຮູບແບບການປ້ອນຂໍ້ມູນ"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ຈຳເປັນຕ້ອງມີແບບຮູບປົດລັອກຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ຈຳເປັນຕ້ອງມີ PIN ຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ, ໃຫ້ໃຊ້ຮູບແບບແທນ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ, ໃຫ້ໃຊ້ PIN ແທນ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ, ໃຫ້ໃຊ້ລະຫັດຜ່ານແທນ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ຈຳເປັນຕ້ອງມີແບບຮູບເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ຈຳເປັນຕ້ອງມີ PIN ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ອຸປະກອນຖືກລັອກໂດຍຜູ້ເບິ່ງແຍງລະບົບ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ອຸປະກອນຖືກສັ່ງໃຫ້ລັອກ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ບໍ່ຮູ້ຈັກ"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ເພື່ອໃຊ້ການປົດລັອກດ້ວຍໜ້າ, ໃຫ້ເປີດໃຊ້ສິດເຂົ້າເຖິງກ້ອງຖ່າຍຮູບໃນການຕັ້ງຄ່າ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{ໃສ່ລະຫັດ PIN ຂອງຊິມ. ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອກ່ອນທີ່ຈະຕ້ອງຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການຂອງທ່ານເພື່ອປົດລັອກອຸປະກອນທ່ານ.}other{ໃສ່ລະຫັດ PIN ຂອງຊິມ. ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອ.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ຕອນນີ້ປິດການນຳໃຊ້ຊິມແລ້ວ. ໃສ່ລະຫັດ PUK ເພື່ອສືບຕໍ່. ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອກ່ອນທີ່ຊິມຈະບໍ່ສາມາດໃຊ້ໄດ້ຖາວອນ. ກະລຸນາຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການສຳລັບລາຍລະອຽດ.}other{ຕອນນີ້ປິດການນຳໃຊ້ຊິມແລ້ວ. ໃສ່ລະຫັດ PUK ເພື່ອສືບຕໍ່. ທ່ານສາມາດລອງໄດ້ອີກ # ເທື່ອກ່ອນທີ່ຊິມຈະບໍ່ສາມາດໃຊ້ໄດ້ຖາວອນ. ກະລຸນາຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການສຳລັບລາຍລະອຽດ.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ເພື່ອໃຊ້ການປົດລັອກດ້ວຍໜ້າ, ກະລຸນາເປີດໃຊ້ "<b>"ສິດເຂົ້າເຖິງກ້ອງຖ່າຍຮູບ"</b>" ໃນການຕັ້ງຄ່າ &gt; ຄວາມເປັນສ່ວນຕົວ"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">ລະຫັດ SIM PIN ບໍ່ຖືກຕ້ອງ. ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="NUMBER_1">%d</xliff:g> ເທື່ອ.</item>
+      <item quantity="one">ໃສ່ລະຫັດ SIM PIN. ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="NUMBER_0">%d</xliff:g> ເທື່ອກ່ອນທີ່ຈະຕ້ອງຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການເພື່ອປົດລັອກ.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">ຕອນນີ້ປິດການນຳໃຊ້ SIM ແລ້ວ. ໃສ່ລະຫັດ PUK ເພື່ອດຳເນີນການຕໍ່. ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="_NUMBER_1">%d</xliff:g> ເທື່ອກ່ອນທີ່ SIM ຈະບໍ່ສາມາດໃຊ້ໄດ້ຖາວອນ. ກະລຸນາຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການສຳລັບລາຍລະອຽດ.</item>
+      <item quantity="one">ຕອນນີ້ປິດການນຳໃຊ້ SIM ແລ້ວ. ໃສ່ລະຫັດ PUK ເພື່ອດຳເນີນການຕໍ່. ທ່ານສາມາດລອງໄດ້ອີກ <xliff:g id="_NUMBER_0">%d</xliff:g> ເທື່ອກ່ອນທີ່ SIM ຈະບໍ່ສາມາດໃຊ້ໄດ້ຖາວອນ. ກະລຸນາຕິດຕໍ່ຜູ້ໃຫ້ບໍລິການສຳລັບລາຍລະອຽດ.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ຄ່າເລີ່ມຕົ້ນ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ຟອງ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ໂມງເຂັມ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ປົດລັອກອຸປະກອນຂອງທ່ານເພື່ອສືບຕໍ່"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lt/strings.xml b/packages/SystemUI/res-keyguard/values-lt/strings.xml
index f4b7aee..64e600e 100644
--- a/packages/SystemUI/res-keyguard/values-lt/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lt/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Netinkama kortelė."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Įkrauta"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kraunama be laidų"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Įkraunama"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Įkraunama doke"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Įkraunama"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Greitai įkraunama"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lėtai įkraunama"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Įkrovimas optimizuotas siekiant apsaugoti akumuliatorių"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Įkrovimas laikinai apribotas"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Paspauskite meniu, jei norite atrakinti."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Tinklas užrakintas"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nėra SIM kortelės"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Įdėkite SIM kortelę."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Trūksta SIM kortelės arba ji neskaitoma. Įdėkite SIM kortelę."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Nenaudojama SIM kortelė."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Jūsų SIM kortelė visam laikui išjungta.\n Susisiekite su belaidžio ryšio paslaugos teikėju, kad gautumėte naują SIM kortelę."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM kortelė užrakinta."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM kortelė užrakinta PUK kodu."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Atrakinama SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nėra SIM kortelės"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Įdėkite SIM kortelę."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Nėra SIM kortelės arba ji neskaitoma. Įdėkite SIM kortelę."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Negalima naudoti SIM kortelės."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM kortelė visam laikui išjungta.\n Jei norite gauti kitą SIM kortelę, susisiekite su belaidžio ryšio paslaugos teikėju."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kortelė užrakinta."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kortelė užrakinta PUK kodu."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Atrakinama SD kortelė..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN kodo sritis"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Įrenginio slaptažodis"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM kortelės PIN kodo sritis"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"„<xliff:g id="CARRIER">%1$s</xliff:g>“ SIM kortelė išjungta. Jei norite tęsti, įveskite PUK kodą. Jei reikia išsamios informacijos, susisiekite su operatoriumi."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Įveskite pageidaujamą PIN kodą"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Patvirtinkite pageidaujamą PIN kodą"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Atrakinama SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Atrakinama SD kortelė..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Įveskite PIN kodą, sudarytą iš 4–8 skaičių."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kodas turėtų būti sudarytas iš 8 ar daugiau skaitmenų."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"<xliff:g id="NUMBER_0">%1$d</xliff:g> kart. netinkamai įvedėte PIN kodą. \n\nBandykite dar kartą po <xliff:g id="NUMBER_1">%2$d</xliff:g> sek."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"<xliff:g id="NUMBER_0">%1$d</xliff:g> kart. netinkamai įvedėte slaptažodį. \n\nBandykite dar kartą po <xliff:g id="NUMBER_1">%2$d</xliff:g> sek."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"<xliff:g id="NUMBER_0">%1$d</xliff:g> kart. netinkamai nupiešėte atrakinimo piešinį. \n\nBandykite dar kartą po <xliff:g id="NUMBER_1">%2$d</xliff:g> sek."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Netinkamas SIM kortelės PIN kodas. Reikės susisiekti su operatoriumi, kad atrakintų įrenginį."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Netinkamas SIM kortelės PIN kodas. Jums liko # bandymas. Paskui reikės susisiekti su operatoriumi, kad atrakintumėte įrenginį.}one{Netinkamas SIM kortelės PIN kodas. Jums liko # bandymas. }few{Netinkamas SIM kortelės PIN kodas. Jums liko # bandymai. }many{Netinkamas SIM kortelės PIN kodas. Jums liko # bandymo. }other{Netinkamas SIM kortelės PIN kodas. Jums liko # bandymų. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Netinkamas SIM kortelės PIN kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymas.</item>
+      <item quantity="few">Netinkamas SIM kortelės PIN kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymai.</item>
+      <item quantity="many">Netinkamas SIM kortelės PIN kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymo.</item>
+      <item quantity="other">Netinkamas SIM kortelės PIN kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymų.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kortelės naudoti nebegalima. Susisiekite su operatoriumi."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Netinkamas SIM kortelės PUK kodas. Jums liko # bandymas. Paskui visiškai nebegalėsite naudoti SIM kortelės.}one{Netinkamas SIM kortelės PUK kodas. Jums liko # bandymas. Paskui visiškai nebegalėsite naudoti SIM kortelės.}few{Netinkamas SIM kortelės PUK kodas. Jums liko # bandymai. Paskui visiškai nebegalėsite naudoti SIM kortelės.}many{Netinkamas SIM kortelės PUK kodas. Jums liko # bandymo. Paskui visiškai nebegalėsite naudoti SIM kortelės.}other{Netinkamas SIM kortelės PUK kodas. Jums liko # bandymų. Paskui visiškai nebegalėsite naudoti SIM kortelės.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Netinkamas SIM kortelės PUK kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymas. Paskui visiškai nebegalėsite naudoti SIM kortelės.</item>
+      <item quantity="few">Netinkamas SIM kortelės PUK kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymai. Paskui visiškai nebegalėsite naudoti SIM kortelės.</item>
+      <item quantity="many">Netinkamas SIM kortelės PUK kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymo. Paskui visiškai nebegalėsite naudoti SIM kortelės.</item>
+      <item quantity="other">Netinkamas SIM kortelės PUK kodas. Liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymų. Paskui visiškai nebegalėsite naudoti SIM kortelės.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Nepavyko atlikti SIM kortelės PIN kodo operacijos."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Nepavyko atlikti SIM kortelės PUK kodo operacijos."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Perjungti įvesties metodą"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iš naujo paleidus įrenginį būtinas atrakinimo piešinys"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iš naujo paleidus įrenginį būtinas PIN kodas"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iš naujo paleidus įrenginį būtinas slaptažodis"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Papildomai saugai užtikrinti geriau naudokite atrakinimo piešinį"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Papildomai saugai užtikrinti geriau naudokite PIN kodą"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Papildomai saugai užtikrinti geriau naudokite slaptažodį"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Norint užtikrinti papildomą saugą būtinas atrakinimo piešinys"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Norint užtikrinti papildomą saugą būtinas PIN kodas"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Norint užtikrinti papildomą saugą būtinas slaptažodis"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Įrenginį užrakino administratorius"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Įrenginys užrakintas neautomatiškai"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Neatpažinta"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Jei nor. naud. atr. pagal veidą, įj. pr. prie fotoap. sk. „Nustatymai“"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Įveskite SIM kortelės PIN kodą. Jums liko # bandymas. Paskui reikės susisiekti su operatoriumi, kad atrakintumėte įrenginį.}one{Įveskite SIM kortelės PIN kodą. Jums liko # bandymas.}few{Įveskite SIM kortelės PIN kodą. Jums liko # bandymai.}many{Įveskite SIM kortelės PIN kodą. Jums liko # bandymo.}other{Įveskite SIM kortelės PIN kodą. Jums liko # bandymų.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko # bandymas. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.}one{SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko # bandymas. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.}few{SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko # bandymai. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.}many{SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko # bandymo. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.}other{SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko # bandymų. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Jei norite naudoti atrakinimą pagal veidą, įjunkite parinktį "<b>"Prieiga prie fotoaparato"</b>" skiltyje „Nustatymai“ &gt; „Privatumas“"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Įveskite SIM kortelės PIN kodą. Jums liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymas.</item>
+      <item quantity="few">Įveskite SIM kortelės PIN kodą. Jums liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymai.</item>
+      <item quantity="many">Įveskite SIM kortelės PIN kodą. Jums liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymo.</item>
+      <item quantity="other">Įveskite SIM kortelės PIN kodą. Jums liko <xliff:g id="NUMBER_1">%d</xliff:g> bandymų.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko <xliff:g id="_NUMBER_1">%d</xliff:g> bandymas. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.</item>
+      <item quantity="few">SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko <xliff:g id="_NUMBER_1">%d</xliff:g> bandymai. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.</item>
+      <item quantity="many">SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko <xliff:g id="_NUMBER_1">%d</xliff:g> bandymo. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.</item>
+      <item quantity="other">SIM kortelė dabar yra išjungta. Jei norite tęsti, įveskite PUK kodą. Jums liko <xliff:g id="_NUMBER_1">%d</xliff:g> bandymų. Paskui visiškai nebegalėsite naudoti SIM kortelės. Jei reikia išsamios informacijos, susisiekite su operatoriumi.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Numatytasis"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Debesėlis"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoginis"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Įrenginio atrakinimas norint tęsti"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lv/strings.xml b/packages/SystemUI/res-keyguard/values-lv/strings.xml
index 68e94d0..9cd30a1 100644
--- a/packages/SystemUI/res-keyguard/values-lv/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lv/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Nederīga karte."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Akumulators uzlādēts"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Notiek bezvadu uzlāde"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Notiek uzlāde"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Notiek uzlāde dokā"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Notiek uzlāde"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Notiek ātrā uzlāde"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Notiek lēnā uzlāde"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Uzlāde optimizēta, lai saudzētu akumulatoru"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Uzlāde īslaicīgi ierobežota"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Lai atbloķētu, nospiediet izvēlnes ikonu."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Tīkls ir bloķēts."</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nav SIM kartes"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Pievienojiet SIM karti."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Nav SIM kartes, vai arī to nevar nolasīt. Pievienojiet SIM karti."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM karte nav izmantojama."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Jūsu SIM karte ir neatgriezeniski deaktivizēta.\n Sazinieties ar savu bezvadu pakalpojumu sniedzēju, lai iegūtu citu SIM karti."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM karte ir bloķēta."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM karte ir bloķēta ar PUK kodu."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Notiek SIM kartes atbloķēšana…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nav SIM kartes."</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Ievietojiet SIM karti."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Nav SIM kartes, vai arī to nevar nolasīt. Ievietojiet SIM karti."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Nelietojama SIM karte."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Jūsu SIM karte ir neatgriezeniski atspējota.\nSazinieties ar savu bezvadu pakalpojumu sniedzēju, lai iegūtu citu SIM karti."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM karte ir bloķēta."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM karte ir bloķēta ar PUK kodu."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Notiek SIM kartes atbloķēšana..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN apgabals"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Ierīces parole"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM kartes PIN apgabals"</string>
@@ -61,16 +61,24 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM karte “<xliff:g id="CARRIER">%1$s</xliff:g>” ir atspējota. Lai turpinātu, ievadiet PUK kodu. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Ievadiet vēlamo PIN kodu."</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Apstipriniet vēlamo PIN kodu."</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Notiek SIM kartes atbloķēšana…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Notiek SIM kartes atbloķēšana..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Ievadiet PIN kodu, kas sastāv no 4 līdz 8 cipariem."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kodam ir jābūt vismaz 8 ciparus garam."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Jūs <xliff:g id="NUMBER_0">%1$d</xliff:g> reizi(-es) esat ievadījis nepareizu PIN kodu.\n\nMēģiniet vēlreiz pēc <xliff:g id="NUMBER_1">%2$d</xliff:g> sekundes(-ēm)."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Jūs <xliff:g id="NUMBER_0">%1$d</xliff:g> reizi(-es) esat ievadījis nepareizu paroli.\n\nMēģiniet vēlreiz pēc <xliff:g id="NUMBER_1">%2$d</xliff:g> sekundes(-ēm)."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Jūs <xliff:g id="NUMBER_0">%1$d</xliff:g> reizi(-es) esat nepareizi uzzīmējis atbloķēšanas kombināciju.\n\nMēģiniet vēlreiz pēc <xliff:g id="NUMBER_1">%2$d</xliff:g> sekundes(-ēm)."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Nepareizs SIM kartes PIN kods. Lai atbloķētu ierīci, sazinieties ar mobilo sakaru operatoru."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Nepareizs SIM kartes PIN. Varat mēģināt vēl # reizi. Kļūdas gadījumā būs jāsazinās ar mobilo sakaru operatoru, lai tas atbloķētu jūsu ierīci.}zero{Nepareizs SIM kartes PIN. Varat mēģināt vēl # reizes. }one{Nepareizs SIM kartes PIN. Varat mēģināt vēl # reizi. }other{Nepareizs SIM kartes PIN. Varat mēģināt vēl # reizes. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="zero">Nepareizs SIM kartes PIN kods. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizes.</item>
+      <item quantity="one">Nepareizs SIM kartes PIN kods. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizi.</item>
+      <item quantity="other">Nepareizs SIM kartes PIN kods. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizes.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM karte nav lietojama. Sazinieties ar mobilo sakaru operatoru."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Nepareizs SIM kartes PUK kods. Varat mēģināt vēl # reizi. Kļūdas gadījumā SIM karti vairs nevarēs izmantot.}zero{Nepareizs SIM kartes PUK kods. Varat mēģināt vēl # reizes. Ja pēdējais mēģinājums būs kļūdains, SIM karti vairs nevarēs izmantot.}one{Nepareizs SIM kartes PUK kods. Varat mēģināt vēl # reizi. Ja pēdējais mēģinājums būs kļūdains, SIM karti vairs nevarēs izmantot.}other{Nepareizs SIM kartes PUK kods. Varat mēģināt vēl # reizes. Ja pēdējais mēģinājums būs kļūdains, SIM karti vairs nevarēs izmantot.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="zero">Nepareizs SIM kartes PUK kods. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizes. Ja pēdējais mēģinājums būs kļūdains, SIM karti vairs nevarēs izmantot.</item>
+      <item quantity="one">Nepareizs SIM kartes PUK kods. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizi. Ja pēdējais mēģinājums būs kļūdains, SIM karti vairs nevarēs izmantot.</item>
+      <item quantity="other">Nepareizs SIM kartes PUK kods. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizes. Ja pēdējais mēģinājums būs kļūdains, SIM karti vairs nevarēs izmantot.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM kartes PIN koda ievadīšana neizdevās!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM kartes PUK koda ievadīšana neizdevās!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Pārslēgt ievades metodi"</string>
@@ -78,17 +86,24 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pēc ierīces restartēšanas ir jāievada atbloķēšanas kombinācija."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pēc ierīces restartēšanas ir jāievada PIN kods."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pēc ierīces restartēšanas ir jāievada parole."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Papildu drošībai izmantojiet kombināciju"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Papildu drošībai izmantojiet PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Papildu drošībai izmantojiet paroli"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Papildu drošībai ir jāievada atbloķēšanas kombinācija."</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Papildu drošībai ir jāievada PIN kods."</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Papildu drošībai ir jāievada parole."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrators bloķēja ierīci."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Ierīce tika bloķēta manuāli."</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nav atpazīts"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Lai izmantotu autorizāciju pēc sejas, iestatījumos ieslēdziet piekļuvi kamerai."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Ievadiet SIM kartes PIN. Varat mēģināt vēl # reizi. Kļūdas gadījumā jums būs jāsazinās ar mobilo sakaru operatoru, lai atbloķētu savu ierīci.}zero{Ievadiet SIM kartes PIN. Jums ir atlikuši # mēģinājumi.}one{Ievadiet SIM kartes PIN. Jums ir atlicis # mēģinājums.}other{Ievadiet SIM kartes PIN. Jums ir atlikuši # mēģinājumi.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl # reizi. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.}zero{SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl # reizes. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.}one{SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl # reizi. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.}other{SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl # reizes. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Lai izmantotu autorizāciju pēc sejas, sadaļā Iestatījumi &gt; Konfidencialitāte ieslēdziet opciju "<b>"Piekļuve kamerai"</b>"."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="zero">Ievadiet SIM kartes PIN. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizes.</item>
+      <item quantity="one">Ievadiet SIM kartes PIN. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizi.</item>
+      <item quantity="other">Ievadiet SIM kartes PIN. Varat mēģināt vēl <xliff:g id="NUMBER_1">%d</xliff:g> reizes.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="zero">SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl <xliff:g id="_NUMBER_1">%d</xliff:g> reizes. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.</item>
+      <item quantity="one">SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl <xliff:g id="_NUMBER_1">%d</xliff:g> reizi. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.</item>
+      <item quantity="other">SIM karte tagad ir atspējota. Ievadiet PUK kodu, lai turpinātu. Varat mēģināt vēl <xliff:g id="_NUMBER_1">%d</xliff:g> reizes. Kļūdas gadījumā SIM karti vairs nevarēs izmantot. Lai iegūtu detalizētu informāciju, sazinieties ar mobilo sakaru operatoru.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Noklusējums"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuļi"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogais"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lai turpinātu, atbloķējiet ierīci"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mk/strings.xml b/packages/SystemUI/res-keyguard/values-mk/strings.xml
index d504228..c5cad18 100644
--- a/packages/SystemUI/res-keyguard/values-mk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mk/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Неважечка картичка."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Полна"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Се полни безжично"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Се полни"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Се полни на док"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Се полни"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Брзо полнење"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Бавно полнење"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Полнењето е оптимизирано за да се заштити батеријата"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Полнењето е привремено ограничено"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Притиснете „Мени“ за отклучување."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Мрежата е заклучена"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Нема SIM-картичка"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Додајте SIM-картичка."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Нема SIM-картичка или не може да се прочита. Додајте SIM-картичка."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-картичката е неупотреблива."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Вашата SIM-картичка е трајно деактивирана.\n Контактирајте со давателот на услуги за безжична мрежа за друга SIM-картичка."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-картичката е заклучена."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-картичката е заклучена со PUK-код."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Се отклучува SIM-картичката…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Нема SIM-картичка"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Вметнете SIM-картичка."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Нема SIM-картичка или не може да се прочита. Вметнете SIM-картичка."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Неупотреблива SIM-картичка."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Вашата SIM-картичка е трајно оневозможена.\nКонтактирајте со безжичниот оператор за друга SIM-картичка."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-картичката е заклучена."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-картичката е заклучена со PUK-код."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Се отклучува SIM-картичката…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Поле за PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Лозинка за уред"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Поле за PIN на SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-картичката на <xliff:g id="CARRIER">%1$s</xliff:g> сега е оневозможена. Внесете PUK-код за да продолжите. Контактирајте со операторот за детали."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Внесете го саканиот PIN-код"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Потврдете го саканиот PIN-код"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Се отклучува SIM-картичката…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Се отклучува SIM-картичката…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Внесете PIN што содржи 4 - 8 броеви."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-кодот треба да содржи 8 или повеќе броеви."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Погрешно сте го напишале вашиот PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> пати. \n\nОбидете се повторно за <xliff:g id="NUMBER_1">%2$d</xliff:g> секунди."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Погрешно сте ја напишале вашата лозинка <xliff:g id="NUMBER_0">%1$d</xliff:g> пати. \n\nОбидете се повторно за <xliff:g id="NUMBER_1">%2$d</xliff:g> секунди."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Погрешно сте ја нацртале вашата шема за отклучување <xliff:g id="NUMBER_0">%1$d</xliff:g> пати. \n\nОбидете се повторно за <xliff:g id="NUMBER_1">%2$d</xliff:g> секунди."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Погрешен PIN-код за SIM, сега мора да контактирате со вашиот оператор за да го отклучите уредот."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Погрешен PIN-код за SIM-картичката. Ви преостанува уште # обид пред да мора да контактирате со вашиот оператор за да го отклучи уредот.}one{Погрешен PIN-код за SIM, ви преостанува уште # обид. }other{Погрешен PIN-код за SIM, ви преостануваат уште # обиди. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Погрешен PIN-код за SIM, ви преостанува уште <xliff:g id="NUMBER_1">%d</xliff:g> обид.</item>
+      <item quantity="other">Погрешен PIN-код за SIM, ви преостануваат уште <xliff:g id="NUMBER_1">%d</xliff:g> обиди.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-картичката е неупотреблива. Контактирајте со вашиот оператор."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{ПУК кодот за SIM-картичката е неточен. Ви преостанува уште # обид, а потоа SIM-картичката ќе стане трајно неупотреблива.}one{Погрешен PUK-код за SIM, ви преостанува уште # обид пред SIM-картичката да стане трајно неупотреблива.}other{Погрешен PUK-код за SIM, ви преостануваат уште # обиди пред SIM-картичката да стане трајно неупотреблива.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Погрешен PUK-код за SIM, ви преостанува уште <xliff:g id="NUMBER_1">%d</xliff:g> обид пред SIM-картичката да стане трајно неупотреблива.</item>
+      <item quantity="other">Погрешен PUK-код за SIM, ви преостануваат уште <xliff:g id="NUMBER_1">%d</xliff:g> обиди пред SIM-картичката да стане трајно неупотреблива.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM-картичката не се отклучи со PIN-кодот!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM-картичката не се отклучи со PUK-кодот!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Префрли метод за внесување"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Потребна е шема по рестартирање на уредот"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Потребен е PIN-код по рестартирање на уредот"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Потребна е лозинка по рестартирање на уредот"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За дополнителна безбедност, користете шема"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За дополнителна безбедност, користете PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За дополнителна безбедност, користете лозинка"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Потребна е шема за дополнителна безбедност"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Потребен е PIN-код за дополнителна безбедност"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Потребна е лозинка за дополнителна безбедност"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Уредот е заклучен од администраторот"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уредот е заклучен рачно"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Непознат"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"За „Отклучување со лик“, вклучете пристап до камерата"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Внесете PIN-код за SIM-картичката. Ви преостанува уште # обид пред да мора да контактирате со вашиот оператор да го отклучи уредот.}one{Внесете PIN на SIM. Имате уште # обид.}other{Внесете PIN на SIM. Имате уште # обиди.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-картичката сега е оневозможена. Внесете PUK-код за да продолжите. Ви преостанува уште # обид пред SIM-картичката да стане трајно неупотреблива. Контактирајте го операторот за детали.}one{SIM-картичката сега е оневозможена. Внесете PUK-код за да продолжите. Ви преостанува уште # обид пред SIM-картичката да стане трајно неупотреблива. Контактирајте го операторот за детали.}other{SIM-картичката сега е оневозможена. Внесете PUK-код за да продолжите. Ви преостануваат уште # обиди пред SIM-картичката да стане трајно неупотреблива. Контактирајте го операторот за детали.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"За да користите „Отклучување со лик“, вклучете "<b>"Пристап до камерата"</b>" во „Поставки &gt; Приватност“"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Внесете PIN-код за SIM-картичката. Ви преостанува уште <xliff:g id="NUMBER_1">%d</xliff:g> обид.</item>
+      <item quantity="other">Внесете PIN-код за SIM-картичката. Ви преостануваат уште <xliff:g id="NUMBER_1">%d</xliff:g> обиди.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM-картичката сега е оневозможена. Внесете PUK-код за да продолжите. Ви преостанува уште <xliff:g id="_NUMBER_1">%d</xliff:g> обид пред SIM-картичката да стане трајно неупотреблива. Контактирајте го операторот за детали.</item>
+      <item quantity="other">SIM-картичката сега е оневозможена. Внесете PUK-код за да продолжите. Ви преостануваат уште <xliff:g id="_NUMBER_1">%d</xliff:g> обиди пред SIM-картичката да стане трајно неупотреблива. Контактирајте го операторот за детали.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Стандарден"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Балонче"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналоген"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Отклучете го уредот за да продолжите"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ml/strings.xml b/packages/SystemUI/res-keyguard/values-ml/strings.xml
index 8d466c18..2b0efc8 100644
--- a/packages/SystemUI/res-keyguard/values-ml/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ml/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"അസാധുവായ കാർഡ്."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ചാർജായി"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • വയർലെസ്സ് ആയി ചാർജ് ചെയ്യുന്നു"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ചാർജ് ചെയ്യുന്നു"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ചാർജിംഗ് ഡോക്ക്"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ചാർജ് ചെയ്യുന്നു"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • വേഗത്തിൽ ചാർജ് ചെയ്യുന്നു"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • പതുക്കെ ചാർജ് ചെയ്യുന്നു"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ബാറ്ററി പരിരക്ഷിക്കാൻ ചാർജിംഗ് ഒപ്റ്റിമൈസ് ചെയ്തു"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ചാർജിംഗ് താൽക്കാലികമായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"അൺലോക്കുചെയ്യാൻ മെനു അമർത്തുക."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"നെറ്റ്‌വർക്ക് ലോക്കുചെയ്‌തു"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"സിം ഇല്ല"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"സിം ചേർക്കുക."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"സിം കാണുന്നില്ല അല്ലെങ്കിൽ റീഡ് ചെയ്യാനായില്ല. സിം ചേർക്കുക."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ഉപയോഗശൂന്യമായ സിം."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"നിങ്ങളുടെ സിം ശാശ്വതമായി നിഷ്ക്രിയമാക്കി.\n മറ്റൊരു സിമ്മിന് നിങ്ങളുടെ വയർലെസ് സേവന ദാതാവിനെ ബന്ധപ്പെടുക."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"സിം ലോക്ക് ചെയ്തു."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"സിം PUK ലോക്ക് ചെയ്തു."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"സിം അൺലോക്ക് ചെയ്യുന്നു…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"സിം കാർഡില്ല"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ഒരു ‌സിം കാർഡ് ഇടുക."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"സിം കാർഡ് കാണുന്നില്ല, അല്ലെങ്കിൽ റീഡുചെയ്യാനായില്ല. ഒരു സിം കാർഡ് ഇടുക."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"ഉപയോഗയോഗ്യമല്ലാത്ത സിം കാർഡ്."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"നിങ്ങളുടെ സിം കാർഡ് ശാശ്വതമായി പ്രവർത്തനരഹിതമാക്കി.\n മറ്റൊരു സിം കാർഡിനായി നിങ്ങളുടെ വയർലെസ് സേവന ദാതാവിനെ ബന്ധപ്പെടുക."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"സിം കാർഡ് ലോക്കുചെയ്‌തു."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"സിം കാർഡ് PUK-ലോക്ക് ചെയ്‌തതാണ്."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"സിം കാർഡ് അൺലോക്കുചെയ്യുന്നു…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"പിൻ ഏരിയ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ഉപകരണ പാസ്‌വേഡ്"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"സിം ‌പിൻ ഏരിയ"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" സിം ഇപ്പോൾ പ്രവർത്തനരഹിതമാക്കി. തുടരുന്നതിന് PUK കോഡ് നൽകുക. വിശദാംശങ്ങൾക്ക് കാരിയറെ ബന്ധപ്പെടുക."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"താൽപ്പര്യപ്പെടുന്ന പിൻ കോഡ് നൽകുക"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"താൽപ്പര്യപ്പെടുന്ന പിൻകോ‌ഡ് സ്ഥിരീകരിക്കുക"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"സിം അൺലോക്ക് ചെയ്യുന്നു…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"സിം കാർഡ് അൺലോക്കുചെയ്യുന്നു…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 മുതൽ 8 വരെ അക്കങ്ങളുള്ള ഒരു പിൻ ടൈപ്പുചെയ്യുക."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK കോഡിൽ 8 അല്ലെങ്കിൽ അതിലധികം സംഖ്യകൾ ഉണ്ടായിരിക്കണം."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"നിങ്ങൾ <xliff:g id="NUMBER_0">%1$d</xliff:g> തവണ പിൻ ‌തെറ്റായി ‌ടൈപ്പുചെയ്തു. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> സെക്കന്റിനു‌ശേഷം വീണ്ടും ശ്രമിക്കുക."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"നിങ്ങൾ <xliff:g id="NUMBER_0">%1$d</xliff:g> തവണ നിങ്ങളുടെ പാസ്‌വേഡ് ‌തെറ്റായി ‌ടൈപ്പുചെയ്തു. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> സെക്കന്റിനു‌ശേഷം വീണ്ടും ശ്രമിക്കുക."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"നിങ്ങൾ <xliff:g id="NUMBER_0">%1$d</xliff:g> തവണ അൺലോക്ക് പാറ്റേൺ ‌തെറ്റായി ‌വരച്ചു. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> സെക്കന്റിനു‌ശേഷം വീണ്ടും ശ്രമിക്കുക."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"സിം പിൻ കോഡ് തെറ്റാണ്, നിങ്ങളുടെ ഉപകരണം അൺലോക്കുചെയ്യാൻ ഇനി നിങ്ങളുടെ കാരിയറുമായി ബന്ധപ്പെടണം."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{സിം പിൻ കോഡ് തെറ്റാണ്, # ശ്രമം ശേഷിക്കുന്നു, അതുകഴിഞ്ഞാൽ ഉപകരണം അൺലോക്ക് ചെയ്യാൻ സേവനദാതാവിനെ ബന്ധപ്പെണം.}other{സിം പിൻ കോഡ് തെറ്റാണ്, # ശ്രമങ്ങൾ ശേഷിക്കുന്നു. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">സിം പിൻ കോഡ് തെറ്റാണ്, നിങ്ങൾക്ക് <xliff:g id="NUMBER_1">%d</xliff:g> ശ്രമങ്ങൾ കൂടി ശേഷിക്കുന്നു.</item>
+      <item quantity="one">സിം പിൻ കോഡ് തെറ്റാണ്, ഉപകരണം അൺലോക്കുചെയ്യാൻ സേവനദാതാവുമായി ബന്ധപ്പെടേണ്ടിവരുന്നതിനു മുമ്പായി നിങ്ങൾക്ക് <xliff:g id="NUMBER_0">%d</xliff:g> ശ്രമം കൂടി ശേഷിക്കുന്നു.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"സിം ഉപയോഗയോഗ്യമല്ല. നിങ്ങളുടെ കാരിയറെ ബന്ധപ്പെടുക."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{സിം PUK കോഡ് തെറ്റാണ്, # ശ്രമം ശേഷിക്കുന്നു, അതുകഴിഞ്ഞാൽ സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകും.}other{സിം PUK കോഡ് തെറ്റാണ്, # ശ്രമങ്ങൾ ശേഷിക്കുന്നു, അതുകഴിഞ്ഞാൽ സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകും.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">സിം PUK കോഡ് തെറ്റാണ്, സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകുന്നതിന് മുമ്പായി നിങ്ങൾക്ക് <xliff:g id="NUMBER_1">%d</xliff:g> ശ്രമങ്ങൾ കൂടി ശേഷിക്കുന്നു.</item>
+      <item quantity="one">സിം PUK കോഡ് തെറ്റാണ്, സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകുന്നതിന് മുമ്പായി നിങ്ങൾക്ക് <xliff:g id="NUMBER_0">%d</xliff:g> ശ്രമം കൂടി ശേഷിക്കുന്നു.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"പിൻ ഉപയോഗിച്ച് സിം അൺലോക്കു‌ചെയ്യാനുള്ള ‌ശ്രമം പരാജയപ്പെട്ടു!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"PUK ഉപയോഗിച്ച് സിം അൺലോക്കു‌ചെയ്യാനുള്ള ‌ശ്രമം പരാജയപ്പെട്ടു!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ഇൻപുട്ട് രീതി മാറുക"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പാറ്റേൺ വരയ്‌ക്കേണ്ടതുണ്ട്"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പിൻ നൽകേണ്ടതുണ്ട്"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പാസ്‌വേഡ് നൽകേണ്ടതുണ്ട്"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"അധിക സുരക്ഷയ്ക്കായി, പകരം പാറ്റേൺ ഉപയോഗിക്കുക"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"അധിക സുരക്ഷയ്ക്കായി, പകരം പിൻ ഉപയോഗിക്കുക"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"അധിക സുരക്ഷയ്ക്കായി, പകരം പാസ്‍വേഡ് ഉപയോഗിക്കുക"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"അധിക സുരക്ഷയ്ക്ക് പാറ്റേൺ ആവശ്യമാണ്"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"അധിക സുരക്ഷയ്ക്ക് പിൻ ആവശ്യമാണ്"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"അധിക സുരക്ഷയ്ക്ക് പാസ്‌വേഡ് ആവശ്യമാണ്"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ഉപകരണം അഡ്‌മിൻ ലോക്കുചെയ്തു"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ഉപകരണം നേരിട്ട് ലോക്കുചെയ്തു"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"തിരിച്ചറിയുന്നില്ല"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ഫെയ്സ് അൺലോക്കിന് ക്രമീകരണത്തിൽ ക്യാമറാ ആക്സസ് ഓണാക്കൂ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{സിം പിൻ നൽകുക. # ശ്രമം ശേഷിക്കുന്നു, അതുകഴിഞ്ഞാൽ ഉപകരണം അൺലോക്ക് ചെയ്യാൻ സേവനദാതാവിനെ ബന്ധപ്പെടണം.}other{സിം പിൻ നൽകുക. # ശ്രമങ്ങൾ ശേഷിക്കുന്നു.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{സിം ഇപ്പോൾ പ്രവർത്തനരഹിതമായിരിക്കുന്നു. തുടരുന്നതിന് PUK കോഡ് നൽകുക. # ശ്രമം ശേഷിക്കുന്നു, അതുകഴിഞ്ഞാൽ സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകും. വിശദാംശങ്ങൾക്ക് സേവനദാതാവിനെ ബന്ധപ്പെടുക.}other{സിം ഇപ്പോൾ പ്രവർത്തനരഹിതമായിരിക്കുന്നു. തുടരുന്നതിന് PUK കോഡ് നൽകുക. # ശ്രമങ്ങൾ ശേഷിക്കുന്നു, അതുകഴിഞ്ഞാൽ സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകും. വിശദാംശങ്ങൾക്ക് സേവനദാതാവിനെ ബന്ധപ്പെടുക.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ഫെയ്‌സ് അൺലോക്ക് ഉപയോഗിക്കാൻ, ക്രമീകരണം &gt; സ്വകാര്യത എന്നതിൽ "<b>"ക്യാമറാ ആക്‌സസ്"</b>" ഓണാക്കുക"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">സിം പിൻ നൽകുക. നിങ്ങൾക്ക് <xliff:g id="NUMBER_1">%d</xliff:g> ശ്രമങ്ങൾ കൂടി ശേഷിക്കുന്നു.</item>
+      <item quantity="one">സിം പിൻ നൽകുക. ഉപകരണം അൺലോക്ക് ചെയ്യാൻ കാരിയറുമായി ബന്ധപ്പെടേണ്ടിവരുന്നതിന് മുമ്പ് <xliff:g id="NUMBER_0">%d</xliff:g> ശ്രമം കൂടി ശേഷിക്കുന്നു.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">സിം ഇപ്പോൾ പ്രവർത്തനരഹിതമാക്കി. തുടരുന്നതിന് PUK കോഡ് നൽകുക. സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകുന്നതിന് മുമ്പായി <xliff:g id="_NUMBER_1">%d</xliff:g> ശ്രമങ്ങൾ കൂടി ശേഷിക്കുന്നു. വിശദാംശങ്ങൾക്ക് കാരിയറുമായി ബന്ധപ്പെടുക.</item>
+      <item quantity="one">സിം ഇപ്പോൾ പ്രവർത്തനരഹിതമാക്കി. തുടരുന്നതിന് PUK കോഡ് നൽകുക. സിം ശാശ്വതമായി ഉപയോഗശൂന്യമാകുന്നതിന് മുമ്പായി <xliff:g id="_NUMBER_0">%d</xliff:g> ശ്രമം കൂടി ശേഷിക്കുന്നു. വിശദാംശങ്ങൾക്ക് കാരിയറുമായി ബന്ധപ്പെടുക.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ഡിഫോൾട്ട്"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ബബിൾ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"അനലോഗ്"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"തുടരാൻ നിങ്ങളുടെ ഉപകരണം അൺലോക്ക് ചെയ്യുക"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mn/strings.xml b/packages/SystemUI/res-keyguard/values-mn/strings.xml
index 07a2194..82fbaf1 100644
--- a/packages/SystemUI/res-keyguard/values-mn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mn/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Карт хүчингүй байна."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Цэнэглэсэн"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Утасгүй цэнэглэж байна"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Цэнэглэж байна"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Цэнэглэх холбогч"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Цэнэглэж байна"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Хурдан цэнэглэж байна"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Удаан цэнэглэж байна"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Батарейг хамгаалахын тулд цэнэглэх явцыг оновчилсон"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Цэнэглэхийг түр хязгаарласан"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Түгжээг тайлах бол цэсийг дарна уу."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Сүлжээ түгжигдсэн"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM байхгүй"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM нэмнэ үү."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM дутуу эсвэл үүнийг унших боломжгүй байна. SIM нэмнэ үү."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Ашиглах боломжгүй SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Таны SIM-г бүрмөсөн идэвхгүй болгосон байна.\n Өөр SIM авах бол утасгүй үйлчилгээ үзүүлэгчтэйгээ холбогдоно уу."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-г түгжсэн байна."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-г PUK-р түгжсэн байна."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-н түгжээг тайлж байна…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM карт алга"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM картыг оруулна уу."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM карт байхгүй, эсвэл унших боломжгүй байна. SIM карт оруулна уу."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Ашиглах боломжгүй SIM карт байна."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Таны SIM карт бүрмөсөн хаагдлаа.\n Өөр SIM карт авах бол wi-fi үйлчилгээ үзүүлэгчтэй холбогдоно уу."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM карт түгжигдсэн."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM картыг PUK-р түгжсэн байна."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM картын түгжээг тайлж байна…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"ПИН кодын хэсэг"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Төхөөрөмжийн нууц үг"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM-н ПИН кодын хэсэг"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\"-г идэвхгүй болголоо. Үргэлжлүүлэхийн тулд PUK кодыг оруулна уу. Дэлгэрэнгүй мэдээлэл авах бол оператор компанитайгаа холбогдоно уу."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Дурын ПИН код оруулна уу"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Хүссэн ПИН кодоо баталгаажуулна уу"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-н түгжээг тайлж байна…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM картын түгжээг тайлж байна…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4-8 тооноос бүтэх ПИН-г оруулна уу."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK код 8-с цөөнгүй тооноос бүтнэ."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Та ПИН кодоо <xliff:g id="NUMBER_0">%1$d</xliff:g> удаа буруу орууллаа. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секундын дараа дахин оролдоно уу."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Та нууц үгээ <xliff:g id="NUMBER_0">%1$d</xliff:g> удаа буруу орууллаа. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секундын дараа дахин оролдоно уу."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Та тайлах хээг <xliff:g id="NUMBER_0">%1$d</xliff:g> удаа буруу орууллаа. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> секундын дараа дахин оролдоно уу."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM-н ПИН кодыг буруу оруулсан тул та төхөөрөмжийнхөө түгжээг тайлахын тулд оператор компанитайгаа холбогдоно уу."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM-н ПИН код буруу байна, танд оператор компанитайгаа холбогдохгүйгээр төхөөрөмжийн түгжээг тайлах # оролдлого үлдлээ.}other{SIM-н ПИН код буруу байна, танд # оролдлого үлдлээ. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM-н ПИН код буруу байна. Танд <xliff:g id="NUMBER_1">%d</xliff:g> оролдлого үлдлээ.</item>
+      <item quantity="one">SIM-н ПИН код буруу байна, танд мобайл оператортойгоо холбогдохгүйгээр төхөөрөмжийн түгжээг тайлахад <xliff:g id="NUMBER_0">%d</xliff:g> оролдлого хийх боломж үлдсэн байна.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-г ашиглах боломжгүй байна. Оператор компанитайгаа холбогдоно уу."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM-н PUK код буруу байна, таны SIM бүрмөсөн хүчингүй болох хүртэл # оролдлого үлдлээ.}other{SIM-н PUK код буруу байна, таны SIM бүрмөсөн хүчингүй болох хүртэл # оролдлого үлдлээ.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM-н PUK код буруу байна. Таны SIM бүрмөсөн хаагдах хүртэл танд <xliff:g id="NUMBER_1">%d</xliff:g> оролдлого үлдлээ.</item>
+      <item quantity="one">SIM-н PUK код буруу байна. Таны SIM бүрмөсөн хаагдах хүртэл танд <xliff:g id="NUMBER_0">%d</xliff:g> оролдлого үлдлээ.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM-н ПИН-г буруу орууллаа!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM-н PUK-г буруу орууллаа!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Оруулах аргыг сэлгэх"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Төхөөрөмжийг дахин эхлүүлсний дараа загвар оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Төхөөрөмжийг дахин эхлүүлсний дараа ПИН оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Төхөөрөмжийг дахин эхлүүлсний дараа нууц үг оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Нэмэлт аюулгүй байдлын үүднээс оронд нь хээ ашиглана уу"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Нэмэлт аюулгүй байдлын үүднээс оронд нь ПИН ашиглана уу"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Нэмэлт аюулгүй байдлын үүднээс оронд нь нууц үг ашиглана уу"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Аюулгүй байдлын үүднээс загвар оруулах шаардлагатай"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Аюулгүй байдлын үүднээс ПИН оруулах шаардлагатай"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Аюулгүй байдлын үүднээс нууц үг оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Админ төхөөрөмжийг түгжсэн"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Төхөөрөмжийг гараар түгжсэн"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таньж чадсангүй"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Царайгаар түгжээ тайлахыг ашиглахын тулд Тохиргоо хэсэгт камерын хандалтыг асаана уу"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM-н ПИН-г оруулна уу. Танд оператор компанитайгаа холбогдохгүйгээр төхөөрөмжийн түгжээг тайлах # оролдлого үлдлээ.}other{SIM-н ПИН-г оруулна уу. Танд # оролдлого үлдлээ.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-г идэвхгүй болголоо. Үргэлжлүүлэхийн тулд PUK кодыг оруулна уу. Таны SIM бүрмөсөн хүчингүй болох хүртэл # оролдлого үлдлээ. Дэлгэрэнгүй мэдээлэл авахын тулд оператор компанитайгаа холбогдоно уу.}other{SIM-г идэвхгүй болголоо. Үргэлжлүүлэхийн тулд PUK кодыг оруулна уу. Таны SIM бүрмөсөн хүчингүй болох хүртэл # оролдлого үлдлээ. Дэлгэрэнгүй мэдээлэл авахын тулд оператор компанитайгаа холбогдоно уу.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Царайгаар түгжээ тайлахыг ашиглахын тулд Тохиргоо &gt; Нууцлал хэсэгт "<b>" Камерын хандалтыг "</b>" асаана уу"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM-н ПИН кодыг оруулна уу. Танд <xliff:g id="NUMBER_1">%d</xliff:g> оролдлого үлдлээ.</item>
+      <item quantity="one">SIM-н ПИН кодыг оруулна уу. Танд оператор компанитайгаа холбогдохгүйгээр төхөөрөмжийн түгжээг тайлах <xliff:g id="NUMBER_0">%d</xliff:g> оролдлого үлдлээ.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM-г идэвхгүй болголоо. Үргэлжлүүлэхийн тулд PUK кодыг оруулна уу. Таны SIM бүрмөсөн хүчингүй болох хүртэл <xliff:g id="_NUMBER_1">%d</xliff:g> оролдлого үлдлээ. Дэлгэрэнгүй мэдээлэл авахын тулд оператор компанитайгаа холбогдоно уу.</item>
+      <item quantity="one">SIM-г идэвхгүй болголоо. Үргэлжлүүлэхийн тулд PUK кодыг оруулна уу. Таны SIM бүрмөсөн хүчингүй болох хүртэл <xliff:g id="_NUMBER_0">%d</xliff:g> оролдлого үлдлээ. Дэлгэрэнгүй мэдээлэл авахын тулд оператор компанитайгаа холбогдоно уу.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Өгөгдмөл"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бөмбөлөг"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Aналог"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Үргэлжлүүлэхийн тулд төхөөрөмжийнхөө түгжээг тайлна уу"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mr/strings.xml b/packages/SystemUI/res-keyguard/values-mr/strings.xml
index 855558d..012c83b 100644
--- a/packages/SystemUI/res-keyguard/values-mr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mr/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"अवैध कार्ड."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"चार्ज झाली"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • वायरलेस पद्धतीने चार्ज करत आहे"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्ज होत आहे"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्जिंग डॉक"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्ज होत आहे"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • वेगाने चार्ज होत आहे"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • सावकाश चार्ज होत आहे"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • बॅटरीचे संरक्षण करण्यासाठी चार्जिंग ऑप्टिमाइझ केले आहे"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्जिंग तात्पुरते मर्यादित आहे"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"अनलॉक करण्यासाठी मेनू दाबा."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"नेटवर्क लॉक केले"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"सिम नाही"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"सिम जोडा."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"सिम गहाळ झाले आहे किंवा ते रीड करू शकत नाही. सिम जोडा."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"वापरण्यायोग्य नसलेले सिम."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"तुमचे सिम कायमचे डीॲक्टिव्हेट केले गेले आहे.\n दुसऱ्या सिमसाठी तुमच्या वायरलेस सेवा पुरवठादाराशी संपर्क साधा."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"सिम लॉक केलेले आहे."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"सिम PUK लॉक केलेले आहे."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"सिम अनलॉक करत आहे…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"सिम कार्ड नाही"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"सिम कार्ड घाला."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"सिम कार्ड गहाळ झाले आहे किंवा ते वाचनीय नाही. सिम कार्ड घाला."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"निरुपयोगी सिम कार्ड."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"तुमचे सिम कार्ड कायमचे अक्षम केले गेले आहे.\n दुसर्‍या सिम कार्डसाठी आपल्‍या वायरलेस सेवा प्रदात्‍याशी संपर्क साधा."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"सिम कार्ड लॉक झाले आहे."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"सिम कार्ड PUK-लॉक केलेले आहे."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"सिम कार्ड अनलॉक करत आहे…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"पिन क्षेत्र"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"डिव्हाइस पासवर्ड"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"सिम पिन क्षेत्र"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" सिम आता अक्षम केले आहे. सुरू ठेवण्यासाठी PUK कोड एंटर करा. तपशीलांसाठी वाहकाशी संपर्क साधा."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"इच्छित पिन कोड एंटर करा"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"इच्छित पिन कोड ची पुष्टी करा"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"सिम अनलॉक करत आहे…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"सिम कार्ड अनलॉक करत आहे…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 ते 8 अंकांचा पिन टाईप करा."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK कोड 8 अंकी किंवा त्यापेक्षा अधिकचा असावा."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"तुम्ही तुमचा PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> वेळा चुकीच्या पद्धतीने टाइप केला आहे. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> सेकंदांमध्ये पुन्हा प्रयत्न करा."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"तुम्ही तुमचा पासवर्ड <xliff:g id="NUMBER_0">%1$d</xliff:g> वेळा चुकीच्या पद्धतीने टाइप केला आहे. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> सेकंदांमध्ये पुन्हा प्रयत्न करा."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"तुम्ही तुमचा अनलॉक पॅटर्न <xliff:g id="NUMBER_0">%1$d</xliff:g> वेळा अयोग्यरितीने काढला. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> सेकंदांमध्ये पुन्हा प्रयत्न करा."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"सिम पिन कोड चुकीचा आहे तुम्ही आता तुमचे डिव्हाइस अनलॉक करण्‍यासाठी तुमच्या वाहकाशी संपर्क साधावा."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{सिमचा पिन कोड चुकीचा आहे, तुम्ही तुमचे डिव्‍हाइस अनलॉक करण्‍यासाठी तुमच्या वाहकाशी संपर्क साधण्‍यापूर्वी तुमच्याकडे # प्रयत्न शिल्लक आहे.}other{सिमचा पिन कोड चुकीचा आहे, तुमच्याकडे # प्रयत्न शिल्लक आहेत. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">चुकीचा सिम पिन कोड, तुमच्याकडे <xliff:g id="NUMBER_1">%d</xliff:g> प्रयत्न शिल्लक आहेत.</item>
+      <item quantity="one"> चुकीचा सिम पिन कोड, तुमचे डिव्हाइस अनलॉक करण्‍यासाठी तुमच्या वाहकाशी संपर्क साधण्‍यापूर्वी तुमच्याकडे <xliff:g id="NUMBER_0">%d</xliff:g> प्रयत्न शिल्लक आहेत.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"सिम निरुपयोगी आहे. आपल्या वाहकाशी संपर्क साधा."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{सिमचा PUK कोड चुकीचा आहे, सिम कायमचे निरुपयोगी होण्‍यापूर्वी तुमच्याकडे # प्रयत्न शिल्लक आहे.}other{सिमचा PUK कोड चुकीचा आहे, सिम कायमचे निरुपयोगी होण्‍यापूर्वी तुमच्याकडे # प्रयत्न शिल्लक आहेत.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">चुकीचा सिम PUK कोड, सिम कायमचे निरुपयोगी होण्यापूर्वी आपल्याकडे <xliff:g id="NUMBER_1">%d</xliff:g> प्रयत्न शिल्लक आहेत.</item>
+      <item quantity="one">चुकीचा सिम PUK कोड, सिम कायमचे निरुपयोगी होण्यापूर्वी आपल्याकडे <xliff:g id="NUMBER_0">%d</xliff:g> प्रयत्न शिल्लक आहे.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"सिम पिन ऑपरेशन अयशस्वी झाले!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"सिम PUK कार्य अयशस्‍वी झाले!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"इनपुट पद्धत स्विच करा"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिव्हाइस रीस्टार्ट झाल्यावर पॅटर्न आवश्यक आहे"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिव्हाइस रीस्टार्ट झाल्यावर पिन आवश्यक आहे"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिव्हाइस रीस्टार्ट झाल्यावर पासवर्ड आवश्यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"अतिरिक्त सुरक्षेसाठी, त्याऐवजी पॅटर्न वापरा"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"अतिरिक्त सुरक्षेसाठी, त्याऐवजी पिन वापरा"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"अतिरिक्त सुरक्षेसाठी, त्याऐवजी पासवर्ड वापरा"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षिततेसाठी पॅटर्न आवश्‍यक आहे"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षिततेसाठी पिन आवश्‍यक आहे"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षिततेसाठी पासवर्ड आवश्‍यक आहे"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकाद्वारे लॉक केलेले डिव्हाइस"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिव्हाइस मॅन्युअली लॉक केले होते"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ओळखले नाही"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"फेस अनलॉक साठी, सेटिंग्ज मध्ये कॅमेराचा अ‍ॅक्सेस द्या"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{सिमचा पिन एंटर करा. तुम्ही तुमचे डिव्‍हाइस अनलॉक करण्‍यासाठी तुमच्या वाहकाशी संपर्क साधण्‍यापूर्वी, तुमच्याकडे # प्रयत्न शिल्लक आहे.}other{सिमचा पिन एंटर करा. तुमच्याकडे # शिल्लक प्रयत्न आहेत.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{सिम आता बंद केलेले आहे. पुढे सुरू ठेवण्यासाठी PUK कोड एंटर करा. सिम कायमचे निरुपयोगी होण्यापूर्वी तुमच्याकडे # प्रयत्न शिल्लक आहे. तपशिलांसाठी वाहकाशी संपर्क साधा.}other{सिम आता बंद केलेले आहे. पुढे सुरू ठेवण्यासाठी PUK कोड एंटर करा. सिम कायमचे निरुपयोगी होण्यापूर्वी तुमच्याकडे # प्रयत्न शिल्लक आहेत. तपशिलांसाठी वाहकाशी संपर्क साधा.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"फेस अनलॉक वापरण्यासाठी, सेटिंग्ज &gt; गोपनीयता येथे "<b>"कॅमेरा अ‍ॅक्सेस"</b>" सुरू करा"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">सिम पिन एंटर करा, तुमच्याकडे <xliff:g id="NUMBER_1">%d</xliff:g> प्रयत्न शिल्लक आहेत.</item>
+      <item quantity="one">सिम पिन एंटर करा. तुम्ही तुमचे डिव्‍हाइस अनलॉक करण्‍यासाठी तुमच्या वाहकाशी संपर्क साधण्‍यापूर्वी, तुमच्याकडे <xliff:g id="NUMBER_0">%d</xliff:g> प्रयत्न शिल्लक आहे.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">सिम आता बंद केलेले आहे. सुरू ठेवण्यासाठी PUK कोड टाका. सिम कायमचे बंद होण्याआधी तुमच्याकडे <xliff:g id="_NUMBER_1">%d</xliff:g> प्रयत्न शिल्लक आहेत. तपशीलांसाठी वाहकाशी संपर्क साधा.</item>
+      <item quantity="one">सिम आता बंद केलेले आहे. सुरू ठेवण्यासाठी PUK कोड टाका. सिम कायमचे बंद होण्याआधी तुमच्याकडे <xliff:g id="_NUMBER_0">%d</xliff:g> प्रयत्न शिल्लक आहे. तपशीलांसाठी वाहकाशी संपर्क साधा.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"डीफॉल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"अ‍ॅनालॉग"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"पुढे सुरू ठेवण्यासाठी तुमचे डिव्हाइस अनलॉक करा"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ms/strings.xml b/packages/SystemUI/res-keyguard/values-ms/strings.xml
index 032cb3d..29672c1 100644
--- a/packages/SystemUI/res-keyguard/values-ms/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ms/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Kad Tidak Sah."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Sudah dicas"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengecas secara wayarles"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengecas"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengecas dengan Dok"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengecas"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengecas dengan cepat"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mengecas dengan perlahan"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Pengecasan dioptimumkan untuk melindungi bateri"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Pengecasan terhad sementara"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Tekan Menu untuk membuka kunci."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rangkaian dikunci"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Tiada SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Tambah SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM tiada atau tidak boleh dibaca. Tambah SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM tidak boleh digunakan."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM anda telah dinyahaktifkan secara kekal.\n Hubungi penyedia perkhidmatan wayarles anda untuk mendapatkan SIM lain."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM dikunci."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM dikunci PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Membuka kunci SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Tiada kad SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Masukkan kad SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Kad SIM tiada atau tidak dapat dibaca. Sila masukkan kad SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Kad SIM tidak boleh digunakan."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Kad SIM anda telah dilumpuhkan secara kekal.\n Hubungi pembekal perkhidmatan wayarles anda untuk mendapatkan kad SIM lain."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Kad SIM dikunci."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Kad SIM dikunci dengan PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Membuka kunci kad SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Bahagian PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Kata laluan peranti"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Bahagian PIN SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" kini dilumpuhkan. Masukkan kod PUK untuk meneruskan. Hubungi pembawa untuk mendapatkan butiran."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Masukkan kod PIN yang diingini"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Sahkan kod PIN yang diingini"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Membuka kunci SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Membuka kunci kad SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Taipkan PIN yang mengandungi 4 hingga 8 nombor."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Kod PUK seharusnya 8 nombor atau lebih."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Anda telah tersilap taip PIN sebanyak <xliff:g id="NUMBER_0">%1$d</xliff:g> kali. \n\nCuba lagi dalam <xliff:g id="NUMBER_1">%2$d</xliff:g> saat."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Anda telah tersilap taip kata laluan sebanyak <xliff:g id="NUMBER_0">%1$d</xliff:g> kali. \n\nCuba lagi dalam <xliff:g id="NUMBER_1">%2$d</xliff:g> saat."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Anda telah tersilap lukis corak buka kunci sebanyak <xliff:g id="NUMBER_0">%1$d</xliff:g> kali. \n\nCuba lagi dalam <xliff:g id="NUMBER_1">%2$d</xliff:g> saat."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Kod PIN SIM salah. Anda mesti menghubungi pembawa anda untuk membuka kunci peranti."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Kod PIN SIM tidak betul, anda mempunyai # percubaan lagi sebelum perlu menghubungi pembawa anda untuk membuka kunci peranti.}other{Kod PIN SIM tidak betul, anda mempunyai # percubaan lagi. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Kod PIN SIM salah, tinggal <xliff:g id="NUMBER_1">%d</xliff:g> percubaan.</item>
+      <item quantity="one">Kod PIN SIM salah. Tinggal <xliff:g id="NUMBER_0">%d</xliff:g> percubaan sebelum anda perlu menghubungi pembawa anda untuk membuka kunci peranti.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM tidak boleh digunakan. Hubungi pembawa anda."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Kod PUK SIM tidak betul, anda mempunyai # percubaan lagi sebelum SIM tidak boleh digunakan secara kekal.}other{Kod PUK SIM tidak betul, anda mempunyai # percubaan lagi sebelum SIM tidak boleh digunakan secara kekal.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Kod PUK SIM salah. Tinggal <xliff:g id="NUMBER_1">%d</xliff:g> percubaan sebelum SIM tidak boleh digunakan secara kekal.</item>
+      <item quantity="one">Kod PUK SIM salah. Tinggal <xliff:g id="NUMBER_0">%d</xliff:g> percubaan sebelum SIM tidak boleh digunakan secara kekal.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Pengendalian PIN SIM gagal!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Pengendalian PUK SIM gagal!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Tukar kaedah masukan"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Corak diperlukan setelah peranti dimulakan semula"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah peranti dimulakan semula"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kata laluan diperlukan setelah peranti dimulakan semula"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Untuk keselamatan tambahan, gunakan corak"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Untuk keselamatan tambahan, gunakan PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Untuk keselamatan tambahan, gunakan kata laluan"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Corak diperlukan untuk keselamatan tambahan"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keselamatan tambahan"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kata laluan diperlukan untuk keselamatan tambahan"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Peranti dikunci oleh pentadbir"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Peranti telah dikunci secara manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Hidupkan kamera dalam Tetapan untuk Buka Kunci Wajah"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Masukkan PIN SIM. Anda mempunyai # percubaan lagi sebelum perlu menghubungi pembawa anda untuk membuka kunci peranti.}other{Masukkan PIN SIM. Anda mempunyai # percubaan lagi.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Kini SIM dilumpuhkan. Masukkan kod PUK untuk meneruskan. Anda mempunyai # percubaan lagi sebelum SIM tidak boleh digunakan secara kekal. Hubungi pembawa untuk mendapatkan butiran.}other{Kini SIM dilumpuhkan. Masukkan kod PUK untuk meneruskan. Anda mempunyai # percubaan lagi sebelum SIM tidak boleh digunakan secara kekal. Hubungi pembawa untuk mendapatkan butiran.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Untuk menggunakan Buka Kunci Wajah, hidupkan "<b>"akses Kamera"</b>" dalam Tetapan &gt; Privasi"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Masukkan PIN SIM. Tinggal <xliff:g id="NUMBER_1">%d</xliff:g> percubaan lagi.</item>
+      <item quantity="one">Masukkan PIN SIM. Tinggal <xliff:g id="NUMBER_0">%d</xliff:g> percubaan lagi sebelum anda perlu menghubungi pembawa anda untuk membuka kunci peranti.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Kini SIM dilumpuhkan. Masukkan kod PUK untuk meneruskan. Tinggal <xliff:g id="_NUMBER_1">%d</xliff:g> percubaan sebelum SIM tidak boleh digunakan secara kekal. Hubungi pembawa untuk mendapatkan butiran.</item>
+      <item quantity="one">Kini SIM dilumpuhkan. Masukkan kod PUK untuk meneruskan. Tinggal <xliff:g id="_NUMBER_0">%d</xliff:g> percubaan sebelum SIM tidak boleh digunakan secara kekal. Hubungi pembawa untuk mendapatkan butiran.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Lalai"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Gelembung"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Buka kunci peranti anda untuk meneruskan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-my/strings.xml b/packages/SystemUI/res-keyguard/values-my/strings.xml
index d38fb8f..91dcb9c 100644
--- a/packages/SystemUI/res-keyguard/values-my/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-my/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ကတ် မမှန်ကန်ပါ။"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"အားသွင်းပြီးပါပြီ"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ကြိုးမဲ့ အားသွင်းနေသည်"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • အားသွင်းနေသည်"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • အားသွင်းအထိုင်"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • အားသွင်းနေသည်"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • အမြန်အားသွင်းနေသည်"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • နှေးကွေးစွာ အားသွင်းနေသည်"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ဘက်ထရီကာကွယ်ရန် အားသွင်းခြင်းကို အကောင်းဆုံးပြင်ဆင်ထားသည်"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • အားသွင်းခြင်းကို လောလောဆယ် ကန့်သတ်ထားသည်"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"မီနူးကို နှိပ်၍ လော့ခ်ဖွင့်ပါ။"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ကွန်ရက်ကို လော့ခ်ချထားသည်"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"ဆင်းမ်မရှိပါ"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"ဆင်းမ်ထည့်ပါ။"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"ဆင်းမ်မရှိပါ (သို့) သုံး၍မရပါ။ ဆင်းမ်ထည့်ပါ။"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ဆင်းမ်ကို သုံး၍မရပါ။"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"သင်၏ဆင်းမ်ကို အပြီးပိတ်လိုက်သည်။\n ဆင်းမ်နောက်တစ်ခု ရယူရန် သင်၏ ကြိုးမဲ့ဝန်ဆောင်မှုပေးသူထံ ဆက်သွယ်ပါ။"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"ဆင်းမ်ကို လော့ခ်ချထားသည်။"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"ဆင်းမ်၏ ပင်နံပါတ်ပြန်ဖွင့်သည့် ကုဒ်ကို လော့ခ်ချထားသည်။"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"ဆင်းမ်ကိုဖွင့်နေသည်…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"ဆင်းမ်ကတ် မရှိပါ"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ဆင်းမ်ကတ် ထည့်ပါ။"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ဆင်းမ်ကတ်မရှိပါ သို့မဟုတ် အသုံးပြု၍မရပါ။ ဆင်းမ်ကတ်တစ်ခု ထည့်ပါ။"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"အသုံးပြု၍ မရတော့သော ဆင်းမ်ကတ်။"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"သင့်ဆင်းမ်ကတ်ကို အပြီးအပိုင် ပိတ်လိုက်ပါပြီ။\n နောက်ထပ်ဆင်းမ်ကတ်တစ်ခု ရယူရန်အတွက် သင်၏ ကြိုးမဲ့ဝန်ဆောင်မှုပေးသူထံ ဆက်သွယ်ပါ။"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"ဆင်းမ်ကတ် လော့ခ်ကျနေပါသည်။"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ဆင်းမ်ကတ်သည် ပင်နံပါတ် ပြန်ဖွင့်သည့်ကုဒ် လော့ခ်ကျနေပါသည်။"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"ဆင်းမ်ကတ်ကို လော့ခ်ဖွင့်နေပါသည်…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"ပင်နံပါတ်နေရာ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"စက်စကားဝှက်"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"ဆင်းမ်ပင်နံပါတ်နေရာ"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" ဆင်းမ်ကို ယခု ပိတ်လိုက်ပါပြီ။ ရှေ့ဆက်ရန် ပင်နံပါတ် ပြန်ဖွင့်သည့်ကုဒ်ကို ထည့်ပါ။ အသေးစိတ် အချက်အလက်များအတွက် မိုဘိုင်းဝန်ဆောင်မှုပေးသူထံ ဆက်သွယ်ပါ။"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"မိမိလိုလားသော ပင်နံပါတ်ကို ထည့်ပါ"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"မိမိလိုလားသော ပင်နံပါတ်ကို အတည်ပြုပါ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"ဆင်းမ်ကိုဖွင့်နေသည်…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"ဆင်းမ်ကတ်ကို လော့ခ်ဖွင့်နေပါသည်…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"ဂဏန်း ၄ လုံးမှ ၈ လုံးအထိ ရှိသော ပင်နံပါတ်ကို ထည့်ပါ။"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"ပင်နံပါတ် ပြန်ဖွင့်သည့်ကုဒ်သည် ဂဏန်း ၈ လုံးနှင့် အထက် ဖြစ်ရပါမည်။"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"သင်သည် ပင်နံပါတ်ကို <xliff:g id="NUMBER_0">%1$d</xliff:g> ကြိမ်မှားယွင်းစွာ ထည့်ခဲ့ပါသည်။ \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> စက္ကန့်အကြာတွင် ထပ်စမ်းကြည့်ပါ။"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"သင်သည် စကားဝှက်ကို <xliff:g id="NUMBER_0">%1$d</xliff:g> ကြိမ်မှားယွင်းစွာ ထည့်ခဲ့ပါသည်။ \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> စက္ကန့်အကြာတွင် ထပ်စမ်းကြည့်ပါ။"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"သင်သည် ပုံစံကို <xliff:g id="NUMBER_0">%1$d</xliff:g> ကြိမ်မှားယွင်းစွာ ဆွဲခဲ့ပါသည်။ \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> စက္ကန့်အကြာတွင် ထပ်စမ်းကြည့်ပါ။"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ဆင်းမ်ကဒ်ပင်နံပါတ် မှားယွင်းနေသောကြောင့် ယခုအခါ သင့်စက်ပစ္စည်းအား လော့ခ်ဖွင့်ရန် ဝန်ဆောင်မှုပေးသူကို ဆက်သွယ်ရပါမည်။"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{ဆင်းမ်ပင်နံပါတ်ကုဒ် မှားနေသည်။ စက်ကို လော့ခ်ဖွင့်ရန် မိုဘိုင်းဖုန်းကုမ္ပဏီသို့ မဆက်သွယ်မီ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။}other{ဆင်းမ်ပင်နံပါတ်ကုဒ် မှားနေသည်။ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။ }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">ဆင်းမ်ပင်နံပါတ် မှန်ကန်မှုမရှိပါ။ <xliff:g id="NUMBER_1">%d</xliff:g> ကြိမ် စမ်းသပ်ခွင့်ရှိပါသေးသည်။</item>
+      <item quantity="one">ဆင်းမ်ပင်နံပါတ် မှန်ကန်မှုမရှိပါ။ သင့်စက်ပစ္စည်းအား လော့ခ်ဖွင့်ပေးရန်အတွက် မိုဘိုင်းဝန်ဆောင်မှုပေးသူကို မဆက်သွယ်မီ <xliff:g id="NUMBER_0">%d</xliff:g> ကြိမ် စမ်းသပ်ခွင့်ရှိပါသေးသည်။</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ဆင်းမ်ကဒ်ကို အသုံးပြု၍ မရတော့ပါ။ ဝန်ဆောင်မှုပေးသူကို ဆက်သွယ်ပါ။"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{ဆင်းမ် PUK ကုဒ် မှားနေသည်။ ဆင်းမ် အပြီးပိတ်မသွားမီ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။}other{ဆင်းမ် PUK ကုဒ် မှားနေသည်။ ဆင်းမ် အပြီးပိတ်မသွားမီ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">ဆင်းမ် ပင်နံပါတ် ပြန်ဖွင့်သည့်ကုဒ် မှန်ကန်မှုမရှိပါ။ ဆင်းမ်ကဒ်ကို အပြီးအပိုင်မပိတ်ခင် <xliff:g id="NUMBER_1">%d</xliff:g> ကြိမ်စမ်းသပ်ခွင့် ရှိပါသေးသည်။</item>
+      <item quantity="one">ဆင်းမ် ပင်နံပါတ် ပြန်ဖွင့်သည့်ကုဒ် မှန်ကန်မှုမရှိပါ။ ဆင်းမ်ကဒ်ကို အပြီးအပိုင်မပိတ်ခင် <xliff:g id="NUMBER_0">%d</xliff:g> ကြိမ်စမ်းသပ်ခွင့် ရှိပါသေးသည်။</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"ဆင်းမ်ကဒ်ပင်နံပါတ် လုပ်ဆောင်ချက် မအောင်မြင်ပါ။"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"ဆင်းမ်ကတ် ပင်နံပါတ် ပြန်ဖွင့်သည့်ကုဒ် လုပ်ဆောင်ချက် မအောင်မြင်ပါ။"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"စာရိုက်စနစ်ပြောင်းရန်"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပုံစံ လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပင်နံပါတ် လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် စကားဝှက် လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ထပ်ဆောင်းလုံခြုံရေးအတွက် ၎င်းအစား ပုံစံသုံးပါ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ထပ်ဆောင်းလုံခြုံရေးအတွက် ၎င်းအစား ပင်နံပါတ်သုံးပါ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ထပ်ဆောင်းလုံခြုံရေးအတွက် ၎င်းအစား စကားဝှက်သုံးပါ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပုံစံ လိုအပ်ပါသည်"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပင်နံပါတ် လိုအပ်ပါသည်"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် စကားဝှက် လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"စက်ပစ္စည်းကို စီမံခန့်ခွဲသူက လော့ခ်ချထားပါသည်"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"စက်ပစ္စည်းကို ကိုယ်တိုင်ကိုယ်ကျ လော့ခ်ချထားခဲ့သည်"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"မသိ"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"‘မျက်နှာပြ လော့ခ်ဖွင့်ခြင်း’ သုံးရန် ‘ဆက်တင်များ’ တွင်ကင်မရာသုံးခွင့်ကိုဖွင့်ပါ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{ဆင်းမ်ပင်နံပါတ် ထည့်သွင်းပါ။ သင့်စက်ကို လော့ခ်ဖွင့်ရန် မိုဘိုင်းဖုန်းကုမ္ပဏီသို့ မဆက်သွယ်မီ # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။}other{ဆင်းမ်ပင်နံပါတ် ထည့်သွင်းပါ။ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ဆင်းမ်သည် ယခု ပိတ်သွားပါပြီ။ ရှေ့ဆက်ရန် PUK ကုဒ်ကို ထည့်ပါ။ ဆင်းမ် အပြီးပိတ်မသွားမီ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။ အသေးစိတ်အတွက် မိုဘိုင်းဖုန်းကုမ္ပဏီကို ဆက်သွယ်ပါ။}other{ဆင်းမ်သည် ယခု ပိတ်သွားပါပြီ။ ရှေ့ဆက်ရန် PUK ကုဒ်ကို ထည့်ပါ။ ဆင်းမ် အပြီးပိတ်မသွားမီ သင့်တွင် # ကြိမ် ကြိုးစားခွင့်ရှိသေးသည်။ အသေးစိတ်အတွက် မိုဘိုင်းဖုန်းကုမ္ပဏီကို ဆက်သွယ်ပါ။}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"မျက်နှာပြ လော့ခ်ဖွင့်ခြင်းကို သုံးရန် "<b>"ကင်မရာ သုံးခွင့်"</b>" ကို ‘ဆက်တင်များ &gt; ကန့်သတ်ဆက်တင်’ တွင်ဖွင့်ပါ"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">ဆင်းမ်ကတ် ပင်နံပါတ် ထည့်ပါ။ <xliff:g id="NUMBER_1">%d</xliff:g> ကြိမ် စမ်းသပ်ခွင့်ရှိပါသေးသည်။</item>
+      <item quantity="one">ဆင်းမ်ကတ် ပင်နံပါတ် ထည့်ပါ။ သင့်စက်ကို လော့ခ်ဖွင့်ပေးရန်အတွက် ဝန်ဆောင်မှုပေးသူသို့ မဆက်သွယ်မီ <xliff:g id="NUMBER_0">%d</xliff:g> ကြိမ် စမ်းသပ်ခွင့်ရှိပါသေးသည်။</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">ဆင်းမ်ကတ်သည် ယခု ပိတ်သွားပါပြီ။ ရှေ့ဆက်ရန် PUK ကုဒ်ကို ထည့်ပါ။ ဆင်းမ်ကတ် အပြီးပိတ်မသွားမီ သင့်တွင် <xliff:g id="_NUMBER_1">%d</xliff:g> ကြိမ် စမ်းသပ်ခွင့် ကျန်ပါသေးသည်။ အသေးစိတ်အချက်များအတွက် ဝန်ဆောင်မှုပေးသူကို ဆက်သွယ်ပါ။</item>
+      <item quantity="one">ဆင်းမ်ကတ်သည် ယခု ပိတ်သွားပါပြီ။ ရှေ့ဆက်ရန် PUK ကုဒ်ကို ထည့်ပါ။ ဆင်းမ်ကတ် အပြီးပိတ်မသွားမီ သင့်တွင် <xliff:g id="_NUMBER_0">%d</xliff:g> ကြိမ် စမ်းသပ်ခွင့် ကျန်ပါသေးသည်။ အသေးစိတ်အချက်များအတွက် ဝန်ဆောင်မှုပေးသူကို ဆက်သွယ်ပါ။</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"မူလ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ပူဖောင်းကွက်"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ရိုးရိုး"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ရှေ့ဆက်ရန် သင့်စက်ကိုဖွင့်ပါ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-nb/strings.xml b/packages/SystemUI/res-keyguard/values-nb/strings.xml
index 3bd4ab7..b312706 100644
--- a/packages/SystemUI/res-keyguard/values-nb/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-nb/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ugyldig kort."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Oppladet"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lader trådløst"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lader"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladedokk"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lader"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lader raskt"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lader sakte"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ladingen er optimalisert for å beskytte batteriet"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Lading er midlertidig begrenset"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Trykk på menyknappen for å låse opp."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Nettverket er låst"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Ingen SIM-kort"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Legg til et SIM-kort."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-kortet mangler eller kan ikke leses. Legg til et SIM-kort."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-kortet kan ikke brukes."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM-kortet er deaktivert permanent.\n Kontakt leverandøren av trådløstjenesten for å få et nytt SIM-kort."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-kortet er låst."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-kortet er låst med PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Låser opp SIM-kortet …"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM-kort mangler"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Sett inn et SIM-kort."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-kort mangler eller er uleselig. Sett inn et SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Ubrukelig SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-kortet er deaktivert permanent.\nTa kontakt med leverandøren av trådløstjenesten for å få et nytt SIM-kort."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kortet er låst."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kortet er PUK-låst."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Låser opp SIM-kortet …"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-området"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Enhetspassord"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"PIN-området for SIM-kortet"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Feil mønster"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Feil passord"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"Feil PIN-kode"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Prøv på nytt om # sekund.}other{Prøv på nytt om # sekunder.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Prøv igjen om # sekund.}other{Prøv igjen om # sekunder.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Skriv inn PIN-koden for SIM-kortet."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Skriv inn PIN-koden for SIM-kortet «<xliff:g id="CARRIER">%1$s</xliff:g>»."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Deaktiver e-SIM-kortet for å bruke enheten uten mobiltjeneste."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-kortet «<xliff:g id="CARRIER">%1$s</xliff:g>» er nå deaktivert. Skriv inn PUK-koden for å fortsette. Ta kontakt med operatøren for mer informasjon."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Tast inn ønsket PIN-kode"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Bekreft ønsket PIN-kode"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Låser opp SIM-kortet …"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Låser opp SIM-kortet …"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Skriv inn en PIN-kode på fire til åtte sifre."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-koden skal være på åtte eller flere sifre."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Du har oppgitt feil PIN-kode <xliff:g id="NUMBER_0">%1$d</xliff:g> ganger. \n\nPrøv på nytt om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Du har tastet inn passordet ditt feil <xliff:g id="NUMBER_0">%1$d</xliff:g> ganger. \n\nPrøv på nytt om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Du har tegnet opplåsningsmønsteret ditt feil <xliff:g id="NUMBER_0">%1$d</xliff:g> ganger. \n\nPrøv på nytt om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Feil PIN-kode for SIM-kortet. Du må nå kontakte operatøren din for å låse opp enheten."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Feil PIN-kode for SIM-kortet. Du har # forsøk igjen før du må kontakte operatøren din for å låse opp enheten.}other{Feil PIN-kode for SIM-kortet. Du har # forsøk igjen. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Feil PIN-kode for SIM-kortet. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøk igjen.</item>
+      <item quantity="one">Feil PIN-kode for SIM-kortet. Du har <xliff:g id="NUMBER_0">%d</xliff:g> forsøk igjen før du må kontakte operatøren din for å låse opp enheten.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-kortet er ubrukelig. Ta kontakt med operatøren din."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Feil PUK-kode for SIM-kortet. Du har # forsøk igjen før SIM-kortet blir permanent ubrukelig.}other{Feil PUK-kode for SIM-kortet. Du har # forsøk igjen før SIM-kortet blir permanent ubrukelig.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Feil PUK-kode for SIM-kortet. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøk igjen før SIM-kortet blir permanent ubrukelig.</item>
+      <item quantity="one">Feil PUK-kode for SIM-kortet. Du har <xliff:g id="NUMBER_0">%d</xliff:g> forsøk igjen før SIM-kortet blir permanent ubrukelig.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"PIN-koden for SIM-kortet ble avvist."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"PUK-koden for SIM-kortet ble avvist."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Bytt inndatametode"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du må tegne mønsteret etter at enheten har startet på nytt"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du må skrive inn PIN-koden etter at enheten har startet på nytt"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du må skrive inn passordet etter at enheten har startet på nytt"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Bruk mønster i stedet, for å øke sikkerheten"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Bruk PIN-kode i stedet, for å øke sikkerheten"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Bruk passord i stedet, for å øke sikkerheten"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du må tegne mønsteret for ekstra sikkerhet"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du må skrive inn PIN-koden for ekstra sikkerhet"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du må skrive inn passordet for ekstra sikkerhet"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheten er låst av administratoren"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten ble låst manuelt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke gjenkjent"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Slå på kameratilgang i Innstillinger for å bruke ansiktslås"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Skriv inn PIN-koden for SIM-kortet. Du har # forsøk igjen før du må kontakte operatøren din for å låse opp enheten.}other{Skriv inn PIN-koden for SIM-kortet. Du har # forsøk igjen.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-kortet er deaktivert nå. Skriv inn PUK-koden for å fortsette. Du har # forsøk igjen før SIM-kortet blir permanent ubrukelig. Kontakt operatøren for å få vite mer.}other{SIM-kortet er deaktivert nå. Skriv inn PUK-koden for å fortsette. Du har # forsøk igjen før SIM-kortet blir permanent ubrukelig. Kontakt operatøren for å få vite mer.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"For å bruke ansiktslås, slå på "<b>"Kameratilgang"</b>" i Innstillinger &gt; Personvern"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Skriv inn PIN-koden for SIM-kortet. Du har <xliff:g id="NUMBER_1">%d</xliff:g> forsøk igjen.</item>
+      <item quantity="one">Skriv inn PIN-koden for SIM-kortet. Du har <xliff:g id="NUMBER_0">%d</xliff:g> forsøk igjen før du må kontakte operatøren din for å låse opp enheten.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM-kortet er deaktivert nå. Skriv inn PUK-koden for å fortsette. Du har <xliff:g id="_NUMBER_1">%d</xliff:g> forsøk igjen før SIM-kortet blir permanent ubrukelig. Kontakt operatøren for å få vite mer.</item>
+      <item quantity="one">SIM-kortet er deaktivert nå. Skriv inn PUK-koden for å fortsette. Du har <xliff:g id="_NUMBER_0">%d</xliff:g> forsøk igjen før SIM-kortet blir permanent ubrukelig. Kontakt operatøren for å få vite mer.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lås opp enheten for å fortsette"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ne/strings.xml b/packages/SystemUI/res-keyguard/values-ne/strings.xml
index 26b28fd..39548de 100644
--- a/packages/SystemUI/res-keyguard/values-ne/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ne/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"अमान्य कार्ड।"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"चार्ज भयो"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • तारविनै चार्ज गर्दै"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्ज हुँदै छ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • डक चार्ज हुँदै छ"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्ज गरिँदै"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • द्रुत गतिमा चार्ज गरिँदै छ"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • मन्द गतिमा चार्ज गरिँदै"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ब्याट्री जोगाउन चार्ज गर्ने प्रक्रिया अप्टिमाइज गरिएको छ"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • चार्जिङ केही समयका लागि सीमित पारिएको छ"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"अनलक गर्न मेनु थिच्नुहोस्।"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"नेटवर्क लक भएको छ"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM कार्ड हालिएको छैन"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM कार्ड हाल्नुहोस्।"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM कार्ड हालिएको छैन वा रिड गर्न मिल्दैन। SIM कार्ड हाल्नुहोस्।"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"यो SIM कार्ड प्रयोग गर्न मिल्दैन।"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"तपाईंको SIM कार्ड सदाका लागि डिएक्टिभेट गरिएको छ।\n आफ्नो वायरलेस सेवा प्रदायकलाई सम्पर्क गरी अर्को SIM कार्ड प्राप्त गर्नुहोस्।"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM कार्ड लक गरिएको छ।"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM कार्ड PUK प्रयोग गरी लक गरिएको छ।"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM कार्ड अनलक गरिँदै छ…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM कार्ड छैन"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM कार्ड हाल्नुहोस्"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM कार्ड हालिएको छैन वा पढ्न योग्य छैन। SIM कार्ड हाल्नुहोस्।"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM कार्ड काम नलाग्ने भएको छ।"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"तपाईंको SIM कार्ड सदाका लागि असक्षम भएको छ।\n अर्को SIM कार्डको लागि आफ्नो वायरलेस सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM कार्ड लक भएको छ।"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM कार्ड PUK-लक भएको छ।"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM कार्ड अनलक गरिँदै..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN क्षेत्र"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"डिभाइसको पासवर्ड"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM को PIN क्षेत्र"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM <xliff:g id="CARRIER">%1$s</xliff:g> अहिले असक्षम छ। सुचारु गर्नको लागि PUK कोड प्रविष्टि गर्नुहोस्। विवरणका लागि सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"रूचाइएको PIN कोड प्रविष्टि गर्नुहोस्"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"रूचाइएको PIN कोड पुष्टि गर्नुहोस्"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM कार्ड अनलक गरिँदै छ…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM कार्ड अनलक गरिँदै..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"४ देखि ८ वटा नम्बर भएको एउटा PIN टाइप गर्नुहोस्।"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK कोड ८ वा सो भन्दा बढी नम्बरको हुनु पर्छ।"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"तपाईंले <xliff:g id="NUMBER_0">%1$d</xliff:g> पटक गलत तरिकाले आफ्नो PIN प्रविष्ट गर्नुभएको छ। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> सेकेन्डमा फेरि प्रयास गर्नुहोस्।"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"तपाईंले <xliff:g id="NUMBER_0">%1$d</xliff:g> पटक आफ्नो गलत पासवर्ड  प्रविष्ट गर्नुभएको छ। \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> सेकेन्डमा फेरि प्रयास गर्नुहोस्।"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"तपाईंले <xliff:g id="NUMBER_0">%1$d</xliff:g> पटक गलत तरिकाले आफ्नो अनलक प्याटर्न कोर्नुभएको छ। \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> सेकेन्डमा फेरि कोसिस गर्नुहोस्।"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM को PIN कोड गलत छ। तपाईंले अब आफ्नो यन्त्र खोल्न आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नै पर्ने हुन्छ।"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{तपाईंले SIM को गलत PIN कोड हाल्नुभयो, तपाईं अब # पटक PIN हाल्ने प्रयास गर्न सक्नुहुन्छ। त्यसपछि आफ्नो डिभाइस अनलक गर्न तपाईंले आफ्नो सेवा प्रदायकमा सम्पर्क गर्नु पर्ने हुन्छ।}other{तपाईंले SIM को गलत PIN कोड हाल्नुभयो, तपाईं अब # पटक PIN हाल्ने प्रयास गर्न सक्नुहुन्छ। }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM को PIN कोड गलत छ, तपाईं अझै <xliff:g id="NUMBER_1">%d</xliff:g> पटक प्रयास गर्न सक्नुहुन्छ।</item>
+      <item quantity="one">SIM को PIN कोड गलत छ,तपाईंले आफ्नो डिभाइस अनलक गर्न आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नैपर्ने अवस्था आउनु अघि तपाईं अझै <xliff:g id="NUMBER_0">%d</xliff:g> पटक प्रयास गर्न सक्नुहुन्छ।</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM काम नलाग्ने भएको छ। आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{तपाईंले SIM को गलत PUK कोड हाल्नुभयो, तपाईं अब # पटक PUK हाल्ने प्रयास गर्न सक्नुहुन्छ। त्यसपछि SIM सदाका लागि प्रयोग गर्न नमिल्ने हुने छ।}other{तपाईंले SIM को गलत PUK कोड हाल्नुभयो, तपाईं अब # पटक PUK हाल्ने प्रयास गर्न सक्नुहुन्छ। त्यसपछि SIM सदाका लागि प्रयोग गर्न नमिल्ने हुने छ।}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM को PUK कोड गलत छ, SIM सदाका लागि काम नलाग्ने हुनु अघि तपाईं अझै <xliff:g id="NUMBER_1">%d</xliff:g> पटक प्रयास गर्न सक्नुहुन्छ।</item>
+      <item quantity="one">SIM को PUK कोड गलत छ, SIM सदाका लागि काम नलाग्ने हुनु अघि तपाईं अझै <xliff:g id="NUMBER_0">%d</xliff:g> पटक प्रयास गर्न सक्नुहुन्छ।</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM को PIN कोड राखेर अनलक गर्ने कार्य असफल भयो!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM को PUK कोड राखेर अनलक गर्ने कार्य असफल भयो!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"इनपुट विधिलाई स्विच गर्नुहोस्"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"यन्त्र पुनः सुरु भएपछि ढाँचा आवश्यक पर्दछ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"यन्त्र पुनः सुरु भएपछि PIN आवश्यक पर्दछ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"यन्त्र पुनः सुरु भएपछि पासवर्ड आवश्यक पर्दछ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"अतिरिक्त सुरक्षाका लागि यो प्रमाणीकरण विधिको साटो प्याटर्न प्रयोग गर्नुहोस्"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"अतिरिक्त सुरक्षाका लागि यो प्रमाणीकरण विधिको साटो पिन प्रयोग गर्नुहोस्"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"अतिरिक्त सुरक्षाका लागि यो प्रमाणीकरण विधिको साटो पासवर्ड प्रयोग गर्नुहोस्"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षाको लागि ढाँचा आवश्यक छ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षाको लागि PIN आवश्यक छ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षाको लागि पासवर्ड आवश्यक छ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकले यन्त्रलाई लक गर्नुभएको छ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"यन्त्रलाई म्यानुअल तरिकाले लक गरिएको थियो"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहिचान भएन"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"तपाईं \"फेस अनलक\" प्रयोग गर्न चाहनुहुन्छ भने सेटिङमा गई क्यामेरा प्रयोग गर्ने अनुमति दिनुहोस्"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM को PIN हाल्नुहोस्। तपाईं अब # पटक PIN हाल्ने प्रयास गर्न सक्नुहुन्छ। त्यसपछि आफ्नो डिभाइस अनलक गर्न तपाईंले आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नु पर्ने हुन्छ।}other{SIM को PIN हाल्नुहोस्। तपाईं अब # पटक PIN हाल्ने प्रयास गर्न सक्नुहुन्छ।}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM अहिले अफ गरिएको छ। जारी राख्न PUK कोड हाल्नुहोस्। तपाईं अब # पटक PUK हाल्ने प्रयास गर्न सक्नुहुन्छ। त्यसपछि SIM सदाका लागि प्रयोग गर्न नमिल्ने हुने छ। थप जानकारी प्राप्त गर्न आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।}other{SIM अहिले अफ गरिएको छ। जारी राख्न PUK कोड हाल्नुहोस्। तपाईं अब # पटक PUK हाल्ने प्रयास गर्न सक्नुहुन्छ। त्यसपछि SIM सदाका लागि प्रयोग गर्न नमिल्ने हुने छ। थप जानकारी प्राप्त गर्न आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"फेस अनलक प्रयोग गर्न \"सेटिङ तथा गोपनीयता\" मा गई "<b>"क्यामेरा प्रयोग गर्ने अनुमति"</b>" दिनुहोस्"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM को PIN प्रविष्टि गर्नुहोस्। तपाईंसँग <xliff:g id="NUMBER_1">%d</xliff:g>  प्रयासहरू बाँकी छन्।</item>
+      <item quantity="one">SIM को PIN प्रविष्टि गर्नुहोस्। तपाईंसँग <xliff:g id="NUMBER_0">%d</xliff:g> प्रयास बाँकी छ, त्यसपछि भने आफ्नो डिभाइस अनलक गर्नका लागि तपाईंले अनिवार्य रूपमा आफ्नो सेवा प्रदायकलाई सम्पर्क गर्नु पर्ने हुन्छ।</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM लाई असक्षम पारिएको छ। जारी राख्न PUK कोड प्रविष्टि गर्नुहोस्। तपाईंसँग <xliff:g id="_NUMBER_1">%d</xliff:g> प्रयासहरू बाँकी छन्, त्यसपछि SIM सदाका लागि प्रयोग गर्न नमिल्ने हुन्छ। विवरणहरूका लागि सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।</item>
+      <item quantity="one">SIM लाई असक्षम पारिएको छ। जारी राख्न PUK कोड प्रविष्टि गर्नुहोस्। तपाईंसँग <xliff:g id="_NUMBER_0">%d</xliff:g> प्रयास बाँकी छ, त्यसपछि SIM सदाका लागि प्रयोग गर्न नमिल्ने हुन्छ। विवरणहरूका लागि सेवा प्रदायकलाई सम्पर्क गर्नुहोस्।</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"डिफल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"एनालग"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"आफ्नो डिभाइस अनलक गरी जारी राख्नुहोस्"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-nl/strings.xml b/packages/SystemUI/res-keyguard/values-nl/strings.xml
index 21c9b13..e97fde4 100644
--- a/packages/SystemUI/res-keyguard/values-nl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-nl/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ongeldige kaart."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Opgeladen"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Draadloos opladen"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Opladen"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Oplaaddock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Opladen"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Snel opladen"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Langzaam opladen"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Opladen geoptimaliseerd om de batterij te beschermen"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Opladen tijdelijk beperkt"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Druk op Menu om te ontgrendelen."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Netwerk vergrendeld"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Geen simkaart"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Voeg een simkaart toe."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"De simkaart ontbreekt of kan niet worden gelezen. Voeg een simkaart toe."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Onbruikbare simkaart."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Je simkaart is permanent gedeactiveerd.\n Neem contact op met je mobiele serviceprovider voor een nieuwe simkaart."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Simkaart is vergrendeld."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Simkaart is vergrendeld met pukcode."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Simkaart ontgrendelen…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Geen simkaart"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Plaats een simkaart."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"De simkaart ontbreekt of kan niet worden gelezen. Plaats een simkaart."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Onbruikbare simkaart."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Je simkaart is definitief uitgezet.\n Neem contact op met je mobiele serviceprovider voor een nieuwe simkaart."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Simkaart is vergrendeld."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Simkaart is vergrendeld met pukcode."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Simkaart ontgrendelen…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Gebied voor pincode"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Apparaatwachtwoord"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Gebied voor pincode van simkaart"</string>
@@ -61,34 +61,45 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Simkaart van <xliff:g id="CARRIER">%1$s</xliff:g> is nu uitgezet. Geef de pukcode op om door te gaan. Neem contact op met je provider voor meer informatie."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Geef de gewenste pincode op"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Gewenste pincode bevestigen"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Simkaart ontgrendelen…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Simkaart ontgrendelen…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Geef een pincode van vier tot acht cijfers op."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"De pukcode is minimaal acht cijfers lang."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Je hebt je pincode <xliff:g id="NUMBER_0">%1$d</xliff:g> keer onjuist getypt. \n\nProbeer het over <xliff:g id="NUMBER_1">%2$d</xliff:g> seconden opnieuw."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Je hebt je wachtwoord <xliff:g id="NUMBER_0">%1$d</xliff:g> keer onjuist getypt. \n\nProbeer het over <xliff:g id="NUMBER_1">%2$d</xliff:g> seconden opnieuw."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Je hebt je ontgrendelingspatroon <xliff:g id="NUMBER_0">%1$d</xliff:g> keer onjuist getekend. \n\nProbeer het over <xliff:g id="NUMBER_1">%2$d</xliff:g> seconden opnieuw."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Onjuiste pincode voor simkaart. Je moet nu contact opnemen met je provider om je apparaat te ontgrendelen."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Onjuiste pincode voor simkaart. Je hebt nog # poging over voordat je contact met je provider moet opnemen om je apparaat te laten ontgrendelen.}other{Onjuiste pincode voor simkaart. Je hebt nog # pogingen over. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Onjuiste pincode voor simkaart. Je hebt nog <xliff:g id="NUMBER_1">%d</xliff:g> pogingen over.</item>
+      <item quantity="one">Onjuiste pincode voor simkaart. Je hebt nog <xliff:g id="NUMBER_0">%d</xliff:g> poging over voordat je contact met je provider moet opnemen om je apparaat te ontgrendelen.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Simkaart is onbruikbaar. Neem contact op met je provider."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Onjuiste pukcode voor simkaart. Je hebt nog # poging over voordat de simkaart definitief onbruikbaar wordt.}other{Onjuiste pukcode voor simkaart. Je hebt nog # pogingen over voordat de simkaart definitief onbruikbaar wordt.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Onjuiste pukcode voor simkaart. Je hebt nog <xliff:g id="NUMBER_1">%d</xliff:g> pogingen over voordat de simkaart definitief onbruikbaar wordt.</item>
+      <item quantity="one">Onjuiste pukcode voor simkaart. Je hebt nog <xliff:g id="NUMBER_0">%d</xliff:g> poging over voordat de simkaart definitief onbruikbaar wordt.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Bewerking met pincode voor simkaart is mislukt."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Bewerking met pukcode voor simkaart is mislukt."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Invoermethode wijzigen"</string>
-    <string name="airplane_mode" msgid="2528005343938497866">"Vliegtuig­modus"</string>
+    <string name="airplane_mode" msgid="2528005343938497866">"Vliegtuigmodus"</string>
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon vereist nadat het apparaat opnieuw is opgestart"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pincode vereist nadat het apparaat opnieuw is opgestart"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wachtwoord vereist nadat het apparaat opnieuw is opgestart"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Gebruik in plaats daarvan het patroon voor extra beveiliging"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Gebruik in plaats daarvan de pincode voor extra beveiliging"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Gebruik in plaats daarvan het wachtwoord voor extra beveiliging"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon vereist voor extra beveiliging"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pincode vereist voor extra beveiliging"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wachtwoord vereist voor extra beveiliging"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Apparaat vergrendeld door beheerder"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Apparaat is handmatig vergrendeld"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Niet herkend"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Zet cameratoegang aan in Instellingen om Ontgrendelen via gezichtsherkenning te gebruiken"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Geef de pincode van de simkaart op. Je hebt nog # poging over voordat je contact met je provider moet opnemen om het apparaat te laten ontgrendelen.}other{Geef de pincode van de simkaart op. Je hebt nog # pogingen.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{De simkaart is nu gedeactiveerd. Geef de pukcode op om door te gaan. Je hebt nog # poging over voordat de simkaart definitief onbruikbaar wordt. Neem contact op met je provider voor meer informatie.}other{De simkaart is nu gedeactiveerd. Geef de pukcode op om door te gaan. Je hebt nog # pogingen over voordat de simkaart definitief onbruikbaar wordt. Neem contact op met je provider voor meer informatie.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Als je Ontgrendelen via gezichtsherkenning wilt gebruiken, zet je "<b>"Cameratoegang"</b>" aan via Instellingen &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Geef de pincode van de simkaart op. Je hebt nog <xliff:g id="NUMBER_1">%d</xliff:g> pogingen over.</item>
+      <item quantity="one">Geef de pincode van de simkaart op. Je hebt nog <xliff:g id="NUMBER_0">%d</xliff:g> poging over voordat je contact met je provider moet opnemen om het apparaat te ontgrendelen.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">De simkaart is nu uitgezet. Geef de pukcode op om door te gaan. Je hebt nog <xliff:g id="_NUMBER_1">%d</xliff:g> pogingen over voordat de simkaart definitief onbruikbaar wordt. Neem contact op met je provider voor meer informatie.</item>
+      <item quantity="one">De simkaart is nu uitgezet. Geef de pukcode op om door te gaan. Je hebt nog <xliff:g id="_NUMBER_0">%d</xliff:g> poging over voordat de simkaart definitief onbruikbaar wordt. Neem contact op met je provider voor meer informatie.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Standaard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bel"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ontgrendel je apparaat om door te gaan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-or/strings.xml b/packages/SystemUI/res-keyguard/values-or/strings.xml
index d8ca55c..edf5d99 100644
--- a/packages/SystemUI/res-keyguard/values-or/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-or/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ଅମାନ୍ୟ କାର୍ଡ।"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ଚାର୍ଜ ହୋଇଗଲା"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"ୱାୟାର୍‍ଲେସ୍‍ଭାବରେ <xliff:g id="PERCENTAGE">%s</xliff:g> • ଚାର୍ଜ ହୋଇଛି"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ଚାର୍ଜ ହେଉଛି"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ଡକରୁ ଚାର୍ଜ ହେଉଛି"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ଚାର୍ଜ ହେଉଛି"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ଦ୍ରୁତ ଭାବେ ଚାର୍ଜ ହେଉଛି"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ଧୀରେ ଚାର୍ଜ ହେଉଛି"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ବେଟେରୀକୁ ସୁରକ୍ଷିତ ରଖିବା ପାଇଁ ଚାର୍ଜିଂକୁ ଅପ୍ଟିମାଇଜ କରାଯାଇଛି"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ଚାର୍ଜିଂ ଅସ୍ଥାୟୀ ଭାବେ ସୀମିତ କରାଯାଇଛି"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"ଅନଲକ୍‌ କରିବା ପାଇଁ ମେନୁକୁ ଦବାନ୍ତୁ।"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ନେଟୱର୍କକୁ ଲକ୍‌ କରାଯାଇଛି"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"କୌଣସି SIM ନାହିଁ"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"ଏକ SIM ଯୋଗ କରନ୍ତୁ।"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM ଉପଲବ୍ଧ ନାହିଁ କିମ୍ବା ପଢ଼ିପାରିବା ଯୋଗ୍ୟ ନୁହେଁ। ଏକ SIM ଯୋଗ କରନ୍ତୁ।"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ବ୍ୟବହାର ଅଯୋଗ୍ୟ ଥିବା SIM।"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ଆପଣଙ୍କ SIMକୁ ସ୍ଥାୟୀ ଭାବରେ ନିଷ୍କ୍ରିୟ କରାଯାଇଛି।\n ଅନ୍ୟ ଏକ SIM ପାଇଁ ଆପଣଙ୍କ ୱେୟାରଲେସ ସେବା ପ୍ରଦାନକାରୀଙ୍କ ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIMକୁ ଲକ କରାଯାଇଛି।"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIMକୁ PUK-ଲକ କରାଯାଇଛି।"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIMକୁ ଅନଲକ କରାଯାଉଛି…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"କୌଣସି SIM କାର୍ଡ ନାହିଁ"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ଗୋଟିଏ SIM କାର୍ଡ ଭର୍ତ୍ତି କରନ୍ତୁ।"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM କାର୍ଡ ନାହିଁ କିମ୍ବା ଖରାପ ଅଛି। SIM କାର୍ଡ ଭର୍ତ୍ତି କରନ୍ତୁ।"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM କାର୍ଡଟିକୁ ବ୍ୟବହାର କରାଯାଇପାରିବ ନାହିଁ।"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ଆପଣଙ୍କ SIM କାର୍ଡକୁ ସ୍ଥାୟୀ ରୂପେ ଅକ୍ଷମ କରିଦିଆଯାଇଛି।\n ଅନ୍ୟ SIM କାର୍ଡ ପାଇଁ ଆପଣଙ୍କ ୱାୟରଲେସ୍‍ ସେବା ପ୍ରଦାତାଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM କାର୍ଡ ଲକ୍‍ ହୋଇଯାଇଛି।"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM କାର୍ଡଟି PUK ଲକ୍‍ ହୋଇଯାଇଛି।"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM କାର୍ଡ ଅନଲକ୍‍ କରାଯାଉଛି…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN ଅଞ୍ଚଳ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ଡିଭାଇସ୍ ପାସ୍‍ୱର୍ଡ"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN ଅଞ୍ଚଳ"</string>
@@ -57,20 +57,26 @@
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"SIMର PIN ଲେଖନ୍ତୁ।"</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" ପାଇଁ SIMର PIN ଲେଖନ୍ତୁ।"</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> ବିନା ମୋବାଇଲ୍ ସେବାରେ ଡିଭାଇସ୍‌କୁ ବ୍ୟବହାର କରିବା ପାଇଁ eSIMକୁ ଅକ୍ଷମ କରନ୍ତୁ।"</string>
-    <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"SIM ବର୍ତ୍ତମାନ ଅକ୍ଷମ ଅଟେ। ଜାରି ରଖିବାକୁ PUK କୋଡ ଲେଖନ୍ତୁ। ବିବରଣୀ ପାଇଁ ନିଜ କ୍ଯାରିଅରଙ୍କ ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।"</string>
-    <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" ବର୍ତ୍ତମାନ ଅକ୍ଷମ କରାଯାଇଛି। ଜାରିରଖିବାକୁ PUK କୋଡ ଲେଖନ୍ତୁ। ବିବରଣୀ ପାଇଁ କ୍ଯାରିଅରଙ୍କ ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।"</string>
+    <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"SIM ବର୍ତ୍ତମାନ ଅକ୍ଷମ ଅଟେ। ଜାରି ରଖିବାକୁ PUK କୋଡ୍‍ ଏଣ୍ଟର୍ କରନ୍ତୁ। ବିବରଣୀ ପାଇଁ ନିଜ କେରିଅର୍‌ଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।"</string>
+    <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" ବର୍ତ୍ତମାନ ଅକ୍ଷମ କରାଯାଇଛି। ଜାରିରଖିବାକୁ PUK କୋଡ୍‍ ଲେଖନ୍ତୁ। ବିବରଣୀ ପାଇଁ କେରିଅରଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"ନିଜ ଇଚ୍ଛାର PIN କୋଡ୍‍ ଲେଖନ୍ତୁ"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"ନିଜ ଇଚ୍ଛାର PIN କୋଡ୍‍ ନିଶ୍ଚିତ କରନ୍ତୁ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIMକୁ ଅନଲକ କରାଯାଉଛି…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM କାର୍ଡ ଅନଲକ୍‍ କରାଯାଉଛି…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 ରୁ 8 ନମ୍ବର ବିଶିଷ୍ଟ ଏକ PIN ଟାଇପ୍ କରନ୍ତୁ।"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK କୋଡ୍‍‍ରେ 8ଟି କିମ୍ବା ଅଧିକ ନମ୍ବର ରହିଥାଏ।"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"ଆପଣଙ୍କ PIN ଆପଣ <xliff:g id="NUMBER_0">%1$d</xliff:g>ଥର ଭୁଲ ଭାବେ ଟାଇପ୍‍ କରିଛନ୍ତି। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ସେକେଣ୍ଡ ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"ଆପଣଙ୍କ ପାସ୍‌ୱର୍ଡକୁ ଆପଣ <xliff:g id="NUMBER_0">%1$d</xliff:g> ଥର ଭୁଲ ଭାବେ ଟାଇପ୍ କରିଛନ୍ତି। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ସେକେଣ୍ଡ ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"ଆପଣଙ୍କ ଲକ୍‍ ଖୋଲିବା ପାଟର୍ନକୁ ଆପଣ <xliff:g id="NUMBER_0">%1$d</xliff:g>ଥର ଭୁଲ ଭାବେ ଅଙ୍କନ କରିଛନ୍ତି। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ସେକେଣ୍ଡ ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।"</string>
-    <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ଭୁଲ SIM PIN କୋଡ, ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରିବା ପାଇଁ ଏବେ ହିଁ ନିଜ କ୍ଯାରିଅରଙ୍କ ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{ଭୁଲ SIM PIN କୋଡ, ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରିବା ପାଇଁ ଆପଣ ଆପଣଙ୍କର କ୍ୟାରିଅର ସହ ନିଶ୍ଚିତରୂପେ କଣ୍ଟାକ୍ଟ କରିବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି।}other{ଭୁଲ SIM PIN କୋଡ, ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି।}}"</string>
-    <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM ବ୍ୟବହାର କରାଯାଇପାରିବ ନାହିଁ। ନିଜ କ୍ଯାରିଅରଙ୍କ ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{ଭୁଲ SIM PUK କୋଡ, SIM ସ୍ଥାୟୀ ଭାବେ ବ୍ୟବହାର ଅଯୋଗ୍ୟ ହେବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି।}other{ଭୁଲ SIM PUK କୋଡ, SIM ସ୍ଥାୟୀ ଭାବେ ବ୍ୟବହାର ଅଯୋଗ୍ୟ ହେବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି।}}"</string>
+    <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ଭୁଲ SIM PIN କୋଡ୍‌, ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ୍‌ କରିବା ପାଇଁ ଏବେ ହିଁ ନିଜ କେରିଅର୍‌ଙ୍କ ସହ ସମ୍ପର୍କ କରନ୍ତୁ।"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">ଭୁଲ SIM PIN କୋଡ୍, ଆପଣଙ୍କର ଆଉ <xliff:g id="NUMBER_1">%d</xliff:g>ଟି ପ୍ରୟାସ ବାକି ରହିଛି।</item>
+      <item quantity="one">ଭୁଲ SIM PIN କୋଡ୍, ଡିଭାଇସ୍‍ ଅନଲକ୍‍ କରିବା ପାଇଁ କେରିଅରଙ୍କ ସହିତ ଯୋଗାଯୋଗ କରିବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ ଆଉ <xliff:g id="NUMBER_0">%d</xliff:g>ଟି ପ୍ରୟାସ ବାକି ରହିଛି।</item>
+    </plurals>
+    <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM ବ୍ୟବହାର କରାଯାଇପାରିବ ନାହିଁ। ନିଜ କେରିଅର୍‌ଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">ଭୁଲ SIM PUK କୋଡ୍‍, SIMଟି ଆଉ <xliff:g id="NUMBER_1">%d</xliff:g>ଟି ପ୍ରୟାସ ପରେ ସ୍ଥାୟୀ ଭାବେ ବ୍ୟବହାର କରାଯାଇରିବ ନାହିଁ।</item>
+      <item quantity="one">ଭୁଲ SIM PUK କୋଡ୍‍, SIMଟି ଆଉ <xliff:g id="NUMBER_0">%d</xliff:g>ଟି ପ୍ରୟାସ ପରେ ସ୍ଥାୟୀ ଭାବେ ବ୍ୟବହାର କରାଯାଇରିବ ନାହିଁ।</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN କାମ ବିଫଳ ହେଲା!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUKର କାମ ବିଫଳ ହେଲା!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ଇନପୁଟ୍‌ ପଦ୍ଧତି ବଦଳାନ୍ତୁ"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାଟର୍ନ ଆବଶ୍ୟକ ଅଟେ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବାପରେ ପାସ୍‌ୱର୍ଡ ଆବଶ୍ୟକ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାସୱର୍ଡ ଆବଶ୍ୟକ ଅଟେ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାଟର୍ନ ବ୍ୟବହାର କରନ୍ତୁ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ PIN ବ୍ୟବହାର କରନ୍ତୁ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାସୱାର୍ଡ ବ୍ୟବହାର କରନ୍ତୁ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାଟର୍ନ ଆବଶ୍ୟକ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ PIN ଆବଶ୍ୟକ ଅଟେ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାସ୍‌ୱର୍ଡ ଆବଶ୍ୟକ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ଡିଭାଇସ୍‍ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଲକ୍‍ କରାଯାଇଛି"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ଡିଭାଇସ୍‍ ମାନୁଆଲ ଭାବେ ଲକ୍‍ କରାଗଲା"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ଚିହ୍ନଟ ହେଲାନାହିଁ"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ଫେସ ଅନଲକର ବ୍ୟବହାର ପାଇଁ ସେଟିଂସରେ କ୍ୟାମେରାକୁ ଆକ୍ସେସ ଦିଅ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PIN ଲେଖନ୍ତୁ। ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରିବା ପାଇଁ ଆପଣ ଆପଣଙ୍କର କ୍ୟାରିଅର ସହ ନିଶ୍ଚିତରୂପେ କଣ୍ଟାକ୍ଟ କରିବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି।}other{SIM PIN ଲେଖନ୍ତୁ। ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି।}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIMକୁ ବର୍ତ୍ତମାନ ଅକ୍ଷମ କରାଯାଇଛି। ଜାରି ରଖିବାକୁ PUK କୋଡ ଲେଖନ୍ତୁ। SIM ସ୍ଥାୟୀ ଭାବେ ବ୍ୟବହାର ଅଯୋଗ୍ୟ ହେବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି। ବିବରଣୀ ପାଇଁ କ୍ୟାରିଅର ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।}other{SIMକୁ ବର୍ତ୍ତମାନ ଅକ୍ଷମ କରାଯାଇଛି। ଜାରି ରଖିବାକୁ PUK କୋଡ ଲେଖନ୍ତୁ। SIM ସ୍ଥାୟୀ ଭାବେ ବ୍ୟବହାର ଅଯୋଗ୍ୟ ହେବା ପୂର୍ବରୁ ଆପଣଙ୍କ ପାଖରେ #ଟି ପ୍ରଚେଷ୍ଟା ବାକି ଅଛି। ବିବରଣୀ ପାଇଁ କ୍ୟାରିଅର ସହ କଣ୍ଟାକ୍ଟ କରନ୍ତୁ।}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ଫେସ ଅନଲକ ବ୍ୟବହାର କରିବା ପାଇଁ, ସେଟିଂସ ଏବଂ ଗୋପନୀୟତାରେ "<b>"କ୍ୟାମେରା ଆକ୍ସେସ"</b>"କୁ ଚାଲୁ କରନ୍ତୁ"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN ପ୍ରବେଶ କରନ୍ତୁ। ଆପଣଙ୍କ ପାଇଁ <xliff:g id="NUMBER_1">%d</xliff:g>ଟି ପ୍ରୟାସ ବଳକା ଅଛି।</item>
+      <item quantity="one">SIM PIN ପ୍ରବେଶ କରନ୍ତୁ। ଆପଣଙ୍କ ଡିଭାଇସ୍‍କୁ ଅନଲକ୍ କରିବା ପାଇଁ ପାଖରେ ବଳକା ଥିବା <xliff:g id="NUMBER_0">%d</xliff:g>ଟି ପ୍ରୟାସର ବ୍ୟବହାର କରିବା ପୂର୍ବରୁ ନିଜର କେରିଅର୍‍ଙ୍କୁ ସମ୍ପର୍କ କରନ୍ତୁ।</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM କାର୍ଡକୁ ବର୍ତ୍ତମାନ ଅକ୍ଷମ କରିଦିଆଯାଇଛି। ଜାରି ରଖିବାକୁ PUK କୋଡ୍‍ ଲେଖନ୍ତୁ। ଆଉ <xliff:g id="_NUMBER_1">%d</xliff:g> ଥର ଭୁଲ କୋଡ୍‍ ଲେଖିବା ପରେ SIM କାର୍ଡ ସ୍ଥାୟୀ ଭାବେ ଅନୁପଯୋଗୀ ହୋଇଯିବ। ବିବରଣୀ ପାଇଁ କେରିଅର୍‌ର ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।</item>
+      <item quantity="one">SIM କାର୍ଡକୁ ବର୍ତ୍ତମାନ ଅକ୍ଷମ କରିଦିଆଯାଇଛି। ଜାରି ରଖିବାକୁ PUK କୋଡ୍‍ ଲେଖନ୍ତୁ। ଆଉ <xliff:g id="_NUMBER_0">%d</xliff:g> ଥର ଭୁଲ କୋଡ୍‍ ଲେଖିବା ପରେ SIM କାର୍ଡ ସ୍ଥାୟୀ ଭାବେ ଅନୁପଯୋଗୀ ହୋଇଯିବ। ବିବରଣୀ ପାଇଁ କେରିଅର୍‌ର ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ଡିଫଲ୍ଟ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ବବଲ୍"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ଆନାଲଗ୍"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ଜାରି ରଖିବା ପାଇଁ ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରନ୍ତୁ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pa/strings.xml b/packages/SystemUI/res-keyguard/values-pa/strings.xml
index 5bcfd05..858682c 100644
--- a/packages/SystemUI/res-keyguard/values-pa/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pa/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"ਅਵੈਧ ਕਾਰਡ।"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ਚਾਰਜ ਹੋ ਗਿਆ"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਬਿਨਾਂ ਤਾਰ ਤੋਂ ਚਾਰਜ ਹੋ ਰਿਹਾ ਹੈ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਚਾਰਜ ਹੋ ਰਿਹਾ ਹੈ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਡੌਕ ਚਾਰਜ ਹੋ ਰਿਹਾ ਹੈ"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਚਾਰਜ ਹੋ ਰਿਹਾ ਹੈ"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਤੇਜ਼ੀ ਨਾਲ ਚਾਰਜ ਹੋ ਰਿਹਾ ਹੈ"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਹੌਲੀ-ਹੌਲੀ ਚਾਰਜ ਹੋ ਰਿਹਾ ਹੈ"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਬੈਟਰੀ ਦੀ ਸੁਰੱਖਿਆ ਲਈ ਚਾਰਜਿੰਗ ਨੂੰ ਸੁਯੋਗ ਬਣਾਇਆ ਗਿਆ"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ਚਾਰਜਿੰਗ ਕੁਝ ਸਮੇਂ ਲਈ ਰੋਕੀ ਗਈ"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"ਅਣਲਾਕ ਕਰਨ ਲਈ \"ਮੀਨੂ\" ਦਬਾਓ।"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ਨੈੱਟਵਰਕ  ਲਾਕ  ਕੀਤਾ ਗਿਆ"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"ਕੋਈ ਸਿਮ ਨਹੀਂ ਹੈ"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"ਸਿਮ ਸ਼ਾਮਲ ਕਰੋ।"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"ਸਿਮ ਮੌਜੂਦ ਨਹੀਂ ਹੈ ਜਾਂ ਪੜ੍ਹਨਯੋਗ ਨਹੀਂ ਹੈ। ਸਿਮ ਸ਼ਾਮਲ ਕਰੋ।"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"ਬੇਕਾਰ ਸਿਮ।"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ਤੁਹਾਡੇ ਸਿਮ ਨੂੰ ਪੱਕੇ ਤੌਰ \'ਤੇ ਅਕਿਰਿਆਸ਼ੀਲ ਕੀਤਾ ਗਿਆ ਹੈ।\n ਦੂਜੇ ਸਿਮ ਲਈ ਆਪਣੇ ਵਾਇਰਲੈੱਸ ਸੇਵਾ ਪ੍ਰਦਾਨਕ ਨੂੰ ਸੰਪਰਕ ਕਰੋ।"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"ਸਿਮ ਲਾਕ ਹੈ।"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"ਸਿਮ PUK-ਲਾਕ ਹੈ।"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"ਸਿਮ ਨੂੰ ਅਣਲਾਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"ਕੋਈ ਸਿਮ ਕਾਰਡ ਨਹੀਂ"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ਇੱਕ SIM ਕਾਰਡ ਪਾਓ।"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM ਕਾਰਡ ਮੌਜੂਦ ਨਹੀਂ ਜਾਂ ਪੜ੍ਹਨਯੋਗ ਨਹੀਂ ਹੈ। ਇੱਕ SIM ਕਾਰਡ ਪਾਓ।"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"ਨਾ-ਵਰਤਣਯੋਗ SIM ਕਾਰਡ।"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ਤੁਹਾਡਾ ਸਿਮ ਕਾਰਡ ਸਥਾਈ ਤੌਰ \'ਤੇ ਅਯੋਗ ਬਣਾ ਦਿੱਤਾ ਗਿਆ ਹੈ।\n ਇੱਕ ਹੋਰ ਸਿਮ ਕਾਰਡ ਲਈ ਆਪਣੇ ਵਾਇਰਲੈੱਸ ਸੇਵਾ ਪ੍ਰਦਾਨਕ ਨੂੰ ਸੰਪਰਕ ਕਰੋ।"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM ਕਾਰਡ  ਲਾਕ  ਕੀਤਾ ਹੋਇਆ ਹੈ।"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM ਕਾਰਡ PUK- ਲਾਕ  ਕੀਤਾ ਹੋਇਆ ਹੈ।"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM ਕਾਰਡ ਨੂੰ ਅਣਲਾਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"ਪਿੰਨ ਖੇਤਰ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"ਡੀਵਾਈਸ ਦਾ ਪਾਸਵਰਡ"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"ਸਿਮ ਪਿੰਨ ਖੇਤਰ"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ਸਿਮ \"<xliff:g id="CARRIER">%1$s</xliff:g>\" ਹੁਣ ਬੰਦ ਕੀਤਾ ਗਿਆ ਹੈ। ਜਾਰੀ ਰੱਖਣ ਲਈ PUK ਕੋਡ ਦਾਖਲ ਕਰੋ। ਵੇਰਵਿਆਂ ਲਈ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"ਇੱਛਤ ਪਿੰਨ ਕੋਡ ਦਾਖਲ ਕਰੋ"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"ਇੱਛਤ ਪਿੰਨ ਕੋਡ ਦੀ ਪੁਸ਼ਟੀ ਕਰੋ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"ਸਿਮ ਨੂੰ ਅਣਲਾਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM ਕਾਰਡ ਨੂੰ ਅਣਲਾਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"ਕੋਈ ਪਿੰਨ ਟਾਈਪ ਕਰੋ ਜੋ 4 ਤੋਂ 8 ਨੰਬਰਾਂ ਦਾ ਹੋਵੇ।"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK ਕੋਡ 8 ਜਾਂ ਵੱਧ ਨੰਬਰਾਂ ਦਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"ਤੁਸੀਂ ਆਪਣਾ ਪਿੰਨ <xliff:g id="NUMBER_0">%1$d</xliff:g> ਵਾਰ ਗਲਤ ਢੰਗ ਨਾਲ ਟਾਈਪ ਕੀਤਾ ਹੈ। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ਸਕਿੰਟਾਂ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"ਤੁਸੀਂ <xliff:g id="NUMBER_0">%1$d</xliff:g> ਵਾਰ ਆਪਣਾ ਪਾਸਵਰਡ ਗਲਤ ਢੰਗ ਨਾਲ ਟਾਈਪ ਕੀਤਾ ਹੈ।\n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ਸਕਿੰਟਾਂ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"ਤੁਸੀਂ <xliff:g id="NUMBER_0">%1$d</xliff:g> ਵਾਰ ਆਪਣਾ ਅਣਲਾਕ ਪੈਟਰਨ ਗਲਤ ਢੰਗ ਨਾਲ ਉਲੀਕਿਆ ਹੈ। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ਸਕਿੰਟਾਂ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ਗਲਤ ਸਿਮ ਪਿੰਨ ਕੋਡ, ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਅਣਲਾਕ ਕਰਨ ਲਈ ਹੁਣ ਤੁਹਾਨੂੰ ਲਾਜ਼ਮੀ ਤੌਰ \'ਤੇ ਆਪਣੇ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ।"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{ਗਲਤ ਸਿਮ ਪਿੰਨ ਕੋਡ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ ਉਸ ਤੋਂ ਬਾਅਦ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਅਣਲਾਕ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਆਪਣੇ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰਨਾ ਪਵੇਗਾ।}one{ਗਲਤ ਸਿਮ ਪਿੰਨ ਕੋਡ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ। }other{ਗਲਤ ਸਿਮ ਪਿੰਨ ਕੋਡ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ। }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">ਗਲਤ ਸਿਮ ਪਿੰਨ ਕੋਡ, ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।</item>
+      <item quantity="other">ਗਲਤ ਸਿਮ ਪਿੰਨ ਕੋਡ, ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM ਨਾ-ਵਰਤਣਯੋਗ ਹੈ। ਆਪਣੇ ਕੈਰੀਅਰ ਨੂੰ ਸੰਪਰਕ ਕਰੋ।"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{ਗਲਤ ਸਿਮ PUK ਕੋਡ, ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਬਿਲਕੁਲ ਬੇਕਾਰ ਹੋ ਜਾਵੇ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।}one{ਗਲਤ ਸਿਮ PUK ਕੋਡ, ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਬਿਲਕੁਲ ਬੇਕਾਰ ਹੋ ਜਾਵੇ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।}other{ਗਲਤ ਸਿਮ PUK ਕੋਡ, ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਬਿਲਕੁਲ ਬੇਕਾਰ ਹੋ ਜਾਵੇ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ।}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">ਗਲਤ ਸਿਮ PUK ਕੋਡ, ਇਸਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਸਥਾਈ ਤੌਰ \'ਤੇ ਵਰਤਣਯੋਗ ਨਾ ਰਹੇ, ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।</item>
+      <item quantity="other">ਗਲਤ ਸਿਮ PUK ਕੋਡ, ਇਸਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਸਥਾਈ ਤੌਰ \'ਤੇ ਵਰਤਣਯੋਗ ਨਾ ਰਹੇ, ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ।</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"ਸਿਮ ਪਿੰਨ ਕਾਰਵਾਈ ਅਸਫਲ ਰਹੀ!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK ਕਾਰਵਾਈ ਅਸਫਲ ਰਹੀ!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ਇਨਪੁੱਟ ਵਿਧੀ ਸਵਿੱਚ ਕਰੋ"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ, ਇਸਦੀ ਬਜਾਏ ਪੈਟਰਨ ਵਰਤੋ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ, ਇਸਦੀ ਬਜਾਏ ਪਿੰਨ ਵਰਤੋ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ, ਇਸਦੀ ਬਜਾਏ ਪਾਸਵਰਡ ਵਰਤੋ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਡੀਵਾਈਸ ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ਡੀਵਾਈਸ ਨੂੰ ਹੱਥੀਂ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ਫ਼ੇਸ ਅਣਲਾਕ ਵਰਤਣ ਲਈ, ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਕੈਮਰਾ ਪਹੁੰਚ ਚਾਲੂ ਕਰੋ"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{ਸਿਮ ਪਿੰਨ ਦਾਖਲ ਕਰੋ। ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ, ਉਸ ਤੋਂ ਬਾਅਦ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਅਣਲਾਕ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਆਪਣੇ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰਨਾ ਪਵੇਗਾ।}one{ਸਿਮ ਪਿੰਨ ਦਾਖਲ ਕਰੋ। ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।}other{ਸਿਮ ਪਿੰਨ ਦਾਖਲ ਕਰੋ। ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ।}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ਸਿਮ ਹੁਣ ਬੰਦ ਹੋ ਗਿਆ ਹੈ। ਜਾਰੀ ਰੱਖਣ ਲਈ PUK ਕੋਡ ਦਾਖਲ ਕਰੋ। ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਬਿਲਕੁਲ ਬੇਕਾਰ ਹੋ ਜਾਵੇ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ। ਵੇਰਵਿਆਂ ਲਈ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।}one{ਸਿਮ ਹੁਣ ਬੰਦ ਹੋ ਗਿਆ ਹੈ। ਜਾਰੀ ਰੱਖਣ ਲਈ PUK ਕੋਡ ਦਾਖਲ ਕਰੋ। ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਬਿਲਕੁਲ ਬੇਕਾਰ ਹੋ ਜਾਵੇ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ। ਵੇਰਵਿਆਂ ਲਈ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।}other{ਸਿਮ ਹੁਣ ਬੰਦ ਹੋ ਗਿਆ ਹੈ। ਜਾਰੀ ਰੱਖਣ ਲਈ PUK ਕੋਡ ਦਾਖਲ ਕਰੋ। ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਸਿਮ ਬਿਲਕੁਲ ਬੇਕਾਰ ਹੋ ਜਾਵੇ, ਤੁਹਾਡੇ ਕੋਲ # ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ। ਵੇਰਵਿਆਂ ਲਈ ਕੈਰੀਅਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ਫ਼ੇਸ ਅਣਲਾਕ ਨੂੰ ਵਰਤਣ ਲਈ, ਸੈਟਿੰਗਾਂ &gt; ਪਰਦੇਦਾਰੀ ਵਿੱਚ ਜਾ ਕੇ "<b>"ਕੈਮਰਾ ਪਹੁੰਚ"</b>" ਨੂੰ ਚਾਲੂ ਕਰੋ"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">ਸਿਮ ਪਿੰਨ ਦਾਖਲ ਕਰੋ। ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ।</item>
+      <item quantity="other">ਸਿਮ ਪਿੰਨ ਦਾਖਲ ਕਰੋ। ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ।</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">ਸਿਮ ਹੁਣ ਬੰਦ ਹੋ ਗਿਆ ਹੈ। ਜਾਰੀ ਰੱਖਣ ਲਈ PUK ਕੋਡ ਦਾਖਲ ਕਰੋ। ਸਿਮ ਦੇ ਪੱਕੇ ਤੌਰ \'ਤੇ ਬੇਕਾਰ ਹੋ ਜਾਣ ਤੋਂ ਪਹਿਲਾਂ ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="_NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ ਬਾਕੀ ਹੈ। ਵੇਰਵਿਆਂ ਲਈ ਕੈਰੀਅਰ ਨੂੰ ਸੰਪਰਕ ਕਰੋ।</item>
+      <item quantity="other">ਸਿਮ ਹੁਣ ਬੰਦ ਹੋ ਗਿਆ ਹੈ। ਜਾਰੀ ਰੱਖਣ ਲਈ PUK ਕੋਡ ਦਾਖਲ ਕਰੋ। ਸਿਮ ਦੇ ਪੱਕੇ ਤੌਰ \'ਤੇ ਬੇਕਾਰ ਹੋ ਜਾਣ ਤੋਂ ਪਹਿਲਾਂ ਤੁਹਾਡੇ ਕੋਲ <xliff:g id="_NUMBER_1">%d</xliff:g> ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ। ਵੇਰਵਿਆਂ ਲਈ ਕੈਰੀਅਰ ਨੂੰ ਸੰਪਰਕ ਕਰੋ।</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ਬੁਲਬੁਲਾ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ਐਨਾਲੌਗ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ਜਾਰੀ ਰੱਖਣ ਲਈ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਅਣਲਾਕ ਕਰੋ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pl/strings.xml b/packages/SystemUI/res-keyguard/values-pl/strings.xml
index 0226297..f1a53e0 100644
--- a/packages/SystemUI/res-keyguard/values-pl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pl/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Nieprawidłowa karta."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Naładowana"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ładowanie bezprzewodowe"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ładowanie"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ładowanie na stacji dokującej"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ładowanie"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Szybkie ładowanie"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Wolne ładowanie"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ładowanie zoptymalizowane w celu ochrony baterii"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ładowanie tymczasowo ograniczone"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Naciśnij Menu, aby odblokować."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Sieć zablokowana"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Brak karty SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Dodaj kartę SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Brak karty SIM lub nie można jej odczytać. Dodaj kartę SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Nie można użyć karty SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Karta SIM została trwale wyłączona.\n Skontaktuj się z dostawcą usług bezprzewodowych, aby uzyskać inną kartę SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Karta SIM jest zablokowana."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Karta SIM została zablokowana kodem PUK"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Odblokowuję kartę SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Brak karty SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Włóż kartę SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Brak karty SIM lub nie można jej odczytać. Włóż kartę SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Karta SIM jest zablokowana."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Karta SIM jest trwale wyłączona.\n Skontaktuj się z dostawcą usług bezprzewodowych, by otrzymać inną kartę SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Karta SIM jest zablokowana."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Karta SIM jest zablokowana kodem PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Odblokowuję kartę SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Miejsce na kod PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Hasło urządzenia"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Miejsce na kod PIN karty SIM"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Karta SIM „<xliff:g id="CARRIER">%1$s</xliff:g>” została wyłączona. Wpisz kod PUK, by przejść dalej. Skontaktuj się z operatorem, by uzyskać więcej informacji."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Podaj wybrany kod PIN"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potwierdź wybrany kod PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Odblokowuję kartę SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Odblokowuję kartę SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Wpisz kod PIN o długości od 4 do 8 cyfr."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Kod PUK musi mieć co najmniej 8 cyfr."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Po raz <xliff:g id="NUMBER_0">%1$d</xliff:g> wpisałeś nieprawidłowy kod PIN. \n\nSpróbuj ponownie za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Po raz <xliff:g id="NUMBER_0">%1$d</xliff:g> wpisałeś nieprawidłowe hasło. \n\nSpróbuj ponownie za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Po raz <xliff:g id="NUMBER_0">%1$d</xliff:g> nieprawidłowo narysowałeś wzór odblokowania. \n\nSpróbuj ponownie za <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Nieprawidłowy kod PIN karty SIM. Musisz teraz skontaktować się z operatorem, by odblokował Twoje urządzenie."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Nieprawidłowy kod PIN karty SIM. Masz jeszcze # próbę, zanim trzeba będzie skontaktować się z operatorem, aby odblokował Twoje urządzenie.}few{Nieprawidłowy kod PIN karty SIM. Masz jeszcze # próby. }many{Nieprawidłowy kod PIN karty SIM. Masz jeszcze # prób. }other{Nieprawidłowy kod PIN karty SIM. Masz jeszcze # próby. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="few">Nieprawidłowy kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> próby.</item>
+      <item quantity="many">Nieprawidłowy kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> prób.</item>
+      <item quantity="other">Nieprawidłowy kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> próby.</item>
+      <item quantity="one">Nieprawidłowy kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_0">%d</xliff:g> próbę, zanim będziesz musiał skontaktować się z operatorem, by odblokował Twoje urządzenie.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Karta SIM została trwale zablokowana. Skontaktuj się z operatorem."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Nieprawidłowy kod PUK karty SIM. Masz jeszcze # próbę, zanim karta SIM zostanie trwale zablokowana.}few{Nieprawidłowy kod PUK karty SIM. Masz jeszcze # próby, zanim karta SIM zostanie trwale zablokowana.}many{Nieprawidłowy kod PUK karty SIM. Masz jeszcze # prób, zanim karta SIM zostanie trwale zablokowana.}other{Nieprawidłowy kod PUK karty SIM. Masz jeszcze # próby, zanim karta SIM zostanie trwale zablokowana.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="few">Nieprawidłowy kod PUK karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> próby, zanim karta SIM zostanie trwale zablokowana.</item>
+      <item quantity="many">Nieprawidłowy kod PUK karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> prób, zanim karta SIM zostanie trwale zablokowana.</item>
+      <item quantity="other">Nieprawidłowy kod PUK karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> próby, zanim karta SIM zostanie trwale zablokowana.</item>
+      <item quantity="one">Nieprawidłowy kod PUK karty SIM. Masz jeszcze <xliff:g id="NUMBER_0">%d</xliff:g> próbę, zanim karta SIM zostanie trwale zablokowana.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operacja z kodem PIN karty SIM nie udała się."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operacja z kodem PUK karty SIM nie udała się."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Przełączanie metody wprowadzania"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po ponownym uruchomieniu urządzenia wymagany jest wzór"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po ponownym uruchomieniu urządzenia wymagany jest kod PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po ponownym uruchomieniu urządzenia wymagane jest hasło"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Ze względów bezpieczeństwa użyj wzoru"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Ze względów bezpieczeństwa użyj kodu PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Ze względów bezpieczeństwa użyj hasła"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Dla większego bezpieczeństwa musisz narysować wzór"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Dla większego bezpieczeństwa musisz podać kod PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Dla większego bezpieczeństwa musisz podać hasło"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Urządzenie zablokowane przez administratora"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Urządzenie zostało zablokowane ręcznie"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie rozpoznano"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Aby używać rozpoznawania twarzy, włącz w Ustawieniach dostęp do aparatu"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Wpisz kod PIN karty SIM. Masz jeszcze # próbę, zanim będzie trzeba skontaktować się z operatorem, aby odblokować to urządzenie.}few{Wpisz kod PIN karty SIM. Masz jeszcze # próby.}many{Wpisz kod PIN karty SIM. Masz jeszcze # prób.}other{Wpisz kod PIN karty SIM. Masz jeszcze # próby.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Karta SIM została wyłączona. Wpisz kod PUK, aby przejść dalej. Masz jeszcze # próbę, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.}few{Karta SIM została wyłączona. Wpisz kod PUK, aby przejść dalej. Masz jeszcze # próby, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.}many{Karta SIM została wyłączona. Wpisz kod PUK, aby przejść dalej. Masz jeszcze # prób, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.}other{Karta SIM została wyłączona. Wpisz kod PUK, aby przejść dalej. Masz jeszcze # próby, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Aby używać rozpoznawania twarzy, włącz "<b>"dostęp do aparatu"</b>" w Ustawieniach i prywatności"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="few">Wpisz kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> próby.</item>
+      <item quantity="many">Wpisz kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> prób.</item>
+      <item quantity="other">Wpisz kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_1">%d</xliff:g> próby.</item>
+      <item quantity="one">Wpisz kod PIN karty SIM. Masz jeszcze <xliff:g id="NUMBER_0">%d</xliff:g> próbę, zanim będzie trzeba skontaktować się z operatorem, by odblokować to urządzenie.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="few">Karta SIM została wyłączona. Wpisz kod PUK, by przejść dalej. Masz jeszcze <xliff:g id="_NUMBER_1">%d</xliff:g> próby, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.</item>
+      <item quantity="many">Karta SIM została wyłączona. Wpisz kod PUK, by przejść dalej. Masz jeszcze <xliff:g id="_NUMBER_1">%d</xliff:g> prób, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.</item>
+      <item quantity="other">Karta SIM została wyłączona. Wpisz kod PUK, by przejść dalej. Masz jeszcze <xliff:g id="_NUMBER_1">%d</xliff:g> próby, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.</item>
+      <item quantity="one">Karta SIM została wyłączona. Wpisz kod PUK, by przejść dalej. Masz jeszcze <xliff:g id="_NUMBER_0">%d</xliff:g> próbę, zanim karta SIM zostanie trwale zablokowana. Aby uzyskać szczegółowe informacje, skontaktuj się z operatorem.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Domyślna"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bąbelkowy"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogowy"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Odblokuj urządzenie, aby kontynuować"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
index f4967d1..aecc5f2 100644
--- a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Cartão inválido."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Carregado"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando sem fio"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando na base"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando rapidamente"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando lentamente"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregamento otimizado para proteger a bateria"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregamento temporariamente limitado"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pressione Menu para desbloquear."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rede bloqueada"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Sem chip"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Adicione um chip."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"O chip não foi inserido ou não pode ser lido. Adicione um chip."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Chip inutilizável."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Seu chip foi desativado permanentemente.\n Entre em contato com seu provedor de serviços sem fio para receber outro chip."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"O chip está bloqueado."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"O chip está bloqueado pela PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Desbloqueando chip…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Sem chip"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insira um chip."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"O chip não foi inserido ou não é possível lê-lo. Insira um chip."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Chip inutilizável."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"O chip foi desativado permanentemente.\nEntre em contato com seu provedor de serviços sem fio para receber outro chip."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"O chip está bloqueado."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"O chip está bloqueado pelo PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Desbloqueando o chip…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Área do PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Senha do dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Área do PIN do chip"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Padrão incorreto"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Senha incorreta"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"PIN incorreto"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Tente novamente em # segundo.}one{Tente novamente em # segundo.}many{Tente novamente em # segundos.}other{Tente novamente em # segundos.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Tente novamente em # segundo.}one{Tente novamente em # segundo.}other{Tente novamente em # segundos.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Informe o PIN do chip."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Informe o PIN do chip para \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Desative o eSIM para usar o dispositivo sem serviço móvel."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"O chip \"<xliff:g id="CARRIER">%1$s</xliff:g>\" foi desativado. Informe o código PUK para continuar. Entre em contato com a operadora para saber mais detalhes."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Digite o código PIN desejado"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirme o código PIN desejado"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Desbloqueando chip…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Desbloqueando o chip…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Digite um PIN com 4 a 8 números."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"O código PUK deve ter oito números ou mais."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Você digitou seu PIN incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente em <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Você digitou sua senha incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente em <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Você desenhou sua sequência de desbloqueio incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente em <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Código PIN do chip incorreto. Entre em contato com a operadora para desbloquear o dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Código PIN do chip incorreto. Você tem # tentativa restante. Se o código correto não for digitado, será necessário entrar em contato com a operadora para desbloquear o dispositivo.}one{Código PIN do chip incorreto. Você tem # tentativa restante. }many{Código PIN do chip incorreto. Você tem # de tentativas restantes. }other{Código PIN do chip incorreto. Você tem # tentativas restantes. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Código PIN do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>.</item>
+      <item quantity="other">Código PIN do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"O chip não pode ser utilizado. Entre em contato com a operadora."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Código PUK do chip incorreto. Você tem # tentativa restante. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}one{Código PUK do chip incorreto. Você tem # tentativa restante. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}many{Código PUK do chip incorreto. Você tem # de tentativas restantes. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}other{Código PUK do chip incorreto. Você tem # tentativas restantes. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Código PUK do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>. Caso o código correto não seja digitado, o chip se tornará permanentemente inutilizável.</item>
+      <item quantity="other">Código PUK do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>. Caso o código correto não seja digitado, o chip se tornará permanentemente inutilizável.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Falha na operação de PIN do chip."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Falha na operação de PUK do chip."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Alterar o método de entrada"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Para usar o Desbloqueio facial, ative o acesso à câmera nas Configurações"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Informe o PIN do chip. Você tem # tentativa restante antes de precisar entrar em contato com a operadora para desbloquear seu dispositivo.}one{Informe o PIN do chip. Você tem # tentativa restante.}many{Informe o PIN do chip. Você tem # de tentativas restantes.}other{Informe o PIN do chip. Você tem # tentativas restantes.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{O chip está desativado. Informe o código PUK para continuar. Você tem # tentativa restante antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}one{O chip está desativado. Informe o código PUK para continuar. Você tem # tentativa restante antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}many{O chip está desativado. Informe o código PUK para continuar. Você tem # de tentativas restantes antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}other{O chip está desativado. Informe o código PUK para continuar. Você tem # tentativas restantes antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para usar o Desbloqueio facial, ative o "<b>"acesso à câmera"</b>" em Configurações &gt; Privacidade"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Informe o PIN do chip. Você tem <xliff:g id="NUMBER_1">%d</xliff:g> tentativa restante.</item>
+      <item quantity="other">Informe o PIN do chip. Você tem <xliff:g id="NUMBER_1">%d</xliff:g> tentativas restantes.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">O chip agora está desativado. Informe o código PUK para continuar. Você tem <xliff:g id="_NUMBER_1">%d</xliff:g> tentativa restante antes de o chip se tornar permanentemente inutilizável. Entre em contato com a operadora para saber mais detalhes.</item>
+      <item quantity="other">O chip agora está desativado. Informe o código PUK para continuar. Você tem <xliff:g id="_NUMBER_1">%d</xliff:g> tentativas restantes antes de o chip se tornar permanentemente inutilizável. Entre em contato com a operadora para saber mais detalhes.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
index 5d4ddd4..a8d8f34 100644
--- a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Cartão inválido."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Carregada"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • A carregar sem fios"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • A carregar"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • A carregar na estação de ancoragem"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • A carregar…"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • A carregar rapidamente…"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • A carregar lentamente…"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregamento otimizado para proteger a bateria"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregamento limitado temporariamente"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Prima Menu para desbloquear."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rede bloqueada"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Sem SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Adicione um SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"O SIM está em falta ou não é legível. Adicione um SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM inutilizável."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"O SIM foi desativado permanentemente.\n Contacte o seu fornecedor de serviços de rede sem fios para obter outro SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"O SIM está bloqueado."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"O SIM está bloqueado com o PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"A desbloquear SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nenhum cartão SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insira um cartão SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"O cartão SIM está em falta ou não é legível. Insira um cartão SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Cartão SIM inutilizável."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"O cartão SIM foi desativado definitivamente.\n Contacte o seu fornecedor de serviços de rede sem fios para obter outro cartão SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"O cartão SIM está bloqueado."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"O cartão SIM está bloqueado pelo PUK"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"A desbloquear o cartão SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Área do PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Palavra-passe do dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Área do PIN do cartão SIM"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Padrão incorreto."</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Palavra-passe incorreta."</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"PIN incorreto"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Tente novamente dentro de # segundo.}many{Tente novamente dentro de # segundos.}other{Tente novamente dentro de # segundos.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Tente novamente dentro de # segundo.}other{Tente novamente dentro de # segundos.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Introduza o PIN do cartão SIM."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Introduza o PIN do cartão SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Desative o eSIM para utilizar o dispositivo sem serviço móvel."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"O cartão SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" está agora desativado. Introduza o código PUK para continuar. Contacte o operador para obter mais detalhes."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Introduza o código PIN pretendido"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirme o código PIN pretendido"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"A desbloquear SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"A desbloquear o cartão SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Introduza um PIN com 4 a 8 números."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"O código PUK deve ter 8 ou mais números."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Introduziu o PIN incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente dentro de <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Introduziu a palavra-passe incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente dentro de <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Desenhou a sua padrão de desbloqueio incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente dentro de <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Código PIN do cartão SIM incorreto. Tem de contactar o seu operador para desbloquear o dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Código PIN do SIM incorreto. Tem mais # tentativa antes de ser necessário contactar o operador para desbloquear o dispositivo.}many{Código PIN do SIM incorreto. Tem mais # tentativas. }other{Código PIN do SIM incorreto. Tem mais # tentativas. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Código PIN do cartão SIM incorreto. Tem mais <xliff:g id="NUMBER_1">%d</xliff:g> tentativas.</item>
+      <item quantity="one">Código PIN do cartão SIM incorreto. Tem mais <xliff:g id="NUMBER_0">%d</xliff:g> tentativa antes de precisar de contactar o seu operador para desbloquear o dispositivo.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Cartão SIM inutilizável. Contacte o seu operador."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Código PUK do SIM incorreto. Tem mais # tentativa antes de o SIM ficar permanentemente inutilizável.}many{Código PUK do SIM incorreto. Tem mais # tentativas antes de o SIM ficar permanentemente inutilizável.}other{Código PUK do SIM incorreto. Tem mais # tentativas antes de o SIM ficar permanentemente inutilizável.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Código PUK do cartão SIM incorreto. Tem mais <xliff:g id="NUMBER_1">%d</xliff:g> tentativas antes de o cartão SIM ficar permanentemente inutilizável.</item>
+      <item quantity="one">Código PUK do cartão SIM incorreto. Tem mais <xliff:g id="NUMBER_0">%d</xliff:g> tentativa antes de o cartão SIM ficar permanentemente inutilizável.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Falha ao introduzir o PIN do cartão SIM!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Falha ao introduzir o PUK do cartão SIM!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Alternar o método de introdução"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necessário um padrão após reiniciar o dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necessário um PIN após reiniciar o dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necessária uma palavra-passe após reiniciar o dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para uma segurança adicional, use antes o padrão"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para uma segurança adicional, use antes o PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para uma segurança adicional, use antes a palavra-passe"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Para segurança adicional, é necessário um padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Para segurança adicional, é necessário um PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Para segurança adicional, é necessária uma palavra-passe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo gestor"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido."</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Ative acesso à câmara nas Def. p/ usar Desbl. facial"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Introduza o PIN do SIM. Tem mais # tentativa antes de ser necessário contactar o operador para desbloquear o dispositivo.}many{Introduza o PIN do SIM. Tem mais # tentativas.}other{Introduza o PIN do SIM. Tem mais # tentativas.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{O SIM está desativado. Introduza o código PUK para continuar. Tem mais # tentativa antes de o SIM ficar permanentemente inutilizável. Contacte o operador para obter mais detalhes.}many{O SIM está desativado. Introduza o código PUK para continuar. Tem mais # tentativas antes de o SIM ficar permanentemente inutilizável. Contacte o operador para obter mais detalhes.}other{O SIM está desativado. Introduza o código PUK para continuar. Tem mais # tentativas antes de o SIM ficar permanentemente inutilizável. Contacte o operador para obter mais detalhes.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para utilizar o Desbloqueio facial, ative o "<b>"Acesso à câmara"</b>" em Definições &gt; Privacidade"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Introduza o PIN do cartão SIM. Tem mais <xliff:g id="NUMBER_1">%d</xliff:g> tentativas.</item>
+      <item quantity="one">Introduza o PIN do cartão SIM. Tem mais <xliff:g id="NUMBER_0">%d</xliff:g> tentativa antes de ser necessário contactar o operador para desbloquear o dispositivo.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">O SIM encontra-se desativado. Introduza o código PUK para continuar. Tem mais <xliff:g id="_NUMBER_1">%d</xliff:g> tentativas antes de o cartão SIM ficar permanentemente inutilizável. Contacte o operador para obter mais detalhes.</item>
+      <item quantity="one">O SIM encontra-se desativado. Introduza o código PUK para continuar. Tem mais <xliff:g id="_NUMBER_0">%d</xliff:g> tentativa antes de o cartão SIM ficar permanentemente inutilizável. Contacte o operador para obter mais detalhes.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predefinido"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balão"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt/strings.xml b/packages/SystemUI/res-keyguard/values-pt/strings.xml
index f4967d1..aecc5f2 100644
--- a/packages/SystemUI/res-keyguard/values-pt/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Cartão inválido."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Carregado"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando sem fio"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando na base"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando rapidamente"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregando lentamente"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregamento otimizado para proteger a bateria"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Carregamento temporariamente limitado"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pressione Menu para desbloquear."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rede bloqueada"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Sem chip"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Adicione um chip."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"O chip não foi inserido ou não pode ser lido. Adicione um chip."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Chip inutilizável."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Seu chip foi desativado permanentemente.\n Entre em contato com seu provedor de serviços sem fio para receber outro chip."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"O chip está bloqueado."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"O chip está bloqueado pela PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Desbloqueando chip…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Sem chip"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Insira um chip."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"O chip não foi inserido ou não é possível lê-lo. Insira um chip."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Chip inutilizável."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"O chip foi desativado permanentemente.\nEntre em contato com seu provedor de serviços sem fio para receber outro chip."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"O chip está bloqueado."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"O chip está bloqueado pelo PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Desbloqueando o chip…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Área do PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Senha do dispositivo"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Área do PIN do chip"</string>
@@ -53,7 +53,7 @@
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Padrão incorreto"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Senha incorreta"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"PIN incorreto"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Tente novamente em # segundo.}one{Tente novamente em # segundo.}many{Tente novamente em # segundos.}other{Tente novamente em # segundos.}}"</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Tente novamente em # segundo.}one{Tente novamente em # segundo.}other{Tente novamente em # segundos.}}"</string>
     <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Informe o PIN do chip."</string>
     <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Informe o PIN do chip para \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string>
     <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Desative o eSIM para usar o dispositivo sem serviço móvel."</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"O chip \"<xliff:g id="CARRIER">%1$s</xliff:g>\" foi desativado. Informe o código PUK para continuar. Entre em contato com a operadora para saber mais detalhes."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Digite o código PIN desejado"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirme o código PIN desejado"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Desbloqueando chip…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Desbloqueando o chip…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Digite um PIN com 4 a 8 números."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"O código PUK deve ter oito números ou mais."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Você digitou seu PIN incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente em <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Você digitou sua senha incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente em <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Você desenhou sua sequência de desbloqueio incorretamente <xliff:g id="NUMBER_0">%1$d</xliff:g> vezes. \n\nTente novamente em <xliff:g id="NUMBER_1">%2$d</xliff:g> segundos."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Código PIN do chip incorreto. Entre em contato com a operadora para desbloquear o dispositivo."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Código PIN do chip incorreto. Você tem # tentativa restante. Se o código correto não for digitado, será necessário entrar em contato com a operadora para desbloquear o dispositivo.}one{Código PIN do chip incorreto. Você tem # tentativa restante. }many{Código PIN do chip incorreto. Você tem # de tentativas restantes. }other{Código PIN do chip incorreto. Você tem # tentativas restantes. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Código PIN do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>.</item>
+      <item quantity="other">Código PIN do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"O chip não pode ser utilizado. Entre em contato com a operadora."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Código PUK do chip incorreto. Você tem # tentativa restante. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}one{Código PUK do chip incorreto. Você tem # tentativa restante. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}many{Código PUK do chip incorreto. Você tem # de tentativas restantes. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}other{Código PUK do chip incorreto. Você tem # tentativas restantes. Se o código correto não for digitado, o chip vai ficar permanentemente inutilizável.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Código PUK do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>. Caso o código correto não seja digitado, o chip se tornará permanentemente inutilizável.</item>
+      <item quantity="other">Código PUK do chip incorreto. Tentativas restantes: <xliff:g id="NUMBER_1">%d</xliff:g>. Caso o código correto não seja digitado, o chip se tornará permanentemente inutilizável.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Falha na operação de PIN do chip."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Falha na operação de PUK do chip."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Alterar o método de entrada"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Para usar o Desbloqueio facial, ative o acesso à câmera nas Configurações"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Informe o PIN do chip. Você tem # tentativa restante antes de precisar entrar em contato com a operadora para desbloquear seu dispositivo.}one{Informe o PIN do chip. Você tem # tentativa restante.}many{Informe o PIN do chip. Você tem # de tentativas restantes.}other{Informe o PIN do chip. Você tem # tentativas restantes.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{O chip está desativado. Informe o código PUK para continuar. Você tem # tentativa restante antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}one{O chip está desativado. Informe o código PUK para continuar. Você tem # tentativa restante antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}many{O chip está desativado. Informe o código PUK para continuar. Você tem # de tentativas restantes antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}other{O chip está desativado. Informe o código PUK para continuar. Você tem # tentativas restantes antes que o chip fique permanentemente inutilizável. Consulte sua operadora para saber mais.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para usar o Desbloqueio facial, ative o "<b>"acesso à câmera"</b>" em Configurações &gt; Privacidade"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Informe o PIN do chip. Você tem <xliff:g id="NUMBER_1">%d</xliff:g> tentativa restante.</item>
+      <item quantity="other">Informe o PIN do chip. Você tem <xliff:g id="NUMBER_1">%d</xliff:g> tentativas restantes.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">O chip agora está desativado. Informe o código PUK para continuar. Você tem <xliff:g id="_NUMBER_1">%d</xliff:g> tentativa restante antes de o chip se tornar permanentemente inutilizável. Entre em contato com a operadora para saber mais detalhes.</item>
+      <item quantity="other">O chip agora está desativado. Informe o código PUK para continuar. Você tem <xliff:g id="_NUMBER_1">%d</xliff:g> tentativas restantes antes de o chip se tornar permanentemente inutilizável. Entre em contato com a operadora para saber mais detalhes.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ro/strings.xml b/packages/SystemUI/res-keyguard/values-ro/strings.xml
index 844c106..04fe6be 100644
--- a/packages/SystemUI/res-keyguard/values-ro/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ro/strings.xml
@@ -20,75 +20,90 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"Introdu codul PIN"</string>
-    <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"Introdu modelul"</string>
-    <string name="keyguard_enter_your_password" msgid="7225626204122735501">"Introdu parola"</string>
+    <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"Introduceți codul PIN"</string>
+    <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"Introduceți modelul"</string>
+    <string name="keyguard_enter_your_password" msgid="7225626204122735501">"Introduceți parola"</string>
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Card nevalid"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Încărcată"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Se încarcă wireless"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Se încarcă"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Suport de încărcare"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Se încarcă"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Se încarcă rapid"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Se încarcă lent"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Încărcarea este optimizată pentru a proteja bateria"</string>
-    <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Apasă pe Meniu pentru a debloca."</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Încărcare limitată temporar"</string>
+    <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Apăsați pe Meniu pentru a debloca."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rețea blocată"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Niciun card SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Adaugă un card SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Cardul SIM lipsește sau nu poate fi citit. Adaugă un card SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Cardul SIM nu se poate folosi."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Cardul tău SIM a fost dezactivat definitiv.\n Contactează furnizorul de servicii wireless pentru a obține un alt card SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Cardul SIM este blocat."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Cardul SIM este blocat prin cod PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Se deblochează cardul SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Niciun card SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Introduceți un card SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Cardul SIM lipsește sau nu poate fi citit. Introduceți un card SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Card SIM inutilizabil."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Cardul dvs. SIM este dezactivat definitiv.\n Contactați furnizorul de servicii wireless pentru a obține un alt card SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Cardul SIM este blocat."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Cardul SIM este blocat cu codul PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Se deblochează cardul SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Zona codului PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Parola dispozitivului"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Zona codului PIN pentru cardul SIM"</string>
     <string name="keyguard_accessibility_sim_puk_area" msgid="5537294043180237374">"Zona codului PUK pentru cardul SIM"</string>
-    <string name="keyboardview_keycode_delete" msgid="8489719929424895174">"Șterge"</string>
-    <string name="disable_carrier_button_text" msgid="7153361131709275746">"Dezactivează cardul eSIM"</string>
+    <string name="keyboardview_keycode_delete" msgid="8489719929424895174">"Ștergeți"</string>
+    <string name="disable_carrier_button_text" msgid="7153361131709275746">"Dezactivați cardul eSIM"</string>
     <string name="error_disable_esim_title" msgid="3802652622784813119">"Nu se poate dezactiva cardul eSIM"</string>
     <string name="error_disable_esim_msg" msgid="2441188596467999327">"Cardul eSIM nu poate fi dezactivat din cauza unei erori."</string>
-    <string name="keyboardview_keycode_enter" msgid="6727192265631761174">"Introdu"</string>
+    <string name="keyboardview_keycode_enter" msgid="6727192265631761174">"Introduceți"</string>
     <string name="kg_wrong_pattern" msgid="5907301342430102842">"Model greșit"</string>
     <string name="kg_wrong_password" msgid="4143127991071670512">"Parolă greșită"</string>
     <string name="kg_wrong_pin" msgid="4160978845968732624">"Cod PIN greșit"</string>
-    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Reîncearcă peste o secundă.}few{Reîncearcă peste # secunde.}other{Reîncearcă peste # de secunde.}}"</string>
-    <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Introdu codul PIN al cardului SIM."</string>
-    <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Introdu codul PIN al cardului SIM pentru „<xliff:g id="CARRIER">%1$s</xliff:g>”."</string>
-    <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Dezactivează cardul eSIM pentru a folosi dispozitivul fără serviciu mobil."</string>
-    <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"Cardul SIM e acum dezactivat. Pentru a continua, introdu codul PUK. Pentru detalii, contactează operatorul."</string>
-    <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Cardul SIM „<xliff:g id="CARRIER">%1$s</xliff:g>\" e acum dezactivat. Pentru a continua, introdu codul PUK. Pentru detalii, contactează operatorul."</string>
-    <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Introdu codul PIN dorit"</string>
-    <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirmă codul PIN dorit"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Se deblochează cardul SIM…"</string>
-    <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Introdu un cod PIN alcătuit din 4 până la 8 cifre."</string>
+    <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Reîncercați peste o secundă.}few{Reîncercați peste # secunde.}other{Reîncercați peste # de secunde.}}"</string>
+    <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Introduceți codul PIN al cardului SIM."</string>
+    <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Introduceți codul PIN al cardului SIM pentru „<xliff:g id="CARRIER">%1$s</xliff:g>”."</string>
+    <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Dezactivați cardul eSIM pentru a folosi dispozitivul fără serviciu mobil."</string>
+    <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"Cardul SIM este acum dezactivat. Pentru a continua, introduceți codul PUK. Pentru detalii, contactați operatorul."</string>
+    <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Cardul SIM „<xliff:g id="CARRIER">%1$s</xliff:g>\" este acum dezactivat. Pentru a continua, introduceți codul PUK. Pentru detalii, contactați operatorul."</string>
+    <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Introduceți codul PIN dorit"</string>
+    <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirmați codul PIN dorit"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Se deblochează cardul SIM…"</string>
+    <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Introduceți un cod PIN alcătuit din 4 până la 8 cifre."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Codul PUK trebuie să aibă minimum 8 cifre."</string>
-    <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Ai introdus incorect codul PIN de <xliff:g id="NUMBER_0">%1$d</xliff:g> ori.\n\nÎncearcă din nou peste <xliff:g id="NUMBER_1">%2$d</xliff:g> secunde."</string>
-    <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Ai introdus incorect parola de <xliff:g id="NUMBER_0">%1$d</xliff:g> ori. \n\nÎncearcă din nou peste <xliff:g id="NUMBER_1">%2$d</xliff:g> secunde."</string>
-    <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Ai desenat incorect modelul pentru deblocare de <xliff:g id="NUMBER_0">%1$d</xliff:g> ori. \n\nÎncearcă din nou peste <xliff:g id="NUMBER_1">%2$d</xliff:g> secunde."</string>
-    <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Codul PIN pentru cardul SIM este incorect. Contactează operatorul pentru a debloca dispozitivul."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{PIN-ul cardului SIM e incorect. Ți-a mai rămas # încercare, după care va trebui să contactezi operatorul pentru a debloca dispozitivul.}few{PIN-ul cardului SIM e incorect. Ți-au mai rămas # încercări. }other{PIN-ul cardului SIM e incorect. Ți-au mai rămas # de încercări. }}"</string>
-    <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Cardul SIM nu poate fi utilizat. Contactează operatorul."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Codul PUK pentru cardul SIM e incorect. Ți-a mai rămas # încercare până când cardul SIM va deveni inutilizabil definitiv.}few{Codul PUK pentru cardul SIM e incorect. Ți-au mai rămas # încercări până când cardul SIM va deveni inutilizabil definitiv.}other{Codul PUK pentru cardul SIM e incorect. Ți-au mai rămas # de încercări până când cardul SIM va deveni inutilizabil definitiv.}}"</string>
+    <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Ați introdus incorect codul PIN de <xliff:g id="NUMBER_0">%1$d</xliff:g> ori.\n\nÎncercați din nou peste <xliff:g id="NUMBER_1">%2$d</xliff:g> secunde."</string>
+    <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Ați introdus incorect parola de <xliff:g id="NUMBER_0">%1$d</xliff:g> ori. \n\nÎncercați din nou peste <xliff:g id="NUMBER_1">%2$d</xliff:g> secunde."</string>
+    <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Ați desenat incorect modelul pentru deblocare de <xliff:g id="NUMBER_0">%1$d</xliff:g> ori. \n\nÎncercați din nou peste <xliff:g id="NUMBER_1">%2$d</xliff:g> secunde."</string>
+    <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Codul PIN pentru cardul SIM este incorect. Contactați operatorul pentru a vă debloca dispozitivul."</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="few">Codul PIN pentru cardul SIM este incorect. V-au mai rămas <xliff:g id="NUMBER_1">%d</xliff:g> încercări.</item>
+      <item quantity="other">Codul PIN pentru cardul SIM este incorect. V-au mai rămas <xliff:g id="NUMBER_1">%d</xliff:g> de încercări.</item>
+      <item quantity="one">Codul PIN pentru cardul SIM este incorect. V-a mai rămas <xliff:g id="NUMBER_0">%d</xliff:g> încercare, după care va trebui să contactați operatorul pentru a vă debloca dispozitivul.</item>
+    </plurals>
+    <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Cardul SIM nu poate fi utilizat. Contactați operatorul."</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="few">Codul PUK pentru cardul SIM este incorect. V-au mai rămas <xliff:g id="NUMBER_1">%d</xliff:g> încercări până când cardul SIM va deveni inutilizabil definitiv.</item>
+      <item quantity="other">Codul PUK pentru cardul SIM este incorect. V-au mai rămas <xliff:g id="NUMBER_1">%d</xliff:g> de încercări până când cardul SIM va deveni inutilizabil definitiv.</item>
+      <item quantity="one">Codul PUK pentru cardul SIM este incorect. V-a mai rămas <xliff:g id="NUMBER_0">%d</xliff:g> încercare până când cardul SIM va deveni inutilizabil definitiv.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Deblocarea cu ajutorul codului PIN pentru cardul SIM nu a reușit!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Deblocarea cu ajutorul codului PUK pentru cardul SIM nu a reușit!"</string>
-    <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Schimbă metoda de introducere"</string>
+    <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Comutați metoda de introducere"</string>
     <string name="airplane_mode" msgid="2528005343938497866">"Mod Avion"</string>
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Modelul este necesar după repornirea dispozitivului"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Codul PIN este necesar după repornirea dispozitivului"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Parola este necesară după repornirea dispozitivului"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pentru mai multă securitate, folosește modelul"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pentru mai multă securitate, folosește codul PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pentru mai multă securitate, folosește parola"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Modelul este necesar pentru securitate suplimentară"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Codul PIN este necesar pentru securitate suplimentară"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Parola este necesară pentru securitate suplimentară"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispozitiv blocat de administrator"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Dispozitivul a fost blocat manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nu este recunoscut"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Pentru Deblocare facială, activează accesul la cameră"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Introdu PIN-ul cardului SIM. Ți-a mai rămas # încercare, după care va trebui să contactezi operatorul pentru a debloca dispozitivul.}few{Introdu PIN-ul cardului SIM. Ți-au rămas # încercări.}other{Introdu PIN-ul cardului SIM. Ți-au rămas # de încercări.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Cardul SIM e acum dezactivat. Introdu codul PUK pentru a continua. Ți-a mai rămas # încercare până când cardul SIM va deveni inutilizabil definitiv. Contactează operatorul pentru detalii.}few{Cardul SIM e acum dezactivat. Introdu codul PUK pentru a continua. Ți-au mai rămas # încercări până când cardul SIM va deveni inutilizabil definitiv. Contactează operatorul pentru detalii.}other{Cardul SIM e acum dezactivat. Introdu codul PUK pentru a continua. Ți-au mai rămas # de încercări până când cardul SIM va deveni inutilizabil definitiv. Contactează operatorul pentru detalii.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Pentru a folosi Deblocarea facială, activați "<b>"Accesul la cameră"</b>" în Setări și confidențialitate"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="few">Introduceți codul PIN pentru cardul SIM. V-au mai rămas <xliff:g id="NUMBER_1">%d</xliff:g> încercări.</item>
+      <item quantity="other">Introduceți codul PIN pentru cardul SIM. V-au mai rămas <xliff:g id="NUMBER_1">%d</xliff:g> de încercări.</item>
+      <item quantity="one">Introduceți codul PIN pentru cardul SIM. V-a mai rămas <xliff:g id="NUMBER_0">%d</xliff:g> încercare, după care va trebui să contactați operatorul pentru a vă debloca dispozitivul.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="few">Cardul SIM este dezactivat acum. Introduceți codul PUK pentru a continua. V-au mai rămas <xliff:g id="_NUMBER_1">%d</xliff:g> încercări până când cardul SIM va deveni inutilizabil definitiv. Contactați operatorul pentru detalii.</item>
+      <item quantity="other">Cardul SIM este dezactivat acum. Introduceți codul PUK pentru a continua. V-au mai rămas <xliff:g id="_NUMBER_1">%d</xliff:g> de încercări până când cardul SIM va deveni inutilizabil definitiv. Contactați operatorul pentru detalii.</item>
+      <item quantity="one">Cardul SIM este dezactivat acum. Introduceți codul PUK pentru a continua. V-a mai rămas <xliff:g id="_NUMBER_0">%d</xliff:g> încercare până când cardul SIM va deveni inutilizabil definitiv. Contactați operatorul pentru detalii.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Prestabilit"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogic"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Deblochează dispozitivul pentru a continua"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ru/strings.xml b/packages/SystemUI/res-keyguard/values-ru/strings.xml
index 6957e18..364c8b7 100644
--- a/packages/SystemUI/res-keyguard/values-ru/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ru/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ошибка SIM-карты."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Батарея заряжена"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Беспроводная зарядка"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядка"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядка от док-станции"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"Идет зарядка (<xliff:g id="PERCENTAGE">%s</xliff:g>)"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"Идет быстрая зарядка (<xliff:g id="PERCENTAGE">%s</xliff:g>)"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"Идет медленная зарядка (<xliff:g id="PERCENTAGE">%s</xliff:g>)"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядка оптимизирована для защиты батареи"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Зарядка временно ограничена"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Для разблокировки нажмите \"Меню\"."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Сеть заблокирована"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM-карта отсутствует"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Добавьте SIM-карту."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-карта отсутствует или не распознана. Добавьте SIM-карту."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-карту невозможно использовать."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM-карта была окончательно деактивирована.\n Чтобы получить новую, обратитесь к своему оператору мобильной связи."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-карта заблокирована."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-карта заблокирована с помощью PUK-кода."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Разблокировка SIM-карты…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Нет SIM-карты."</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Вставьте SIM-карту."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-карта отсутствует или недоступна. Вставьте SIM-карту."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM-карта непригодна к использованию."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-карта окончательно заблокирована.\nЧтобы получить новую, обратитесь к своему оператору."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-карта заблокирована."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-карта заблокирована с помощью PUK-кода."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Разблокировка SIM-карты…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-код"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Пароль устройства"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"PIN-код SIM-карты"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-карта \"<xliff:g id="CARRIER">%1$s</xliff:g>\" отключена. Чтобы продолжить, введите PUK-код. За подробной информацией обратитесь к оператору связи."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Введите PIN-код"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Подтвердите PIN-код"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Разблокировка SIM-карты…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Разблокировка SIM-карты…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Введите PIN-код (от 4 до 8 цифр)."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-код должен содержать не менее 8 цифр."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Вы ввели неверный PIN-код несколько раз (<xliff:g id="NUMBER_0">%1$d</xliff:g>).\n\nПовторите попытку через <xliff:g id="NUMBER_1">%2$d</xliff:g> сек."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Вы ввели неверный пароль несколько раз (<xliff:g id="NUMBER_0">%1$d</xliff:g>).\n\nПовторите попытку через <xliff:g id="NUMBER_1">%2$d</xliff:g> сек."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Вы начертили неверный графический ключ несколько раз (<xliff:g id="NUMBER_0">%1$d</xliff:g>).\n\nПовторите попытку через <xliff:g id="NUMBER_1">%2$d</xliff:g> сек."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Неверный PIN-код. Обратитесь к оператору связи, чтобы разблокировать SIM-карту."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Неверный PIN-код. Осталась # попытка. Если ввести неправильный PIN-код ещё раз, SIM-карта будет заблокирована и вам придется обратиться к оператору связи.}one{Неверный PIN-код. Осталась # попытка. }few{Неверный PIN-код. Осталось # попытки. }many{Неверный PIN-код. Осталось # попыток. }other{Неверный PIN-код. Осталось # попытки. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Неверный PIN-код. Осталась <xliff:g id="NUMBER_1">%d</xliff:g> попытка.</item>
+      <item quantity="few">Неверный PIN-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попытки.</item>
+      <item quantity="many">Неверный PIN-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попыток.</item>
+      <item quantity="other">Неверный PIN-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попытки.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-карта заблокирована навсегда. Обратитесь к оператору связи."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Неверный PUK-код. Осталась # попытка. Если ввести неправильный PUK-код ещё раз, SIM-карта будет заблокирована навсегда.}one{Неверный PUK-код. Осталась # попытка. После того как попытки закончатся, SIM-карта будет заблокирована навсегда.}few{Неверный PUK-код. Осталось # попытки. После того как они закончатся, SIM-карта будет заблокирована навсегда.}many{Неверный PUK-код. Осталось # попыток. После того как они закончатся, SIM-карта будет заблокирована навсегда.}other{Неверный PUK-код. Осталось # попытки. После того как попытки закончатся, SIM-карта будет заблокирована навсегда.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Неверный PUK-код. Осталась <xliff:g id="NUMBER_1">%d</xliff:g> попытка. После этого SIM-карта будет заблокирована навсегда.</item>
+      <item quantity="few">Неверный PUK-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попытки. После этого SIM-карта будет заблокирована навсегда.</item>
+      <item quantity="many">Неверный PUK-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попыток. После этого SIM-карта будет заблокирована навсегда.</item>
+      <item quantity="other">Неверный PUK-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попытки. После этого SIM-карта будет заблокирована навсегда.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Не удалось разблокировать SIM-карту"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Не удалось разблокировать SIM-карту"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Сменить способ ввода"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"После перезагрузки устройства необходимо ввести графический ключ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"После перезагрузки устройства необходимо ввести PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"После перезагрузки устройства необходимо ввести пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"В целях дополнительной безопасности используйте графический ключ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"В целях дополнительной безопасности используйте PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"В целях дополнительной безопасности используйте пароль"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"В качестве дополнительной меры безопасности введите графический ключ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"В качестве дополнительной меры безопасности введите PIN-код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"В качестве дополнительной меры безопасности введите пароль"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройство заблокировано администратором"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройство было заблокировано вручную"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распознано"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"В настройках разрешите фейсконтролю доступ к камере."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Введите PIN-код. Осталась # попытка. Если указать неверный PIN-код ещё раз, SIM-карта будет заблокирована и вам придется обратиться к оператору связи.}one{Введите PIN-код SIM-карты. Осталась # попытка.}few{Введите PIN-код SIM-карты. Осталось # попытки.}many{Введите PIN-код SIM-карты. Осталось # попыток.}other{Введите PIN-код SIM-карты. Осталось # попытки.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-карта заблокирована. Чтобы продолжить, введите PUK-код. Осталась # попытка. Если ввести неверный PUK-код, SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.}one{SIM-карта заблокирована. Чтобы продолжить, введите PUK-код. Осталась # попытка. После того как попытки закончатся, SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.}few{SIM-карта заблокирована. Чтобы продолжить, введите PUK-код. Осталось # попытки. После того как они закончатся, SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.}many{SIM-карта заблокирована. Чтобы продолжить, введите PUK-код. Осталось # попыток. После того как они закончатся, SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.}other{SIM-карта заблокирована. Чтобы продолжить, введите PUK-код. Осталось # попытки. После того как попытки закончатся, SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Чтобы использовать фейсконтроль, разрешите "<b>"доступ к камере"</b>". Для этого перейдите в настройки и нажмите \"Конфиденциальность\"."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Введите PIN-код. Осталась <xliff:g id="NUMBER_1">%d</xliff:g> попытка.</item>
+      <item quantity="few">Введите PIN-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попытки.</item>
+      <item quantity="many">Введите PIN-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попыток.</item>
+      <item quantity="other">Введите PIN-код. Осталось <xliff:g id="NUMBER_1">%d</xliff:g> попытки.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM-карта отключена. Чтобы продолжить, введите PUK-код. Осталась <xliff:g id="_NUMBER_1">%d</xliff:g> попытка. После этого SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.</item>
+      <item quantity="few">SIM-карта отключена. Чтобы продолжить, введите PUK-код. Осталось <xliff:g id="_NUMBER_1">%d</xliff:g> попытки. После этого SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.</item>
+      <item quantity="many">SIM-карта отключена. Чтобы продолжить, введите PUK-код. Осталось <xliff:g id="_NUMBER_1">%d</xliff:g> попыток. После этого SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.</item>
+      <item quantity="other">SIM-карта отключена. Чтобы продолжить, введите PUK-код. Осталось <xliff:g id="_NUMBER_1">%d</xliff:g> попытки. После этого SIM-карта будет заблокирована навсегда. За подробной информацией обратитесь к оператору связи.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"По умолчанию"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Пузырь"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Стрелки"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Чтобы продолжить, разблокируйте устройство"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-si/strings.xml b/packages/SystemUI/res-keyguard/values-si/strings.xml
index 2402d50..5e934cc 100644
--- a/packages/SystemUI/res-keyguard/values-si/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-si/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"වලංගු නොවන කාඩ්පත."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"අරෝපිතයි"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • නොරැහැන්ව ආරෝපණ කෙරේ"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ආරෝපණය වෙමින්"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ආරෝපණය වන ඩොකය"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ආරෝපණය වෙමින්"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • වේගයෙන් ආරෝපණය වෙමින්"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • සෙමින් ආරෝපණය වෙමින්"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • බැටරිය ආරක්ෂා කිරීම සඳහා ආරෝපණය ප්‍රශස්ත කර ඇත"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ආරෝපණය කිරීම තාවකාලිකව සීමා කර ඇත"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"අගුලු හැරීමට මෙනුව ඔබන්න."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"ජාලය අගුළු දමා ඇත"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM නැත"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM එකක් එක් කරන්න."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM අස්ථානගතයි හෝ කියවිය නොහැක. SIM එකක් එක් කරන්න."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"භාවිත කළ නොහැකි SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ඔබේ SIM ස්ථිරවම අක්‍රිය කර ඇත.\n වෙනත් SIM පතක් සඳහා ඔබේ රැහැන් රහිත සේවා සපයන්නා අමතන්න."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM අගුළු දමා ඇත."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM PUK-අගුළු දමා ඇත."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM අගුළු අරිමින්…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM පත නැත"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM කාඩ්පතක් ඇතුළු කරන්න."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM පත නොමැත හෝ කියවිය නොහැක. SIM පතක් ඇතුලත් කරන්න."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"භාවිතා කළ නොහැකි SIM පත."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ඔබගේ SIM පත ස්ථිරව අබල කර තිබේ.\n වෙනත් SIM පතක් සඳහා ඔබගේ නොරැහැන් සේවා සැපයුම්කරු සම්බන්ධ කර ගන්න."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM පත අගුළු දමා ඇත."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM පත PUK අගුළු ලා ඇත."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM පත අගුළු හරිමින්..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN කොටස"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"උපාංග මුරපදය"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN කොටස"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" දැන් අබල කර ඇත. දිගටම පවත්වා ගෙන යාමට PUK කේතය ඇතුළත් කරන්න. විස්තර සඳහා වාහකයා සම්බන්ධ කර ගන්න."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"අපේක්ෂිත PIN කේතය ඇතුළත් කරන්න"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"අපේක්ෂිත PIN කේතය ස්ථිර කරන්න"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM අගුළු අරිමින්…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM පත අගුළු හරිමින්..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 සිට 8 දක්වා අංක සහිත PIN එකක් ටයිප් කරන්න."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK කේතය සංඛ්‍යා 8 ක් හෝ වැඩි විය යුතුය."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"ඔබ PIN අංකය <xliff:g id="NUMBER_0">%1$d</xliff:g> වාරයක් වැරදියට ටයිප් කොට ඇත.\n\n තත්පර <xliff:g id="NUMBER_1">%2$d</xliff:g> ක් ඇතුළත නැවත උත්සාහ කරන්න."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"ඔබ මුරපදය වාර <xliff:g id="NUMBER_0">%1$d</xliff:g> ක් වැරදියට ටයිප්කොට ඇත. \n\nතත්පර <xliff:g id="NUMBER_1">%2$d</xliff:g> කින් නැවත උත්සහ කරන්න."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"ඔබ <xliff:g id="NUMBER_0">%1$d</xliff:g> වාරයක් අගුළු ඇරීමේ රටාව වැරදියට ඇඳ ඇත. \n\nතත්පර <xliff:g id="NUMBER_1">%2$d</xliff:g> ක් ඇතුළත නැවත උත්සාහ කරන්න."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"වැරදී SIM PIN කේතයකි, ඔබගේ දුරකතනයේ අඟුල හැරීමට ඔබගේ වාහකයා ඔබ දැන් සම්බන්ධ කරගත යුතුය."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{වැරදි SIM PIN කේතයකි, ඔබේ උපාංගයේ අගුළු හැරීමට ඔබ ඔබේ වාහකයා සම්බන්ධ කර ගත යුතු වීමට පෙර ඔබ සතුව # උත්සාහයක් ඉතිරිව ඇත.}one{වැරදි SIM PIN කේතයකි, ඔබ සතුව උත්සාහයන් #ක් ඉතිරිව ඇත. }other{වැරදි SIM PIN කේතයකි, ඔබ සතුව උත්සාහයන් #ක් ඉතිරිව ඇත. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">වැරදී SIM PIN කේතයකි, ඔබගේ දුරකථනයේ අඟුල හැරීමට ඔබගේ වාහකයා සම්බන්ධ කරගැනීමට පෙර ඔබ සතුව තවත් උත්සාහයන් <xliff:g id="NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත.</item>
+      <item quantity="other">වැරදී SIM PIN කේතයකි, ඔබගේ දුරකථනයේ අගුල හැරීමට ඔබගේ වාහකයා සම්බන්ධ කරගැනීමට පෙර ඔබ සතුව තවත් උත්සාහයන් <xliff:g id="NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM කාඩ් පත භාවිතා කළ නොහැක. ඔබගේ වාහකය සම්බන්ධ කරගන්න."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{වැරදි SIM PUK කේතයකි, SIM කාඩ්පත ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබට # උත්සාහයක් ඉතිරිව ඇත.}one{වැරදි SIM PUK කේතයකි, SIM කාඩ්පත ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබට උත්සාහයන් #ක් ඉතිරිව ඇත.}other{වැරදි SIM PUK කේතයකි, SIM කාඩ්පත ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබට උත්සාහයන් #ක් ඉතිරිව ඇත.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">වැරදි SIM PUK කේතයකි, SIM කාඩ්පත ස්ථිරවම භාවිත කළ නොහැකි බවට පත්වීමට පෙර ඔබට තවත් උත්සාහයන් <xliff:g id="NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත.</item>
+      <item quantity="other">වැරදි SIM PUK කේතයකි, SIM කාඩ්පත ස්ථිරවම භාවිත කළ නොහැකි බවට පත්වීමට පෙර ඔබට තවත් උත්සාහයන් <xliff:g id="NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN මෙහෙයුම අසාර්ථක විය!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK මෙහෙයුම අසාර්ථක විය!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ආදාන ක්‍රමය මාරු කිරීම"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"උපාංගය නැවත ආරම්භ වූ පසු රටාව අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"උපාංගය නැවත ආරම්භ වූ පසු PIN අංකය අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"උපාංගය නැවත ආරම්භ වූ පසු මුරපදය අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"අතිරේක ආරක්ෂාව සඳහා, ඒ වෙනුවට රටාව භාවිතා කරන්න"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"අතිරේක ආරක්ෂාව සඳහා, ඒ වෙනුවට PIN භාවිතා කරන්න"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"අතිරේක ආරක්ෂාව සඳහා, ඒ වෙනුවට මුරපදය භාවිතා කරන්න"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"අමතර ආරක්ෂාව සඳහා රටාව අවශ්‍යයි"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"අමතර ආරක්ෂාව සඳහා PIN අංකය අවශ්‍යයි"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"අමතර ආරක්ෂාව සඳහා මුරපදය අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ඔබගේ පරිපාලක විසින් උපාංගය අගුළු දමා ඇත"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"උපාංගය හස්තීයව අගුලු දමන ලදී"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"හඳුනා නොගන්නා ලදී"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"මුහුණෙන් අගුලු හැරීමට, සැකසීම් තුළ කැමරා ප්‍රවේශය සක්‍රීය කරන්න"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PIN ඇතුළු කරන්න, ඔබේ උපාංගය අගුළු හැරීමට ඔබේ වාහකය සම්බන්ධ කර ගැනීමට පෙර ඔබ සතුව # උත්සාහයක් ඉතිරිව ඇත.}one{SIM PIN ඇතුළු කරන්න. ඔබ සතුව උත්සාහයන් #ක් ඉතිරිව ඇත.}other{SIM PIN ඇතුළු කරන්න. ඔබ සතුව උත්සාහයන් #ක් ඉතිරිව ඇත.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM දැන් අබල කර ඇත. ඉදිරියට යාමට PUK කේතය ඇතුළු කරන්න. SIM ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබ සතුව # උත්සාහයක් ඉතිරිව ඇත. විස්තර සඳහා වාහකය සම්බන්ධ කර ගන්න.}one{SIM දැන් අබල කර ඇත. ඉදිරියට යාමට PUK කේතය ඇතුළු කරන්න. SIM ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබ සතුව උත්සාහයන් #ක් ඉතිරිව ඇත. විස්තර සඳහා වාහකය සම්බන්ධ කර ගන්න.}other{SIM දැන් අබල කර ඇත. ඉදිරියට යාමට PUK කේතය ඇතුළු කරන්න. SIM ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබ සතුව උත්සාහයන් #ක් ඉතිරිව ඇත. විස්තර සඳහා වාහකය සම්බන්ධ කර ගන්න.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"මුහුණෙන් අගුලු හැරීම භාවිත කිරීමට, සැකසීම් &gt; පෞද්ගලිකත්වය තුළ "<b>"කැමරා ප්‍රවේශය"</b>" ක්‍රියාත්මක කරන්න"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">SIM PIN ඇතුළු කරන්න, ඔබ සතුව උත්සාහයන් <xliff:g id="NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත.</item>
+      <item quantity="other">SIM PIN ඇතුළු කරන්න, ඔබ සතුව උත්සාහයන් <xliff:g id="NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM දැන් අබල කර ඇත. දිගටම කරගෙන යාමට PUK කේතය ඇතුළු කරන්න. SIM ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබ සතුව උත්සාහයන් <xliff:g id="_NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත. විස්තර සඳහා වාහක සම්බන්ධ කර ගන්න.</item>
+      <item quantity="other">SIM දැන් අබල කර ඇත. දිගටම කරගෙන යාමට PUK කේතය ඇතුළු කරන්න. SIM ස්ථිරවම භාවිත කළ නොහැකි බවට පත් වීමට පෙර ඔබ සතුව උත්සාහයන් <xliff:g id="_NUMBER_1">%d</xliff:g>ක් ඉතිරිව ඇත. විස්තර සඳහා වාහක සම්බන්ධ කර ගන්න.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"පෙරනිමි"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"බුබුළ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ප්‍රතිසමය"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ඉදිරියට යාමට ඔබේ උපාංගය අගුළු හරින්න"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sk/strings.xml b/packages/SystemUI/res-keyguard/values-sk/strings.xml
index 9bc746b..187683f 100644
--- a/packages/SystemUI/res-keyguard/values-sk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sk/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Neplatná karta."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Nabité"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíja sa bezdrôtovo"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíja sa"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjací dok"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíja sa"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíja sa rýchlo"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíja sa pomaly"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjanie je optimalizované, aby sa chránila batéria"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nabíjanie je dočasne obmedzené"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Odomknete stlačením tlačidla ponuky."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Sieť je zablokovaná"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Žiadna SIM karta"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Pridajte SIM kartu."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM karta chýba alebo sa nedá čítať. Pridajte SIM kartu."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Nepoužiteľná SIM karta."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Vaša SIM karta bola natrvalo deaktivovaná.\n Požiadajte svojho poskytovateľa bezdrôtových služieb o ďalšiu SIM kartu."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM karta je uzamknutá."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM karta je uzamknutá kódom PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM karta sa odomyká…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Žiadna SIM karta"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Vložte SIM kartu."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM karta chýba alebo sa z nej nedá čítať. Vložte SIM kartu."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM karta je nepoužiteľná."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Vaša SIM karta bola natrvalo zakázaná.\nAk chcete získať inú SIM kartu, kontaktujte svojho poskytovateľa bezdrôtových služieb."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM karta je uzamknutá."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM karta je uzamknutá pomocou kódu PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Prebieha odomykanie SIM karty…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Oblasť kódu PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Heslo zariadenia"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Oblasť kódu PIN SIM karty"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM karta operátora <xliff:g id="CARRIER">%1$s</xliff:g> bola zakázaná. Ak chcete pokračovať, zadajte kód PUK. Podrobnosti získate od svojho operátora."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Zadajte požadovaný kód PIN"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potvrďte požadovaný kód PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM karta sa odomyká…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Prebieha odomykanie SIM karty…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Zadajte kód PIN s dĺžkou 4 až 8 číslic."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Kód PUK musí obsahovať 8 alebo viac číslic."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Už <xliff:g id="NUMBER_0">%1$d</xliff:g>-krát ste zadali nesprávny kód PIN. \n\nSkúste to znova o <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Už <xliff:g id="NUMBER_0">%1$d</xliff:g>-krát ste zadali nesprávne heslo. \n\nSkúste to znova o <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Už <xliff:g id="NUMBER_0">%1$d</xliff:g>-krát ste použili nesprávny bezpečnostný vzor. \n\nSkúste to znova o <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Nesprávny kód PIN SIM karty. Teraz musíte kontaktovať svojho operátora, aby vám odomkol zariadenie."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Nesprávny kód PIN SIM karty. Zostáva vám # pokus, potom budete musieť kontaktovať svojho operátora, aby zariadenie odomkol.}few{Nesprávny kód PIN SIM karty. Zostávajú vám # pokusy. }many{Incorrect SIM PIN code, you have # remaining attempts. }other{Nesprávny kód PIN SIM karty. Zostáva vám # pokusov. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="few">Nesprávny kód PIN SIM karty. Zostávajú vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusy.</item>
+      <item quantity="many">Nesprávny kód PIN SIM karty. Zostáva vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusu.</item>
+      <item quantity="other">Nesprávny kód PIN SIM karty. Zostáva vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusov.</item>
+      <item quantity="one">Nesprávny kód PIN SIM karty. Zostáva vám <xliff:g id="NUMBER_0">%d</xliff:g> pokus. Potom budete musieť kontaktovať operátora, aby vám odomkol zariadenie.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM karta je nepoužiteľná. Kontaktujte svojho operátora."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Nesprávny kód PUK SIM karty. Zostáva vám # pokus, potom sa SIM karta natrvalo zablokuje.}few{Nesprávny kód PUK SIM karty. Zostávajú vám # pokusy, potom sa SIM karta natrvalo zablokuje.}many{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}other{Nesprávny kód PUK SIM karty. Zostáva vám # pokusov, potom sa SIM karta natrvalo zablokuje.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="few">Nesprávny kód PUK SIM karty. Zostávajú vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusy. Potom sa SIM karta natrvalo zablokuje.</item>
+      <item quantity="many">Nesprávny kód PUK SIM karty. Zostáva vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusu. Potom sa SIM karta natrvalo zablokuje.</item>
+      <item quantity="other">Nesprávny kód PUK SIM karty. Zostáva vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusov. Potom sa SIM karta natrvalo zablokuje.</item>
+      <item quantity="one">Nesprávny kód PUK SIM karty. Zostáva vám <xliff:g id="NUMBER_0">%d</xliff:g> pokus. Potom sa vaša SIM karta natrvalo zablokuje.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operácia kódu PIN SIM karty zlyhala!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operácia kódu PUK SIM karty zlyhala!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Prepnúť metódu vstupu"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po reštartovaní zariadenia musíte zadať bezpečnostný vzor"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po reštartovaní zariadenia musíte zadať kód PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po reštartovaní zariadenia musíte zadať heslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"V rámci zvýšenia zabezpečenia použite radšej vzor"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"V rámci zvýšenia zabezpečenia použite radšej PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"V rámci zvýšenia zabezpečenia použite radšej heslo"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Na ďalšie zabezpečenie musíte zadať bezpečnostný vzor"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Na ďalšie zabezpečenie musíte zadať kód PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Na ďalšie zabezpečenie musíte zadať heslo"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zariadenie zamkol správca"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zariadenie bolo uzamknuté ručne"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznané"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"V nastaveniach zapnite prístup ku kamere"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Zadajte kód PIN SIM karty. Zostáva vám # pokus, potom budete musieť kontaktovať svojho operátora, aby zariadenie odomkol.}few{Zadajte PIN SIM karty. Zostávajú vám # pokusy.}many{Enter SIM PIN. You have # remaining attempts.}other{Zadajte PIN SIM karty. Zostáva vám # pokusov.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostáva vám # pokus, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.}few{SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostávajú vám # pokusy, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.}many{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact carrier for details.}other{SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostáva vám # pokusov, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Ak chcete používať odomknutie tvárou, v sekcii Nastavenia &gt; Ochrana súkromia zapnite "<b>"prístup ku kamere"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="few">Zadajte kód PIN SIM karty. Zostávajú vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusy.</item>
+      <item quantity="many">Enter SIM PIN. You have <xliff:g id="NUMBER_1">%d</xliff:g> remaining attempts.</item>
+      <item quantity="other">Zadajte kód PIN SIM karty. Zostáva vám <xliff:g id="NUMBER_1">%d</xliff:g> pokusov.</item>
+      <item quantity="one">Zadajte kód PIN SIM karty. Zostáva vám <xliff:g id="NUMBER_0">%d</xliff:g> pokus, potom budete musieť kontaktovať svojho operátora, aby zariadenie odomkol.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="few">SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostávajú vám <xliff:g id="_NUMBER_1">%d</xliff:g> pokusy, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.</item>
+      <item quantity="many">SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostáva vám <xliff:g id="_NUMBER_1">%d</xliff:g> pokusu, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.</item>
+      <item quantity="other">SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostáva vám <xliff:g id="_NUMBER_1">%d</xliff:g> pokusov, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.</item>
+      <item quantity="one">SIM karta je deaktivovaná. Pokračujte zadaním kódu PUK. Zostáva vám <xliff:g id="_NUMBER_0">%d</xliff:g> pokus, potom sa SIM karta natrvalo zablokuje. Podrobnosti vám poskytne operátor.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Predvolený"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógový"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ak chcete pokračovať, odomknite zariadenie"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sl/strings.xml b/packages/SystemUI/res-keyguard/values-sl/strings.xml
index b0a2501..17c590f 100644
--- a/packages/SystemUI/res-keyguard/values-sl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sl/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Neveljavna kartica"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Baterija napolnjena"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • brezžično polnjenje"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Polnjenje"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Polnjenje na nosilcu"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • polnjenje"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • hitro polnjenje"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • počasno polnjenje"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Polnjenje je optimizirano zaradi zaščite baterije"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Polnjenje začasno omejeno"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Če želite odkleniti, pritisnite meni."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Omrežje je zaklenjeno"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Ni kartice SIM."</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Dodajte kartico SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Ni kartice SIM ali je ni mogoče prebrati. Dodajte kartico SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Kartica SIM je neuporabna."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Vaša kartica SIM je bila trajno deaktivirana.\n Za drugo kartico SIM se obrnite na ponudnika brezžičnih storitev."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Kartica SIM je zaklenjena."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Kartica SIM je zaklenjena s kodo PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Odklepanje kartice SIM …"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Ni kartice SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Vstavite kartico SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Ni kartice SIM ali je ni mogoče prebrati. Vstavite kartico SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Neuporabna kartica SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Kartica SIM je trajno onemogočena.\n Obrnite se na operaterja za drugo."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Kartica SIM je zaklenjena."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Kartica SIM je zaklenjena s kodo PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Odklepanje kartice SIM …"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Območje za kodo PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Geslo naprave"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Območje za kodo PIN kartice SIM"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Kartica SIM operaterja »<xliff:g id="CARRIER">%1$s</xliff:g>« je onemogočena. Če želite nadaljevati, vnesite kodo PUK. Za podrobnosti se obrnite na operaterja."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Vnesite želeno kodo PIN"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Potrdite želeno kodo PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Odklepanje kartice SIM …"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Odklepanje kartice SIM …"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Vnesite kodo PIN, ki vsebuje od štiri do osem številk."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Koda PUK mora biti 8- ali večmestno število."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Kodo PIN ste <xliff:g id="NUMBER_0">%1$d</xliff:g>-krat vnesli napačno. \n\nPoskusite znova čez <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Geslo ste <xliff:g id="NUMBER_0">%1$d</xliff:g>-krat vnesli napačno. \n\nPoskusite znova čez <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Vzorec za odklepanje ste <xliff:g id="NUMBER_0">%1$d</xliff:g>-krat nepravilno narisali. \n\nPoskusite znova čez <xliff:g id="NUMBER_1">%2$d</xliff:g> s."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Napačna koda PIN kartice SIM. Zdaj se boste morali za odklenitev naprave obrniti na operaterja."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Napačna koda PIN kartice SIM. Na voljo imate še # poskus. Nato se boste morali za odklepanje naprave obrniti na operaterja.}one{Napačna koda PIN kartice SIM. Na voljo imate še # poskus. }two{Napačna koda PIN kartice SIM. Na voljo imate še # poskusa. }few{Napačna koda PIN kartice SIM. Na voljo imate še # poskuse. }other{Napačna koda PIN kartice SIM. Na voljo imate še # poskusov. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Napačna koda PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskus.</item>
+      <item quantity="two">Napačna koda PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskusa.</item>
+      <item quantity="few">Napačna koda PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskuse.</item>
+      <item quantity="other">Napačna koda PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskusov.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Kartica SIM ni več uporabna. Obrnite se na operaterja."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Napačna koda PUK kartice SIM. Na voljo imate še # poskus. Nato bo kartica SIM postala trajno neuporabna.}one{Napačna koda PUK kartice SIM. Na voljo imate še # poskus. Nato bo kartica SIM postala trajno neuporabna.}two{Napačna koda PUK kartice SIM. Na voljo imate še # poskusa. Nato bo kartica SIM postala trajno neuporabna.}few{Napačna koda PUK kartice SIM. Na voljo imate še # poskuse. Nato bo kartica SIM postala trajno neuporabna.}other{Napačna koda PUK kartice SIM. Na voljo imate še # poskusov. Nato bo kartica SIM postala trajno neuporabna.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Napačna koda PUK kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskus. Potem bo kartica SIM postala trajno neuporabna.</item>
+      <item quantity="two">Napačna koda PUK kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskusa. Potem bo kartica SIM postala trajno neuporabna.</item>
+      <item quantity="few">Napačna koda PUK kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskuse. Potem bo kartica SIM postala trajno neuporabna.</item>
+      <item quantity="other">Napačna koda PUK kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskusov. Potem bo kartica SIM postala trajno neuporabna.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Postopek za odklepanje s kodo PIN kartice SIM ni uspel."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Postopek za odklepanje s kodo PUK kartice SIM ni uspel."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Preklop načina vnosa"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po vnovičnem zagonu naprave je treba vnesti vzorec"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po vnovičnem zagonu naprave je treba vnesti kodo PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po vnovičnem zagonu naprave je treba vnesti geslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Za dodatno varnost raje uporabite vzorec."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Za dodatno varnost raje uporabite kodo PIN."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Za dodatno varnost raje uporabite geslo."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zaradi dodatne varnosti morate vnesti vzorec"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zaradi dodatne varnosti morate vnesti kodo PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zaradi dodatne varnosti morate vnesti geslo"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Napravo je zaklenil skrbnik"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Naprava je bila ročno zaklenjena"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ni prepoznano"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Odklepanje z obrazom potrebuje dostop do fotoaparata."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Vnesite kodo PIN kartice SIM. Na voljo imate še # poskus. Nato se boste morali za odklepanje naprave obrniti na operaterja.}one{Vnesite kodo PIN kartice SIM. Na voljo imate še # poskus.}two{Vnesite kodo PIN kartice SIM. Na voljo imate še # poskusa.}few{Vnesite kodo PIN kartice SIM. Na voljo imate še # poskuse.}other{Vnesite kodo PIN kartice SIM. Na voljo imate še # poskusov.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še # poskus. Nato bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.}one{Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še # poskus. Nato bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.}two{Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še # poskusa. Nato bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.}few{Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še # poskuse. Nato bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.}other{Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še # poskusov. Nato bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Če želite uporabljati odklepanje z obrazom, v meniju »Nastavitve« &gt; »Zasebnost« vklopite možnost "<b>"Dostop do fotoaparata"</b>"."</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Vnesite kodo PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskus.</item>
+      <item quantity="two">Vnesite kodo PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskusa.</item>
+      <item quantity="few">Vnesite kodo PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskuse.</item>
+      <item quantity="other">Vnesite kodo PIN kartice SIM. Na voljo imate še <xliff:g id="NUMBER_1">%d</xliff:g> poskusov.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še <xliff:g id="_NUMBER_1">%d</xliff:g> poskus. Potem bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.</item>
+      <item quantity="two">Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še <xliff:g id="_NUMBER_1">%d</xliff:g> poskusa. Potem bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.</item>
+      <item quantity="few">Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še <xliff:g id="_NUMBER_1">%d</xliff:g> poskuse. Potem bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.</item>
+      <item quantity="other">Kartica SIM je zdaj onemogočena. Če želite nadaljevati, vnesite kodo PUK. Na voljo imate še <xliff:g id="_NUMBER_1">%d</xliff:g> poskusov. Potem bo kartica SIM postala trajno neuporabna. Za podrobnosti se obrnite na operaterja.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Privzeto"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurček"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogno"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Za nadaljevanje odklenite napravo"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sq/strings.xml b/packages/SystemUI/res-keyguard/values-sq/strings.xml
index 6e1ae2d..16adc13 100644
--- a/packages/SystemUI/res-keyguard/values-sq/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sq/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Karta e pavlefshme."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"I karikuar"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Po karikohet me valë"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Po karikohet"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Po karikohet në stacion"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Po karikohet"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Po karikohet me shpejtësi"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Po karikohet ngadalë"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Karikimi u optimizua për të mbrojtur baterinë"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Karikimi përkohësisht i kufizuar"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Shtyp \"Meny\" për të shkyçur."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Rrjeti është i kyçur"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Nuk ka kartë SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Shto një kartë SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Karta SIM mungon ose është e palexueshme. Shto një kartë SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Kartë SIM e papërdorshme."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Karta jote SIM është çaktivizuar përgjithmonë.\n Kontakto me ofruesin e shërbimit me valë për një kartë tjetër SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Karta SIM është e kyçur."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Karta SIM është e kyçur me PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Karta SIM po shkyçet…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Nuk ka kartë SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Fut një kartë SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Karta SIM mungon ose është e palexueshme. Fut një kartë të re SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Kartë SIM është e papërdorshme."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Karta jote SIM është çaktivizuar përgjithnjë.\n Kontakto ofruesin e shërbimit me valë për një tjetër kartë SIM."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Karta SIM është e kyçur."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Karta SIM është e kyçur me PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Po shkyç kartën SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Zona PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Fjalëkalimi i pajisjes"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Zona PIN e kartës SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Karta SIM e \"<xliff:g id="CARRIER">%1$s</xliff:g>\" tani është e çaktivizuar. Fut kodin PUK për të vazhduar. Kontakto me operatorin për detaje."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Fut kodin PIN të dëshiruar"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Konfirmo kodin e dëshiruar PIN"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Karta SIM po shkyçet…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Po shkyç kartën SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Shkruaj një PIN me 4 deri në 8 numra."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Kodi PUK duhet të jetë me 8 numra ose më shumë."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"E ke shkruar <xliff:g id="NUMBER_0">%1$d</xliff:g> herë gabimisht kodin PIN.\n\nProvo sërish për <xliff:g id="NUMBER_1">%2$d</xliff:g> sekonda."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"E ke shkruar <xliff:g id="NUMBER_0">%1$d</xliff:g> herë gabimisht fjalëkalimin.\n\nProvo sërish për <xliff:g id="NUMBER_1">%2$d</xliff:g> sekonda."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Ke tentuar <xliff:g id="NUMBER_0">%1$d</xliff:g> herë pa sukses për të vizatuar motivin tënd. \n\nProvo sërish për <xliff:g id="NUMBER_1">%2$d</xliff:g> sekonda."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Kodi PIN i kartës SIM është i pasaktë. Tani duhet të kontaktosh me operatorin për ta shkyçur pajisjen tënde."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Kodi PIN i kartës SIM është i pasaktë. Të ka mbetur edhe # përpjekje përpara se të të duhet të kontaktosh me operatorin celular për ta shkyçur pajisjen.}other{Kodi PIN i kartës SIM është i pasaktë. Të kanë mbetur edhe # përpjekje. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Kodi PIN i kartës SIM është i pasaktë. Të kanë mbetur edhe <xliff:g id="NUMBER_1">%d</xliff:g> tentativa.</item>
+      <item quantity="one">Kodi PIN i kartës SIM është i pasaktë. Të ka mbetur edhe <xliff:g id="NUMBER_0">%d</xliff:g> tentativë para se të duhet të kontaktosh me operatorin tënd celular për të shkyçur pajisjen.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Karta SIM është e papërdorshme. Kontakto me operatorin."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Kodi PUK i kartës SIM është i pasaktë. Të ka mbetur edhe # përpjekje përpara se karta SIM të bëhet përgjithmonë e papërdorshme.}other{Kodi PUK i kartës SIM është i pasaktë. Të kanë mbetur edhe # përpjekje përpara se karta SIM të bëhet përgjithmonë e papërdorshme.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">PUK-u i kartës SIM është i pasaktë. Të kanë mbetur edhe <xliff:g id="NUMBER_1">%d</xliff:g> tentativa para se karta SIM të bëhet e papërdorshme përgjithmonë.</item>
+      <item quantity="one">PUK-u i kartës SIM është i pasaktë. Të ka mbetur edhe <xliff:g id="NUMBER_0">%d</xliff:g> tentativë para se karta SIM të bëhet e papërdorshme përgjithmonë.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Operacioni i kodit PIN të kartës SIM dështoi!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Operacioni i kodit PUK të kartës SIM dështoi!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Ndërro metodën e hyrjes"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kërkohet motivi pas rinisjes së pajisjes"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kërkohet kodi PIN pas rinisjes së pajisjes"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kërkohet fjalëkalimi pas rinisjes së pajisjes"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Për më shumë siguri, përdor motivin më mirë"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Për më shumë siguri, përdor kodin PIN më mirë"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Për më shumë siguri, përdor fjalëkalimin më mirë"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kërkohet motivi për më shumë siguri"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kërkohet kodi PIN për më shumë siguri"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kërkohet fjalëkalimi për më shumë siguri"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Pajisja është e kyçur nga administratori"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Pajisja është kyçur manualisht"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nuk njihet"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Për \"Shkyçjen me fytyrë\", aktivizo qasjen te kamera te \"Cilësimet\""</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Fut kodin PIN të kartës SIM. Të ka mbetur edhe # përpjekje përpara se të të duhet të kontaktosh me operatorin celular për ta shkyçur pajisjen.}other{Fut kodin PIN të kartës SIM. Të kanë mbetur edhe # përpjekje.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Karta SIM është çaktivizuar tani. Fut kodin PUK për të vazhduar. Të ka mbetur edhe # përpjekje përpara se karta SIM të bëhet përgjithmonë e papërdorshme. Kontakto me operatorin celular për detajet.}other{Karta SIM tani është çaktivizuar. Fut kodin PUK për të vazhduar. Të kanë mbetur edhe # përpjekje përpara se karta SIM të bëhet përgjithmonë e papërdorshme. Kontakto me operatorin celular për detajet.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Për të përdorur \"Shkyçjen me fytyrë\", aktivizo "<b>"Qasjen te kamera"</b>" te \"Cilësimet\" &gt; \"Privatësia\""</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Fut kodin PIN të kartës SIM. Të kanë mbetur edhe <xliff:g id="NUMBER_1">%d</xliff:g> tentativa.</item>
+      <item quantity="one">Fut kodin PIN të kartës SIM. Të ka mbetur edhe <xliff:g id="NUMBER_0">%d</xliff:g> tentativë para se të duhet të kontaktosh me operatorin tënd celular për ta shkyçur pajisjen.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Karta SIM tani është çaktivizuar. Fut kodin PUK për të vazhduar. Të kanë mbetur edhe <xliff:g id="_NUMBER_1">%d</xliff:g> përpjekje përpara se karta SIM të bëhet përgjithmonë e papërdorshme. Kontakto me operatorin për detaje.</item>
+      <item quantity="one">Karta SIM tani është çaktivizuar. Fut kodin PUK për të vazhduar. Të ka mbetur edhe <xliff:g id="_NUMBER_0">%d</xliff:g> përpjekje përpara se karta SIM të bëhet përgjithmonë e papërdorshme. Kontakto me operatorin për detaje.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"E parazgjedhur"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Flluskë"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoge"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Shkyç pajisjen tënde për të vazhduar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sr/strings.xml b/packages/SystemUI/res-keyguard/values-sr/strings.xml
index f40f9a9..2fb2939 100644
--- a/packages/SystemUI/res-keyguard/values-sr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sr/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Неважећа картица."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Напуњена је"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Бежично пуњење"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Пуни се"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Базна станица за пуњење"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Пуни се"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Брзо се пуни"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Споро се пуни"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Пуњење је оптимизовано да би се заштитила батерија"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Пуњење је привремено ограничено"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Притисните Мени да бисте откључали."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Мрежа је закључана"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Нема SIM-а"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Додајте SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM недостаје или не може да се прочита. Додајте SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Неупотребљив SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM је трајно деактивиран.\n Обратите се добављачу услуге бежичне телефоније да бисте добили други SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM је закључан."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM је закључан PUK-ом."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Откључава се SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Нема SIM картице"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Уметните SIM картицу."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM картица недостаје или не може да се прочита. Уметните SIM картицу."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM картица је неупотребљива."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM картица је трајно онемогућена.\nОбратите се добављачу услуге бежичне мреже да бисте добили другу SIM картицу."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM картица је закључана."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM картица је закључана PUK кодом."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM картица се откључава…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Област за PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Лозинка за уређај"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Област за PIN за SIM"</string>
@@ -61,16 +61,24 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM „<xliff:g id="CARRIER">%1$s</xliff:g>“ је сада онемогућен. Унесите PUK кôд да бисте наставили. Детаљне информације потражите од мобилног оператера."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Унесите жељени PIN кôд"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Потврдите жељени PIN кôд"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Откључава се SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM картица се откључава…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Унесите PIN који има 4–8 бројева."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK кôд треба да има 8 или више бројева."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Унели сте погрешан PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> пута. \n\nПробајте поново за <xliff:g id="NUMBER_1">%2$d</xliff:g> сек."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Унели сте погрешну лозинку <xliff:g id="NUMBER_0">%1$d</xliff:g> пута. \n\nПробајте поново за <xliff:g id="NUMBER_1">%2$d</xliff:g> сек."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Нацртали сте нетачан шаблон за откључавање <xliff:g id="NUMBER_0">%1$d</xliff:g> пута. \n\nПробајте поново за <xliff:g id="NUMBER_1">%2$d</xliff:g> сек."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Нетачан PIN кôд за SIM. Сада морате да контактирате мобилног оператера да бисте откључали уређај."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Нетачан PIN за SIM кôд. Имате још # покушај, а онда морате да се обратите мобилном оператеру да бисте откључали уређај.}one{Нетачан PIN за SIM кôд. Имате још # покушај. }few{Нетачан PIN за SIM кôд. Имате још # покушаја. }other{Нетачан PIN за SIM кôд. Имате још # покушаја. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Нетачан PIN кôд за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушај.</item>
+      <item quantity="few">Нетачан PIN кôд за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушаја.</item>
+      <item quantity="other">Нетачан PIN кôд за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушаја.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM картица је неупотребљива. Контактирајте мобилног оператера."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Нетачан SIM PUK кôд. Имате још # покушај пре него што SIM картица постане трајно неупотребљива.}one{Нетачан PUK кôд за SIM. Имате још # покушај пре него што SIM картица постане трајно неупотребљива.}few{Нетачан PUK кôд за SIM. Имате још # покушаја пре него што SIM картица постане трајно неупотребљива.}other{Нетачан PUK кôд за SIM. Имате још # покушаја пре него што SIM картица постане трајно неупотребљива.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Нетачан PUK кôд за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушај пре него што SIM картица постане трајно неупотребљива.</item>
+      <item quantity="few">Нетачан PUK кôд за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушаја пре него што SIM картица постане трајно неупотребљива.</item>
+      <item quantity="other">Нетачан PUK кôд за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушаја пре него што SIM картица постане трајно неупотребљива.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Радња са PIN кодом за SIM није успела!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Радња са PUK кодом за SIM није успела!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Промени метод уноса"</string>
@@ -78,17 +86,24 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Треба да унесете шаблон када се уређај поново покрене"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Треба да унесете PIN када се уређај поново покрене"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Треба да унесете лозинку када се уређај поново покрене"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За додатну безбедност користите шаблон"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За додатну безбедност користите PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За додатну безбедност користите лозинку"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Треба да унесете шаблон ради додатне безбедности"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Треба да унесете PIN ради додатне безбедности"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Треба да унесете лозинку ради додатне безбедности"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Администратор је закључао уређај"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уређај је ручно закључан"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Није препознат"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Откључавање лицем тражи приступ камери у Подешавањима"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Унесите PIN за SIM. Још # покушај и мораћете да се обратите мобилном оператеру да бисте откључали уређај.}one{Унесите PIN за SIM. Имате још # покушај.}few{Унесите PIN за SIM. Имате још # покушаја.}other{Унесите PIN за SIM. Имате још # покушаја.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још # покушај пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.}one{SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још # покушај пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.}few{SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још # покушаја пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.}other{SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још # покушаја пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Да бисте користили откључавање лицем, укључите "<b>"приступ камери"</b>" у одељку Подешавања &gt; Приватност"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Унесите PIN за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушај.</item>
+      <item quantity="few">Унесите PIN за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушаја.</item>
+      <item quantity="other">Унесите PIN за SIM. Имате још <xliff:g id="NUMBER_1">%d</xliff:g> покушаја.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још <xliff:g id="_NUMBER_1">%d</xliff:g> покушај пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.</item>
+      <item quantity="few">SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још <xliff:g id="_NUMBER_1">%d</xliff:g> покушаја пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.</item>
+      <item quantity="other">SIM је сада онемогућен. Унесите PUK кôд да бисте наставили. Имате још <xliff:g id="_NUMBER_1">%d</xliff:g> покушаја пре него што SIM постане трајно неупотребљив. Детаљне информације потражите од мобилног оператера.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Подразумевани"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Мехурићи"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогни"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Откључајте уређај да бисте наставили"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sv/strings.xml b/packages/SystemUI/res-keyguard/values-sv/strings.xml
index 9d1a511..8e58b90 100644
--- a/packages/SystemUI/res-keyguard/values-sv/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sv/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ogiltigt kort."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Laddat"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddas trådlöst"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddas"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Dockningsstation"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddas"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddas snabbt"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddas långsamt"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddningen har optimerats för att skydda batteriet"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Laddning har begränsats tillfälligt"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Lås upp genom att trycka på Meny."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Nätverk låst"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Inget SIM-kort"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Lägg till ett SIM-kort."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-kort saknas eller går inte att läsa. Lägg till ett SIM-kort."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM-kortet går inte att använda."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Ditt SIM-kort har inaktiverats permanent.\n Kontakta din operatör och be om ett nytt SIM-kort."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-kortet är låst."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-kortet har låsts med PUK-kod."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM-kortet låses upp …"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Inget SIM-kort"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Sätt i ett SIM-kort."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-kort saknas eller kan inte läsas. Sätt i ett SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Oanvändbart SIM-kort."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM-kortet har inaktiverats permanent.\n Beställ ett nytt SIM-kort från din operatör."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-kortet är låst."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-kortet är PUK-låst."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Låser upp SIM-kort …"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Pinkodsområde"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Lösenord för enhet"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Pinkodsområde för SIM-kort"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-kortet för <xliff:g id="CARRIER">%1$s</xliff:g> har inaktiverats. Du måste ange en PUK-kod innan du kan fortsätta. Kontakta operatören för mer information."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Ange önskad pinkod"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Bekräfta önskad pinkod"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM-kortet låses upp …"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Låser upp SIM-kort …"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Ange en pinkod med fyra till åtta siffror."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-koden ska vara minst åtta siffror."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Du har angett fel pinkod <xliff:g id="NUMBER_0">%1$d</xliff:g> gånger. \n\nFörsök igen om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Du har angett fel lösenord <xliff:g id="NUMBER_0">%1$d</xliff:g> gånger. \n\nFörsök igen om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Du har ritat ditt grafiska lösenord fel <xliff:g id="NUMBER_0">%1$d</xliff:g> gånger. \n\nFörsök igen om <xliff:g id="NUMBER_1">%2$d</xliff:g> sekunder."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Du angav fel pinkod för SIM-kortet och måste nu kontakta operatören för att låsa upp enheten."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Du angav fel pinkod för SIM-kortet. # försök återstår innan du måste kontakta operatören för att låsa upp enheten.}other{Du angav fel pinkod för SIM-kortet. # försök återstår. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Du angav fel pinkod för SIM-kortet. <xliff:g id="NUMBER_1">%d</xliff:g> försök återstår.</item>
+      <item quantity="one">Du angav fel pinkod för SIM-kortet. <xliff:g id="NUMBER_0">%d</xliff:g> försök återstår innan du måste kontakta operatören för att låsa upp enheten.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-kortet är obrukbart. Kontakta operatören."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Du angav fel PUK-kod för SIM-kortet. # försök återstår innan SIM-kortet blir obrukbart.}other{Du angav fel PUK-kod för SIM-kortet. # försök återstår innan SIM-kortet blir obrukbart.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Du angav fel PUK-kod för SIM-kortet. <xliff:g id="NUMBER_1">%d</xliff:g> försök återstår innan SIM-kortet blir obrukbart.</item>
+      <item quantity="one">Du angav fel PUK-kod för SIM-kortet. <xliff:g id="NUMBER_0">%d</xliff:g> försök återstår innan SIM-kortet blir obrukbart.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Det gick inte att låsa upp med pinkoden för SIM-kortet."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Det gick inte att låsa upp med PUK-koden för SIM-kortet."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Byt inmatningsmetod"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du måste rita mönster när du har startat om enheten"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du måste ange pinkod när du har startat om enheten"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du måste ange lösenord när du har startat om enheten"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"För ytterligare säkerhet använder du mönstret i stället"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"För ytterligare säkerhet använder du pinkoden i stället"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"För ytterligare säkerhet använder du lösenordet i stället"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du måste rita mönster för ytterligare säkerhet"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du måste ange pinkod för ytterligare säkerhet"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du måste ange lösenord för ytterligare säkerhet"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratören har låst enheten"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten har låsts manuellt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Identifierades inte"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"För ansiktslås aktiverar du kameraåtkomst i Inställn."</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Ange pinkod för SIM-kortet. # försök återstår innan du måste kontakta operatören för att låsa upp enheten.}other{Ange pinkod för SIM-kortet. # försök återstår.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-kortet är inaktiverat. Ange PUK-koden om du vill fortsätta. # försök återstår innan SIM-kortet blir obrukbart. Kontakta operatören för mer information.}other{SIM-kortet är inaktiverat. Ange PUK-koden om du vill fortsätta. # försök återstår innan SIM-kortet blir obrukbart. Kontakta operatören för mer information.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Om du vill använda ansiktslås aktiverar du "<b>"Kameraåtkomst"</b>" i Inställningar &gt; Integritet"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Ange pinkod för SIM-kortet. <xliff:g id="NUMBER_1">%d</xliff:g> försök återstår.</item>
+      <item quantity="one">Ange pinkod för SIM-kortet. <xliff:g id="NUMBER_0">%d</xliff:g> försök återstår innan du måste kontakta operatören för att låsa upp enheten.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM-kortet är inaktiverat. Ange PUK-koden om du vill fortsätta. <xliff:g id="_NUMBER_1">%d</xliff:g> försök återstår innan SIM-kortet blir obrukbart. Kontakta operatören för mer information.</item>
+      <item quantity="one">SIM-kortet är inaktiverat. Ange PUK-koden om du vill fortsätta. <xliff:g id="_NUMBER_0">%d</xliff:g> försök återstår innan SIM-kortet blir obrukbart. Kontakta operatören för mer information.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubbla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lås upp enheten för att fortsätta"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw/strings.xml b/packages/SystemUI/res-keyguard/values-sw/strings.xml
index 8696237..193bb60 100644
--- a/packages/SystemUI/res-keyguard/values-sw/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sw/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Kadi si Sahihi."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Betri imejaa"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Inachaji bila kutumia waya"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Inachaji"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kituo cha Kuchaji"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Inachaji"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Inachaji kwa kasi"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Inachaji pole pole"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hali ya kuchaji imeboreshwa ili kulinda betri"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kuchaji kumedhibitiwa kwa muda"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Bonyeza Menyu ili kufungua."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Mtandao umefungwa"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Hakuna SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Weka SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM haipo au haiwezi kusomwa. Weka SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM haiwezi kutumika."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM kadi yako imefungwa kabisa.\n wasiliana na mtoa huduma wako wa pasi waya ili upate SIM nyingine."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM imefungwa."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM imefungwa kwa PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Inafungua SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Hakuna SIM kadi"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Weka SIM kadi."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM kadi haiko au haisomeki. Weka SIM kadi."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM kadi isiyotumika."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM kadi yako imefungwa kabisa.\n Wasiliana na mtoa huduma za mtandao ili upate SIM kadi nyingine."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kadi imefungwa."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kadi imefungwa kwa PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Inafungua SIM kadi..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Eneo la PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Nenosiri la kifaa"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Eneo la PIN ya SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM ya \"<xliff:g id="CARRIER">%1$s</xliff:g>\" sasa imezimwa. Weka nambari ya PUK ili uendelee. Wasiliana na mtoa huduma kwa maelezo."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Weka nambari ya PIN unayopendelea"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Thibitisha nambari ya PIN unayopendelea"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Inafungua SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Inafungua SIM kadi..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Andika PIN ya tarakimu 4 hadi 8."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Nambari ya PUK inafaa kuwa na tarakimu 8 au zaidi."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Umeandika vibaya PIN mara <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\n Jaribu tena baada ya sekunde <xliff:g id="NUMBER_1">%2$d</xliff:g>."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Umeandika vibaya nenosiri lako mara <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\n Jaribu tena baada ya sekunde <xliff:g id="NUMBER_1">%2$d</xliff:g>."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Umechora vibaya mchoro wako wa kufungua mara <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\n Jaribu tena baada ya sekunde <xliff:g id="NUMBER_1">%2$d</xliff:g>."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Nambari ya PIN ya SIM si sahihi, sasa lazima uwasiliane na mtoa huduma za mtandao ndipo ufungue kifaa chako."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Nambari ya PIN ya SIM si sahihi, unaweza kujaribu mara # kabla ya kulazimika kuwasiliana na mtoa huduma wako ili afungue kifaa chako.}other{Nambari ya PIN ya SIM si sahihi, unaweza kujaribu mara #. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Nambari ya PIN ya SIM si sahihi. Una nafasi zingine <xliff:g id="NUMBER_1">%d</xliff:g> za kujaribu.</item>
+      <item quantity="one">Nambari ya PIN ya SIM si sahihi. Una nafasi zingine <xliff:g id="NUMBER_0">%d</xliff:g> za kujaribu kabla ulazimike kuwasiliana na mtoa huduma wako ili akufungulie kifaa chako.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM haiwezi kutumika. Wasiliana na mtoa huduma wako."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Nambari ya PUK ya SIM si sahihi, unaweza kujaribu mara # kabla ya SIM kuacha kutumika kabisa.}other{Nambari ya PUK ya SIM si sahihi, unaweza kujaribu mara # kabla ya SIM kuacha kutumika kabisa.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Nambari ya PUK ya SIM si sahihi, bado unaweza kujaribu mara <xliff:g id="NUMBER_1">%d</xliff:g> kabla ya SIM kuacha kutumika kabisa.</item>
+      <item quantity="one">Nambari ya PUK ya SIM si sahihi, bado unaweza kujaribu mara <xliff:g id="NUMBER_0">%d</xliff:g> kabla ya SIM kuacha kutumika kabisa.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Utendakazi wa PIN ya SIM haujafanikiwa!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Utendakazi wa PUK ya SIM haujafanikiwa!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Kubadili mbinu ya kuingiza data"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Unafaa kuchora mchoro baada ya kuwasha kifaa upya"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Unafaa kuweka PIN baada ya kuwasha kifaa upya"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Unafaa kuweka nenosiri baada ya kuwasha kifaa upya"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Kwa usalama wa ziada, tumia mchoro badala yake"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Kwa usalama wa ziada, tumia PIN badala yake"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Kwa usalama wa ziada, tumia nenosiri badala yake"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mchoro unahitajika ili kuongeza usalama"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN inahitajika ili kuongeza usalama"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Nenosiri linahitajika ili kuongeza usalama."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Msimamizi amefunga kifaa"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Umefunga kifaa mwenyewe"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Haitambuliwi"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Ili ufungue kwa Uso, ruhusu kamera ifikiwe"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Weka PIN ya SIM. Unaweza kujaribu mara # kuweka PIN ya SIM kabla ya kulazimika kuwasiliana na mtoa huduma wako ili afungue kifaa chako endapo kitafungwa.}other{Weka PIN ya SIM. Unaweza kujaribu mara #.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Sasa SIM imefungwa. Weka msimbo wa PUK ili uendelee. Unaweza kujaribu mara # kabla ya SIM kuacha kutumika kabisa. Wasiliana na mtoa huduma kwa maelezo.}other{Sasa SIM imefungwa. Weka msimbo wa PUK ili uendelee. Unaweza kujaribu mara # kabla ya SIM kuacha kutumika kabisa. Wasiliana na mtoa huduma kwa maelezo.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Ili utumie kipengele cha kufungua kwa uso, washa kipengele cha "<b>"ufikiaji wa Kamera"</b>" katika Mipangilio na Faragha"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Weka PIN ya SIM. Zimesalia mara <xliff:g id="NUMBER_1">%d</xliff:g> za kujaribu.</item>
+      <item quantity="one">Weka PIN ya SIM. Ukijaribu tena mara <xliff:g id="NUMBER_0">%d</xliff:g> bila kufaulu, kifaa chako kitafungwa na utalazimika uwasiliane na mtoa huduma wako ili akifungue.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">Sasa SIM imefungwa. Weka msimbo wa PUK ili uendelee. Umesalia na majaribio <xliff:g id="_NUMBER_1">%d</xliff:g> kabla ya SIM kuacha kufanya kazi kabisa. Wasiliana na mtoa huduma kwa maelezo.</item>
+      <item quantity="one">Sasa SIM imefungwa. Weka msimbo wa PUK ili uendelee. Umesalia na jaribio <xliff:g id="_NUMBER_0">%d</xliff:g> kabla ya SIM kuacha kufanya kazi kabisa. Wasiliana na mtoa huduma kwa maelezo.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Chaguomsingi"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Kiputo"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogi"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Fungua kifaa chako ili uendelee"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml b/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml
deleted file mode 100644
index a3c37e4..0000000
--- a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/any/dimens.xml
-**
-** Copyright 2013, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources>
-    <!-- Height of the sliding KeyguardSecurityContainer
-        (includes 2x keyguard_security_view_top_margin) -->
-    <dimen name="keyguard_security_height">550dp</dimen>
-</resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index a1068c6..6c8db91 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -25,9 +25,6 @@
     <!-- Margin around the various security views -->
     <dimen name="keyguard_security_view_top_margin">12dp</dimen>
 
-    <!-- Padding for the lock icon on the keyguard -->
-    <dimen name="lock_icon_padding">16dp</dimen>
-
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">100dp</dimen>
     <dimen name="widget_label_font_size">18sp</dimen>
diff --git a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml
index 1dc61c5..b7a1bb4 100644
--- a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml
@@ -17,10 +17,5 @@
 */
 -->
 <resources>
-
-    <!-- Height of the sliding KeyguardSecurityContainer
-         (includes 2x keyguard_security_view_top_margin) -->
-    <dimen name="keyguard_security_height">470dp</dimen>
-
     <dimen name="widget_big_font_size">100dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ta/strings.xml b/packages/SystemUI/res-keyguard/values-ta/strings.xml
index cf62c2f..cfa865a 100644
--- a/packages/SystemUI/res-keyguard/values-ta/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ta/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"செல்லாத சிம் கார்டு."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"சார்ஜ் செய்யப்பட்டது"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • வயர்லெஸ் முறையில் சார்ஜாகிறது"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • சார்ஜாகிறது"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • டாக் மூலம் சார்ஜாகிறது"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • சார்ஜாகிறது"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • வேகமாகச் சார்ஜாகிறது"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • மெதுவாகச் சார்ஜாகிறது"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • பேட்டரியைப் பாதுகாக்க சார்ஜிங் மேம்படுத்தப்பட்டுள்ளது"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • சார்ஜிங் தற்காலிகமாக வரம்பிடப்பட்டுள்ளது"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"அன்லாக் செய்ய மெனுவை அழுத்தவும்."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"நெட்வொர்க் பூட்டப்பட்டது"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"சிம் இல்லை"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"சிம்மைச் சேருங்கள்."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"சிம் இல்லை அல்லது படிக்கக்கூடியதாக இல்லை. சிம்மைச் சேருங்கள்."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"பயன்படுத்த முடியாத சிம்."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"உங்கள் சிம் நிரந்தரமாக முடக்கப்பட்டுள்ளது.\n மற்றொரு சிம்மிற்கான உங்கள் வயர்லெஸ் சேவை வழங்குநரைத் தொடர்புகொள்ளுங்கள்."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"சிம் லாக் செய்யப்பட்டுள்ளது."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"சிம் PUK-லாக் செய்யப்பட்டுள்ளது."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"சிம்மை அன்லாக் செய்கிறது…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"சிம் கார்டு இல்லை"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"சிம் கார்டைச் செருகவும்."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"சிம் கார்டு செருகப்படவில்லை அல்லது படிக்கக்கூடியதாக இல்லை. சிம் கார்டைச் செருகவும்."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"பயன்படுத்த முடியாத சிம் கார்டு."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"சிம் கார்டு நிரந்தரமாக முடக்கப்பட்டது.\n வேறு சிம் கார்டைப் பெற, உங்கள் வயர்லெஸ் சேவை வழங்குநரைத் தொடர்புகொள்ளவும்."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"சிம் கார்டு பூட்டப்பட்டுள்ளது."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"சிம் கார்டு PUK ஆல் பூட்டப்பட்டது."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"சிம் கார்டைத் திறக்கிறது…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"பின்னுக்கான பகுதி"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"சாதனத்தின் கடவுச்சொல்"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"சிம் பின்னுக்கான பகுதி"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>\" சிம் தற்போது முடக்கப்பட்டுள்ளது. தொடர, PUK குறியீட்டை உள்ளிடவும். விவரங்களுக்கு, தொலைத்தொடர்பு நிறுவனத்தைத் தொடர்புகொள்ளவும்."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"பின் குறியீட்டை உள்ளிடவும்"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"பின் குறியீட்டை உறுதிப்படுத்தவும்"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"சிம்மை அன்லாக் செய்கிறது…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"சிம் கார்டைத் திறக்கிறது…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 இலிருந்து 8 எண்கள் உள்ள பின்னை உள்ளிடவும்."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK குறியீட்டில் 8 அல்லது அதற்கும் அதிகமான எண்கள் இருக்க வேண்டும்."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"உங்கள் பின்னை <xliff:g id="NUMBER_0">%1$d</xliff:g> முறை தவறாக உள்ளிட்டுவிட்டீர்கள். \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> வினாடிகளில் மீண்டும் முயலவும்."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"உங்கள் கடவுச்சொல்லை <xliff:g id="NUMBER_0">%1$d</xliff:g> முறை தவறாக உள்ளிட்டுவிட்டீர்கள். \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> வினாடிகளில் மீண்டும் முயலவும்."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"அன்லாக் பேட்டர்னை, <xliff:g id="NUMBER_0">%1$d</xliff:g> முறை தவறாக வரைந்துவிட்டீர்கள். \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> வினாடிகளில் மீண்டும் முயலவும்."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"சிம்மின் பின் குறியீடு தவறானது. இனி சாதனத்தை அன்லாக் செய்ய, உங்கள் தொலைத்தொடர்பு நிறுவனத்தைத் தொடர்புகொள்ள வேண்டும்."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{சிம் பின் குறியீடு தவறானது. உங்கள் சாதனத்தை அன்லாக் செய்ய, உங்கள் மொபைல் நிறுவனத்தைத் தொடர்புகொள்வதற்கு முன்பு மேலும் # முறை முயலலாம்.}other{சிம் பின் குறியீடு தவறானது. இன்னும் # முறை மட்டுமே முயலலாம். }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">சிம்மின் பின் குறியீடு தவறானது, இன்னும் நீங்கள் <xliff:g id="NUMBER_1">%d</xliff:g> முறை முயலலாம்.</item>
+      <item quantity="one">சிம்மின் பின் குறியீடு தவறானது, மேலும் <xliff:g id="NUMBER_0">%d</xliff:g> முயற்சிகளுக்குப் பின்னர், உங்கள் தொலைத்தொடர்பு நிறுவனத்தைத் தொடர்பு கொண்டு மட்டுமே சாதனத்தை அன்லாக் செய்ய முடியும்.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"பயன்படுத்த முடியாத சிம். உங்கள் தொலைத்தொடர்பு நிறுவனத்தைத் தொடர்புகொள்ளவும்."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{சிம் PUK குறியீடு தவறானது. சிம் நிரந்தரமாகப் பயன்படுத்த முடியாமல் போவதற்கு முன்பு நீங்கள் # முறை முயலலாம்.}other{சிம் PUK குறியீடு தவறானது. சிம் நிரந்தரமாகப் பயன்படுத்த முடியாமல் போவதற்கு முன்பு நீங்கள் # முறை முயலலாம்.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">சிம்மின் PUK குறியீடு தவறானது, மேலும் <xliff:g id="NUMBER_1">%d</xliff:g> முறை முயலலாம். தொடர்ந்து தவறான குறியீடு உள்ளிடப்பட்டால், அதன் பிறகு சிம்மை நிரந்தரமாகப் பயன்படுத்த முடியாமல் போகும்.</item>
+      <item quantity="one">சிம்மின் PUK குறியீடு தவறானது, மேலும் <xliff:g id="NUMBER_0">%d</xliff:g> முறை முயலலாம். தொடர்ந்து தவறான குறியீடு உள்ளிடப்பட்டால், அதன் பிறகு சிம்மை நிரந்தரமாகப் பயன்படுத்த முடியாமல் போகும்.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"சிம் பின் செயல்பாடு தோல்வியடைந்தது!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"சிம் PUK செயல்பாடு தோல்வியடைந்தது!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"உள்ளீட்டு முறையை மாற்றும்"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"சாதனத்தை மீண்டும் தொடங்கியதும், பேட்டர்னை வரைய வேண்டும்"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"சாதனத்தை மீண்டும் தொடங்கியதும், பின்னை உள்ளிட வேண்டும்"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"சாதனத்தை மீண்டும் தொடங்கியதும், கடவுச்சொல்லை உள்ளிட வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"கூடுதல் பாதுகாப்பிற்குப் பேட்டர்னைப் பயன்படுத்தவும்"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"கூடுதல் பாதுகாப்பிற்குப் பின்னை (PIN) பயன்படுத்தவும்"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"கூடுதல் பாதுகாப்பிற்குக் கடவுச்சொல்லைப் பயன்படுத்தவும்"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"கூடுதல் பாதுகாப்பிற்கு, பேட்டர்னை வரைய வேண்டும்"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"கூடுதல் பாதுகாப்பிற்கு, பின்னை உள்ளிட வேண்டும்"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"கூடுதல் பாதுகாப்பிற்கு, கடவுச்சொல்லை உள்ளிட வேண்டும்"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"நிர்வாகி சாதனத்தைப் பூட்டியுள்ளார்"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"பயனர் சாதனத்தைப் பூட்டியுள்ளார்"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"அடையாளங்காணபடவில்லை"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"முகம் காட்டித் திறத்தல் அம்சத்தைப் பயன்படுத்த, அமைப்புகளில் கேமரா அணுகலை இயக்கவும்"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{சிம் பின்னை உள்ளிடவும். உங்கள் சாதனத்தை அன்லாக் செய்ய, உங்கள் மொபைல் நிறுவனத்தைத் தொடர்புகொள்வதற்கு முன்பு மேலும் # முறை முயலலாம்.}other{சிம் பின்னை உள்ளிடவும். இன்னும் # முறை மட்டுமே முயல முடியும்.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{சிம் இப்போது முடக்கப்பட்டுள்ளது. தொடர்வதற்கு PUK குறியீட்டை உள்ளிடவும். நீங்கள் # முறை மட்டுமே முயற்சிக்க முடியும். அதன்பிறகு சிம் நிரந்தரமாக முடக்கப்படும். விவரங்களுக்கு மொபைல் நிறுவனத்தைத் தொடர்புகொள்ளவும்.}other{சிம் இப்போது முடக்கப்பட்டுள்ளது. தொடர்வதற்கு PUK குறியீட்டை உள்ளிடவும். நீங்கள் # முறை மட்டுமே முயற்சிக்க முடியும். அதன்பிறகு சிம் நிரந்தரமாக முடக்கப்படும். விவரங்களுக்கு மொபைல் நிறுவனத்தைத் தொடர்புகொள்ளவும்.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"முகம் காட்டித் திறத்தல் அம்சத்தைப் பயன்படுத்த, அமைப்புகள் &gt; தனியுரிமை என்பதற்குச் சென்று "<b>"கேமரா அணுகலை"</b>" இயக்கவும்"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">சிம் பின்னை உள்ளிடவும். மேலும், <xliff:g id="NUMBER_1">%d</xliff:g> வாய்ப்புகள் மீதமுள்ளன.</item>
+      <item quantity="one">சிம் பின்னை உள்ளிடவும். மீதமுள்ள <xliff:g id="NUMBER_0">%d</xliff:g> வாய்ப்பில் தவறுதலான பின் உள்ளிடப்பட்டால், உங்கள் தொலைத்தொடர்பு நிறுவனத்தைத் தொடர்பு கொண்டு மட்டுமே சாதனத்தை அன்லாக் செய்ய முடியும்.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">சிம் தற்போது முடக்கப்பட்டுள்ளது. தொடர்வதற்கு, PUK குறியீட்டை உள்ளிடவும். நீங்கள் <xliff:g id="_NUMBER_1">%d</xliff:g> முறை மட்டுமே முயற்சிக்க முடியும். அதன்பிறகு சிம் நிரந்தரமாக முடக்கப்படும். விவரங்களுக்கு, மொபைல் நிறுவனத்தைத் தொடர்புகொள்ளவும்.</item>
+      <item quantity="one">சிம் தற்போது முடக்கப்பட்டுள்ளது. தொடர்வதற்கு, PUK குறியீட்டை உள்ளிடவும். நீங்கள் <xliff:g id="_NUMBER_0">%d</xliff:g> முறை மட்டுமே முயற்சிக்க முடியும். அதன்பிறகு சிம் நிரந்தரமாக முடக்கப்படும். விவரங்களுக்கு, மொபைல் நிறுவனத்தைத் தொடர்புகொள்ளவும்.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"இயல்பு"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"பபிள்"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"அனலாக்"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"தொடர, சாதனத்தை அன்லாக் செய்யுங்கள்"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-te/strings.xml b/packages/SystemUI/res-keyguard/values-te/strings.xml
index 8b21761..54ff7ad 100644
--- a/packages/SystemUI/res-keyguard/values-te/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-te/strings.xml
@@ -22,25 +22,25 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"మీ పిన్‌ని నమోదు చేయండి"</string>
     <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"మీ నమూనాను నమోదు చేయండి"</string>
-    <string name="keyguard_enter_your_password" msgid="7225626204122735501">"మీ పాస్‌వర్డ్‌ను ఎంటర్ చేయండి"</string>
+    <string name="keyguard_enter_your_password" msgid="7225626204122735501">"మీ పాస్‌వర్డ్‌ను నమోదు చేయండి"</string>
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"చెల్లని కార్డ్."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ఛార్జ్ చేయబడింది"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • వైర్‌ లేకుండా ఛార్జ్ అవుతోంది"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ఛార్జ్ అవుతోంది"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ఛార్జింగ్ డాక్"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ఛార్జ్ అవుతోంది"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • వేగంగా ఛార్జ్ అవుతోంది"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • నెమ్మదిగా ఛార్జ్ అవుతోంది"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • బ్యాటరీని రక్షించడానికి ఛార్జింగ్ ఆప్టిమైజ్ చేయబడింది"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ఛార్జింగ్ తాత్కాలికంగా పరిమితం చేయబడింది"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"అన్‌లాక్ చేయడానికి మెనూను నొక్కండి."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"నెట్‌వర్క్ లాక్ చేయబడింది"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM లేదు"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIMను జోడించండి."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM మిస్ అయ్యింది లేదా ఆమోదయోగ్యం కాదు. SIMను జోడించండి."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"వినియోగించలేని SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"మీ SIM శాశ్వతంగా డీయాక్టివేట్ చేయబడింది.\n మరో SIMను పొందడం కోసం మీ వైర్‌లెస్ సర్వీస్ ప్రొవైడర్‌ను సంప్రదించండి."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM లాక్ చేయబడింది."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM PUK లాక్ చేయబడింది."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIMను అన్‌లాక్ చేస్తోంది…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM కార్డ్ లేదు"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM కార్డ్‌ని చొప్పించండి."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM కార్డ్ లేదు లేదా ఆమోదయోగ్యం కాదు. SIM కార్డ్‌ని చొప్పించండి."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM కార్డ్ నిరుపయోగకరంగా మారింది."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"మీ SIM కార్డ్ శాశ్వతంగా నిలిపివేయబడింది.\n మరో SIM కార్డ్‌ని పొందడం కోసం మీ వైర్‌లెస్ సేవా ప్రదాతను సంప్రదించండి."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM కార్డ్ లాక్ చేయబడింది."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM కార్డ్ PUK-లాక్ చేయబడింది."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM కార్డ్‌ని అన్‌లాక్ చేస్తోంది…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"పిన్ ప్రాంతం"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"పరికరం పాస్‌వర్డ్‌"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM పిన్ ప్రాంతం"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ఇప్పుడు SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\"ని నిలిపివేయడం జరిగింది. కొనసాగించాలంటే, PUK కోడ్‌ను నమోదు చేయండి. వివరాల కోసం క్యారియర్‌ను సంప్రదించండి."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"కావల్సిన పిన్ కోడ్‌ను నమోదు చేయండి"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"కావల్సిన పిన్ కోడ్‌ను నిర్ధారించండి"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIMను అన్‌లాక్ చేస్తోంది…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM కార్డ్‌ని అన్‌లాక్ చేస్తోంది…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 నుండి 8 సంఖ్యలు ఉండే పిన్‌ను టైప్ చేయండి."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK కోడ్ అనేది 8 లేదా అంతకంటే ఎక్కువ సంఖ్యలు ఉండాలి."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"మీరు మీ పిన్‌ను <xliff:g id="NUMBER_0">%1$d</xliff:g> సార్లు తప్పుగా టైప్ చేశారు. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> సెకన్లలో మళ్లీ ప్రయత్నించండి."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"మీరు మీ పాస్‌వర్డ్‌ను <xliff:g id="NUMBER_0">%1$d</xliff:g> సార్లు తప్పుగా టైప్ చేశారు. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> సెకన్లలో మళ్లీ ప్రయత్నించండి."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"మీరు మీ అన్‌లాక్ నమూనాను <xliff:g id="NUMBER_0">%1$d</xliff:g> సార్లు తప్పుగా గీసారు. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> సెకన్లలో మళ్లీ ప్రయత్నించండి."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM పిన్ కోడ్ తప్పు, ఇప్పుడు మీ డివైజ్‌ను అన్‌లాక్ చేయాలంటే, మీరు తప్పనిసరిగా మీ క్యారియర్‌ను సంప్రదించాలి."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{తప్పు SIM PIN కోడ్, మీరు మీ డివైజ్‌ను అన్‌లాక్ చేయడానికి మీరు తప్పనిసరిగా మీ క్యారియర్‌ను కాంటాక్ట్ చేయడానికి ముందు మీకు # ప్రయత్నం మిగిలి ఉంది.}other{తప్పు SIM PIN కోడ్, మీకు # ప్రయత్నాలు మిగిలి ఉన్నాయి. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM పిన్ కోడ్ తప్పు, మీకు మరో <xliff:g id="NUMBER_1">%d</xliff:g> ప్రయత్నాలు మిగిలి ఉన్నాయి.</item>
+      <item quantity="one">SIM పిన్ కోడ్ తప్పు, మీకు మరో <xliff:g id="NUMBER_0">%d</xliff:g> ప్రయత్నం మిగిలి ఉంది, ఆ తర్వాత మీ డివైజ్‌ను అన్‌లాక్ చేయాలంటే, మీరు తప్పనిసరిగా మీ క్యారియర్‌ని సంప్రదించాలి.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM నిరుపయోగకరంగా మారింది. మీ క్యారియర్‌ను సంప్రదించండి."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{తప్పు SIM PUK కోడ్, SIM శాశ్వతంగా నిరుపయోగం కాకుండా ఉండటానికి మీకు # ప్రయత్నం మిగిలి ఉంది.}other{తప్పు SIM PUK కోడ్, SIM శాశ్వతంగా నిరుపయోగం కాకుండా ఉండటానికి మీకు # ప్రయత్నాలు మిగిలి ఉన్నాయి.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM PUK కోడ్ తప్పు, మీకు మరో <xliff:g id="NUMBER_1">%d</xliff:g> ప్రయత్నాలు మిగిలి ఉన్నాయి, ఆ తర్వాత SIM శాశ్వతంగా నిరుపయోగకరంగా మారుతుంది.</item>
+      <item quantity="one">SIM PUK కోడ్ తప్పు, మీకు మరో <xliff:g id="NUMBER_0">%d</xliff:g> ప్రయత్నం మిగిలి ఉంది, ఆ తర్వాత SIM శాశ్వతంగా నిరుపయోగకరంగా మారుతుంది.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM పిన్ చర్య విఫలమైంది!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK చర్య విఫలమైంది!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ఇన్‌పుట్ పద్ధతిని మార్చు"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత నమూనాను గీయాలి"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"డివైజ్‌ను పునఃప్రారంభించిన తర్వాత పిన్ నమోదు చేయాలి"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత పాస్‌వర్డ్‌ను నమోదు చేయాలి"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"అదనపు సెక్యూరిటీ కోసం, బదులుగా ఆకృతిని ఉపయోగించండి"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"అదనపు సెక్యూరిటీ కోసం, బదులుగా PINను ఉపయోగించండి"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"అదనపు సెక్యూరిటీ కోసం, బదులుగా పాస్‌వర్డ్‌ను ఉపయోగించండి"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"అదనపు సెక్యూరిటీ కోసం ఆకృతి అవసరం"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"అదనపు సెక్యూరిటీ కోసం పిన్ ఎంటర్ చేయాలి"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"అదనపు సెక్యూరిటీ కోసం పాస్‌వర్డ్‌ను ఎంటర్ చేయాలి"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"పరికరం నిర్వాహకుల ద్వారా లాక్ చేయబడింది"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"పరికరం మాన్యువల్‌గా లాక్ చేయబడింది"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"గుర్తించలేదు"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"ఫేస్ అన్‌లాక్ వాడేందుకు కెమెరా యాక్సెస్ ఆన్‌లో ఉండాలి"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PINను ఎంటర్ చేయండి. మీరు మీ పరికరాన్ని అన్‌లాక్ చేయడానికి మీరు తప్పనిసరిగా మీ క్యారియర్‌ను కాంటాక్ట్ చేయడానికి ముందు మీకు # ప్రయత్నం మిగిలి ఉంది.}other{SIM PINను ఎంటర్ చేయండి. మీకు # ప్రయత్నాలు మిగిలి ఉన్నాయి.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM ఇప్పుడు డిజేబుల్ చేయబడింది. కొనసాగించడానికి PUK కోడ్‌ను ఎంటర్ చేయండి. SIM శాశ్వతంగా నిరుపయోగం కాకుండా ఉండటానికి మీకు # ప్రయత్నం మిగిలి ఉంది. వివరాల కోసం క్యారియర్‌ను కాంటాక్ట్ చేయండి.}other{SIM ఇప్పుడు డిజేబుల్ చేయబడింది. కొనసాగించడానికి PUK కోడ్‌ను ఎంటర్ చేయండి. SIM శాశ్వతంగా నిరుపయోగం కాకుండా ఉండటానికి మీకు # ప్రయత్నాలు మిగిలి ఉన్నాయి. వివరాల కోసం క్యారియర్‌ను కాంటాక్ట్ చేయండి.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"ఫేస్ అన్‌లాక్‌ను ఉపయోగించడానికి, సెట్టింగ్‌లు &gt; గోప్యతలో "<b>"కెమెరా యాక్సెస్"</b>"ను ఆన్ చేయండి"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM పిన్‌ని నమోదు చేయండి. మీకు <xliff:g id="NUMBER_1">%d</xliff:g> ప్రయత్నలు మిగిలి ఉన్నాయి.</item>
+      <item quantity="one">SIM పిన్‌ని నమోదు చేయండి, మీరు మీ పరికరాన్ని అన్‌లాక్ చేయడానికి తప్పనిసరిగా మీ క్యారియర్‌ను సంప్రదించడానికి ముందు మీకు <xliff:g id="NUMBER_0">%d</xliff:g> ప్రయత్నం మిగిలి ఉంది.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM ఇప్పుడు నిలిపివేయబడింది. PUK కోడ్‌ను నమోదు చేయండి. SIM శాశ్వతంగా నిరుపయోగం కాకుండా ఉండటానికి మీకు <xliff:g id="_NUMBER_1">%d</xliff:g> ప్రయత్నాలు మిగిలి ఉన్నాయి. వివరాల కోసం కారియర్‌ను సంప్రదించండి.</item>
+      <item quantity="one">SIM ఇప్పుడు నిలిపివేయబడింది. PUK కోడ్‌ను నమోదు చేయండి. SIM శాశ్వతంగా నిరుపయోగం కాకుండా ఉండటానికి మీకు <xliff:g id="_NUMBER_0">%d</xliff:g> ప్రయత్నం మిగిలి ఉంది వివరాల కోసం కారియర్‌ను సంప్రదించండి.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ఆటోమేటిక్"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"బబుల్"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ఎనలాగ్"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"కొనసాగించడానికి మీ పరికరాన్ని అన్‌లాక్ చేయండి"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-th/strings.xml b/packages/SystemUI/res-keyguard/values-th/strings.xml
index 688ccdb..10bdd0c 100644
--- a/packages/SystemUI/res-keyguard/values-th/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-th/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"การ์ดไม่ถูกต้อง"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"ชาร์จแล้ว"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • กำลังชาร์จแบบไร้สาย"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • กำลังชาร์จ"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • กำลังชาร์จบนแท่นชาร์จ"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • กำลังชาร์จ"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • กำลังชาร์จอย่างเร็ว"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • กำลังชาร์จอย่างช้าๆ"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • ปรับการชาร์จให้เหมาะสมเพื่อถนอมแบตเตอรี่"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • จำกัดการชาร์จชั่วคราว"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"กด \"เมนู\" เพื่อปลดล็อก"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"เครือข่ายถูกล็อก"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"ไม่มี SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"โปรดใส่ SIM"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"ไม่มี SIM หรืออ่านไม่ได้ โปรดใส่ SIM"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM ใช้งานไม่ได้"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"ปิดใช้งาน SIM อย่างถาวรแล้ว\n ติดต่อผู้ให้บริการไร้สายของคุณเพื่อรับ SIM ใหม่"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM ถูกล็อก"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM ถูกล็อกด้วย PUK"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"กำลังปลดล็อก SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"ไม่มีซิมการ์ด"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"ใส่ซิมการ์ด"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"ไม่มีซิมการ์ดหรือไม่สามารถอ่านได้ โปรดใส่ซิมการ์ด"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"ซิมการ์ดใช้ไม่ได้"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"ซิมการ์ดถูกปิดใช้อย่างถาวร\nติดต่อผู้ให้บริการระบบไร้สายของคุณเพื่อขอซิมการ์ดใหม่"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"ซิมการ์ดถูกล็อก"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"ซิมการ์ดถูกล็อกด้วย PUK"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"กำลังปลดล็อกซิมการ์ด…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"พื้นที่ PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"รหัสผ่านของอุปกรณ์"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"พื้นที่ PIN ของซิม"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"ปิดใช้ซิม \"<xliff:g id="CARRIER">%1$s</xliff:g>\" แล้ว ป้อนรหัส PUK เพื่อดำเนินการต่อ โปรดสอบถามรายละเอียดจากผู้ให้บริการ"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"ป้อนรหัส PIN ที่ต้องการ"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"ยืนยันรหัส PIN ที่ต้องการ"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"กำลังปลดล็อก SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"กำลังปลดล็อกซิมการ์ด…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"พิมพ์ PIN ซึ่งเป็นเลข 4-8 หลัก"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"รหัส PUK ต้องเป็นตัวเลขอย่างน้อย 8 หลัก"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"คุณพิมพ์ PIN ไม่ถูกต้อง <xliff:g id="NUMBER_0">%1$d</xliff:g> ครั้งแล้ว \n\nโปรดลองอีกครั้งใน <xliff:g id="NUMBER_1">%2$d</xliff:g> วินาที"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"คุณพิมพ์รหัสผ่านไม่ถูกต้อง <xliff:g id="NUMBER_0">%1$d</xliff:g> ครั้งแล้ว \n\nโปรดลองอีกครั้งใน <xliff:g id="NUMBER_1">%2$d</xliff:g> วินาที"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"คุณวาดรูปแบบการปลดล็อกไม่ถูกต้อง <xliff:g id="NUMBER_0">%1$d</xliff:g> ครั้งแล้ว \n\nโปรดลองอีกครั้งในอีก <xliff:g id="NUMBER_1">%2$d</xliff:g> วินาที"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"รหัส PIN ของซิมไม่ถูกต้อง ตอนนี้คุณต้องติดต่อผู้ให้บริการเพื่อปลดล็อกอุปกรณ์ของคุณ"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{รหัส PIN ของซิมไม่ถูกต้อง คุณพยายามได้อีก # ครั้งก่อนที่จะต้องติดต่อผู้ให้บริการเพื่อปลดล็อกอุปกรณ์}other{รหัส PIN ของซิมไม่ถูกต้อง คุณพยายามได้อีก # ครั้ง }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">รหัส PIN ของซิมไม่ถูกต้อง คุณพยายามได้อีก <xliff:g id="NUMBER_1">%d</xliff:g> ครั้ง</item>
+      <item quantity="one">รหัส PIN ของซิมไม่ถูกต้อง คุณพยายามได้อีก <xliff:g id="NUMBER_0">%d</xliff:g> ครั้งก่อนที่จะต้องติดต่อผู้ให้บริการเพื่อปลดล็อกอุปกรณ์ของคุณ</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ซิมไม่สามารถใช้งานได้ โปรดติดต่อผู้ให้บริการ"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{รหัส PUK ของซิมไม่ถูกต้อง คุณพยายามได้อีก # ครั้งก่อนที่ซิมจะใช้งานไม่ได้อย่างถาวร}other{รหัส PUK ของซิมไม่ถูกต้อง คุณพยายามได้อีก # ครั้งก่อนที่ซิมจะใช้งานไม่ได้อย่างถาวร}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">รหัส PUK ของซิมไม่ถูกต้อง คุณพยายามได้อีก <xliff:g id="NUMBER_1">%d</xliff:g> ครั้งก่อนที่ซิมจะไม่สามารถใช้งานได้อย่างถาวร</item>
+      <item quantity="one">รหัส PUK ของซิมไม่ถูกต้อง คุณพยายามได้อีก <xliff:g id="NUMBER_0">%d</xliff:g> ครั้งก่อนที่ซิมจะไม่สามารถใช้งานได้อย่างถาวร</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"การปลดล็อกด้วย PIN ของซิมล้มเหลว!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"การปลดล็อกด้วย PUK ของซิมล้มเหลว!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"สลับวิธีการป้อนข้อมูล"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ต้องวาดรูปแบบหลังจากอุปกรณ์รีสตาร์ท"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ต้องระบุ PIN หลังจากอุปกรณ์รีสตาร์ท"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ต้องป้อนรหัสผ่านหลังจากอุปกรณ์รีสตาร์ท"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ใช้รูปแบบแทนเพื่อเพิ่มความปลอดภัย"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ใช้ PIN แทนเพื่อเพิ่มความปลอดภัย"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ใช้รหัสผ่านแทนเพื่อเพิ่มความปลอดภัย"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ต้องวาดรูปแบบเพื่อความปลอดภัยเพิ่มเติม"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ต้องระบุ PIN เพื่อความปลอดภัยเพิ่มเติม"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ต้องป้อนรหัสผ่านเพื่อความปลอดภัยเพิ่มเติม"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ผู้ดูแลระบบล็อกอุปกรณ์"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"มีการล็อกอุปกรณ์ด้วยตัวเอง"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ไม่รู้จัก"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"เปิดการเข้าถึงกล้องในการตั้งค่าเพื่อใช้การปลดล็อกด้วยใบหน้า"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{ป้อน PIN ของซิม คุณพยายามได้อีก # ครั้งก่อนที่จะต้องติดต่อผู้ให้บริการเพื่อปลดล็อกอุปกรณ์}other{ป้อน PIN ของซิม คุณลองได้อีก # ครั้ง}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{ตอนนี้ซิมถูกปิดใช้แล้ว ป้อนรหัส PUK เพื่อดำเนินการต่อ คุณพยายามได้อีก # ครั้งก่อนที่ซิมจะใช้งานไม่ได้อย่างถาวร โปรดติดต่อสอบถามรายละเอียดจากผู้ให้บริการ}other{ตอนนี้ซิมถูกปิดใช้แล้ว ป้อนรหัส PUK เพื่อดำเนินการต่อ คุณพยายามได้อีก # ครั้งก่อนที่ซิมจะใช้งานไม่ได้อย่างถาวร โปรดติดต่อสอบถามรายละเอียดจากผู้ให้บริการ}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"หากต้องการใช้ปลดล็อกด้วยใบหน้า ให้เปิด"<b>"การเข้าถึงกล้อง"</b>"ในการตั้งค่าและความเป็นส่วนตัว"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">โปรดป้อน PIN ของซิม คุณพยายามได้อีก <xliff:g id="NUMBER_1">%d</xliff:g> ครั้ง</item>
+      <item quantity="one">โปรดป้อน PIN ของซิม คุณพยายามได้อีก <xliff:g id="NUMBER_0">%d</xliff:g> ครั้งก่อนที่จะต้องติดต่อผู้ให้บริการเพื่อปลดล็อกอุปกรณ์</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">ซิมถูกปิดใช้งานในขณะนี้ โปรดป้อนรหัส PUK เพื่อทำต่อ คุณพยายามได้อีก <xliff:g id="_NUMBER_1">%d</xliff:g> ครั้งก่อนที่ซิมจะไม่สามารถใช้งานได้อย่างถาวร โปรดติดต่อสอบถามรายละเอียดจากผู้ให้บริการ</item>
+      <item quantity="one">ซิมถูกปิดใช้งานในขณะนี้ โปรดป้อนรหัส PUK เพื่อทำต่อ คุณพยายามได้อีก <xliff:g id="_NUMBER_0">%d</xliff:g> ครั้งก่อนที่ซิมจะไม่สามารถใช้งานได้อย่างถาวร โปรดติดต่อสอบถามรายละเอียดจากผู้ให้บริการ</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ค่าเริ่มต้น"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"บับเบิล"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"แอนะล็อก"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ปลดล็อกอุปกรณ์ของคุณเพื่อดำเนินการต่อ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-tl/strings.xml b/packages/SystemUI/res-keyguard/values-tl/strings.xml
index 158570b..85fcb7a 100644
--- a/packages/SystemUI/res-keyguard/values-tl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-tl/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Di-wasto ang Card."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Tapos nang mag-charge"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Wireless na nagcha-charge"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nagcha-charge"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging Dock"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Nagcha-charge"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mabilis na nagcha-charge"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Mabagal na nagcha-charge"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Naka-optimize ang pag-charge para protektahan ang baterya"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Pansamantalang limitado ang pag-charge"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Pindutin ang Menu upang i-unlock."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Naka-lock ang network"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Walang SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Magdagdag ng SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Wala o hindi nababasa ang SIM. Magdagdag ng SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Hindi magagamit na SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"Permanenteng na-deactivate ang iyong SIM.\n Makipag-ugnayan sa iyong service provider ng wireless para sa isa pang SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"Naka-lock ang SIM."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"Naka-PUK lock ang SIM."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Ina-unlock ang SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Walang SIM card"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Maglagay ng SIM card."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Wala o hindi nababasa ang SIM card. Maglagay ng SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Hindi na magagamit na SIM card."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Permanenteng na-disable ang iyong SIM card.\n Makipag-ugnayan sa iyong wireless service provider para sa isa pang SIM card."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Naka-lock ang SIM card."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Naka-PUK-lock ang SIM card."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Ina-unlock ang SIM card…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Lugar ng PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Password ng device"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Lugar ng PIN ng SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"Naka-disable na ngayon ang SIM na \"<xliff:g id="CARRIER">%1$s</xliff:g>.\" Ilagay ang PUK code upang magpatuloy. Makipag-ugnayan sa carrier para sa mga detalye."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Ilagay ang gustong PIN code"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Kumpirmahin ang gustong PIN code"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Ina-unlock ang SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Ina-unlock ang SIM card…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Mag-type ng PIN na 4 hanggang 8 numero."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Dapat ay 8 numero o higit pa ang PUK code."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Na-type mo nang mali ang iyong PIN nang <xliff:g id="NUMBER_0">%1$d</xliff:g> (na) beses. \n\nSubukang muli sa loob ng <xliff:g id="NUMBER_1">%2$d</xliff:g> (na) segundo."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Na-type mo nang hindi tama ang iyong password nang <xliff:g id="NUMBER_0">%1$d</xliff:g> (na) beses. \n\nSubukang muli sa loob ng <xliff:g id="NUMBER_1">%2$d</xliff:g> (na) segundo."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Naguhit mo nang hindi tama ang iyong pattern sa pag-unlock nang <xliff:g id="NUMBER_0">%1$d</xliff:g> (na) beses. \n\nSubukang muli sa loob ng <xliff:g id="NUMBER_1">%2$d</xliff:g> (na) segundo."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Mali ang PIN code ng SIM, dapat ka nang makipag-ugnayan sa iyong carrier upang i-unlock ang iyong device."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Mali ang PIN code ng SIM, mayroon kang # natitirang pagsubok bago mo kailanganing makipag-ugnayan sa iyong carrier para ma-unlock ang device mo.}one{Mali ang PIN code ng SIM, mayroon kang # natitirang pagsubok. }other{Mali ang PIN code ng SIM, mayroon kang # na natitirang pagsubok. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Mali ang PIN code ng SIM, mayroon kang <xliff:g id="NUMBER_1">%d</xliff:g> natitirang pagsubok.</item>
+      <item quantity="other">Mali ang PIN code ng SIM, mayroon kang <xliff:g id="NUMBER_1">%d</xliff:g> na natitirang pagsubok.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"Hindi magagamit ang SIM. Makipag-ugnayan sa iyong carrier."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Mali ang PUK code ng SIM, mayroon kang # natitirang pagsubok bago tuluyang hindi magamit ang SIM.}one{Mali ang PUK code ng SIM, mayroon kang # natitirang pagsubok bago tuluyang hindi magamit ang SIM.}other{Mali ang PUK code ng SIM, mayroon kang # na natitirang pagsubok bago tuluyang hindi magamit ang SIM.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Mali ang PUK code ng SIM, mayroon kang <xliff:g id="NUMBER_1">%d</xliff:g> natitirang pagsubok bago tuluyang hindi magamit ang SIM.</item>
+      <item quantity="other">Mali ang PUK code ng SIM, mayroon kang <xliff:g id="NUMBER_1">%d</xliff:g> na natitirang pagsubok bago tuluyang hindi magamit ang SIM.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Nabigo ang operasyon ng PIN ng SIM!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Nabigo ang operasyon ng PUK ng SIM!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Magpalit ng pamamaraan ng pag-input"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kailangan ng pattern pagkatapos mag-restart ng device"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kailangan ng PIN pagkatapos mag-restart ng device"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kailangan ng password pagkatapos mag-restart ng device"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para sa karagdagang seguridad, gumamit na lang ng pattern"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para sa karagdagang seguridad, gumamit na lang ng PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para sa karagdagang seguridad, gumamit na lang ng password"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kinakailangan ang pattern para sa karagdagang seguridad"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kinakailangan ang PIN para sa karagdagang seguridad"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kinakailangan ang password para sa karagdagang seguridad"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Na-lock ng admin ang device"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Manual na na-lock ang device"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Hindi nakilala"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Para sa Pag-unlock Gamit ang Mukha, i-on ang access ng camera sa Mga Setting"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Ilagay ang PIN ng SIM. Mayroon kang # natitirang pagsubok bago mo kailanganing makipag-ugnayan sa iyong carrier para ma-unlock ang device mo.}one{Ilagay ang PIN ng SIM. Mayroon kang # natitirang pagsubok.}other{Ilagay ang PIN ng SIM. Mayroon kang # na natitirang pagsubok.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Na-disable na ang SIM. Ilagay ang PUK code para magpatuloy. Mayroon kang # natitirang pagsubok bago tuluyang hindi magamit ang SIM. Makipag-ugnayan sa carrier para sa mga detalye.}one{Na-disable na ang SIM. Ilagay ang PUK code para magpatuloy. Mayroon kang # natitirang pagsubok bago tuluyang hindi magamit ang SIM. Makipag-ugnayan sa carrier para sa mga detalye.}other{Na-disable na ang SIM. Ilagay ang PUK code para magpatuloy. Mayroon kang # na natitirang pagsubok bago tuluyang hindi magamit ang SIM. Makipag-ugnayan sa carrier para sa mga detalye.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Para magamit ang Pag-unlock Gamit ang Mukha, i-on ang "<b>"Access sa camera"</b>" sa Mga Setting &gt; Privacy"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Ilagay ang PIN ng SIM. Mayroon kang <xliff:g id="NUMBER_1">%d</xliff:g> natitirang pagsubok.</item>
+      <item quantity="other">Ilagay ang PIN ng SIM. Mayroon kang <xliff:g id="NUMBER_1">%d</xliff:g> na natitirang pagsubok.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">Naka-disable na ang SIM. Ilagay ang PUK code upang magpatuloy. Mayroon kang <xliff:g id="_NUMBER_1">%d</xliff:g> natitirang pagsubok bago tuluyang hindi magamit ang SIM. Makipag-ugnayan sa carrier para sa mga detalye.</item>
+      <item quantity="other">Naka-disable na ang SIM. Ilagay ang PUK code upang magpatuloy. Mayroon kang <xliff:g id="_NUMBER_1">%d</xliff:g> na natitirang pagsubok bago tuluyang hindi magamit ang SIM. Makipag-ugnayan sa carrier para sa mga detalye.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"I-unlock ang iyong device para magpatuloy"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-tr/strings.xml b/packages/SystemUI/res-keyguard/values-tr/strings.xml
index 0e5a035..e9c3d5a 100644
--- a/packages/SystemUI/res-keyguard/values-tr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-tr/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Geçersiz Kart."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Şarj oldu"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Kablosuz olarak şarj ediliyor"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj oluyor"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Yuvada Şarj Oluyor"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj oluyor"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Hızlı şarj oluyor"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Yavaş şarj oluyor"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj işlemi pili korumak üzere optimize edildi"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Şarj etme geçici olarak sınırlı"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Kilidi açmak için Menü\'ye basın."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Ağ kilitli"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM yok"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM ekleyin."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM yok veya okunamıyor. SIM ekleyin."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Kullanılamayan SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM\'iniz kalıcı olarak devre dışı bırakıldı.\n Başka bir SIM için kablosuz servis sağlayıcınızla iletişime geçin."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM kilitli."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM\'in PUK kilidi devrede."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM\'in kilidi açılıyor…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM kart yok"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"SIM kart takın."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM kart yok veya okunamıyor. Bir SIM kart takın."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Kullanılamayan SIM kartı"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM kartınız kalıcı olarak devre dışı bırakıldı.\n Başka bir SIM kart için kablosuz servis sağlayıcınıza başvurun."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM kart kilitli."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM kart PUK kilidi devrede."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM kart kilidi açılıyor…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN alanı"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Cihaz şifresi"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM PIN alanı"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"\"<xliff:g id="CARRIER">%1$s</xliff:g>1 SIM artık devre dışı. Devam etmek için PUK kodunu girin. Ayrıntılar için operatör ile iletişim kurun."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"İstenen PIN kodunu girin"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"İstenen PIN kodunu onaylayın"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM\'in kilidi açılıyor…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM kart kilidi açılıyor…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4 ila 8 haneli bir PIN yazın."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kodu 8 veya daha çok basamaklı bir sayı olmalıdır."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN kodunuzu <xliff:g id="NUMBER_0">%1$d</xliff:g> kez yanlış girdiniz. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> saniye içinde tekrar deneyin."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Şifrenizi <xliff:g id="NUMBER_0">%1$d</xliff:g> kez yanlış yazdınız. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> saniye içinde tekrar deneyin."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Kilit açma deseninizi <xliff:g id="NUMBER_0">%1$d</xliff:g> kez yanlış çizdiniz. \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> saniye içinde tekrar deneyin."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Yanlış SIM PIN kodu. Cihazınızın kilidini açmak için artık operatörünüzle bağlantı kurmanız gerekiyor."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Yanlış SIM PIN kodu. Cihazınızın kilidini açması için operatörünüzle bağlantı kurmak zorunda kalmadan önce # deneme hakkınız kaldı.}other{Yanlış SIM PIN kodu. # deneme hakkınız kaldı. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Yanlış SIM PIN kodu, <xliff:g id="NUMBER_1">%d</xliff:g> deneme hakkınız kaldı.</item>
+      <item quantity="one">Yanlış SIM PIN kodu. Cihazının kilidini açmak için operatörünüzle bağlantı kurmak zorunda kalmadan önce <xliff:g id="NUMBER_0">%d</xliff:g> deneme hakkınız kaldı.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kullanılamaz. Operatörünüzle bağlantı kurun."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Yanlış SIM PUK kodu. SIM kalıcı olarak kullanılmaz hale gelmeden önce # deneme hakkınız kaldı.}other{Yanlış SIM PUK kodu. SIM kalıcı olarak kullanılmaz hale gelmeden önce # deneme hakkınız kaldı.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Yanlış SIM PUK kodu, SIM kalıcı olarak kullanılmaz hale gelmeden önce <xliff:g id="NUMBER_1">%d</xliff:g> deneme hakkınız kaldı.</item>
+      <item quantity="one">Yanlış SIM PUK kodu. SIM kalıcı olarak kullanılmaz hale gelmeden önce <xliff:g id="NUMBER_0">%d</xliff:g> deneme hakkınız kaldı.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN işlemi başarısız oldu!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK işlemi başarısız oldu!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Giriş yöntemini değiştir"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yeniden başladıktan sonra desen gerekir"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıktan sonra PIN gerekir"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıktan sonra şifre gerekir"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Ek güvenlik için bunun yerine desen kullanın"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Ek güvenlik için bunun yerine PIN kullanın"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Ek güvenlik için bunun yerine şifre kullanın"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Ek güvenlik için desen gerekir"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Ek güvenlik için PIN gerekir"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Ek güvenlik için şifre gerekir"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz, yönetici tarafından kilitlendi"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihazın manuel olarak kilitlendi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmadı"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Yüz Tanıma Kilidi için Ayarlar\'da kamera erişimini açın"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PIN\'inizi girin. Cihazınızın kilidini açmak için operatörünüzle bağlantı kurmak zorunda kalmadan önce # deneme hakkınız kaldı.}other{SIM PIN kodunu girin. # deneme hakkınız kaldı.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM artık devre dışı. Devam etmek için PUK kodunu girin. SIM kalıcı olarak kullanım dışı kalmadan önce # deneme hakkınız kaldı. Ayrıntılı bilgi için operatörünüzle iletişim kurun.}other{SIM artık devre dışı. Devam etmek için PUK kodunu girin. SIM kalıcı olarak kullanım dışı kalmadan önce # deneme hakkınız kaldı. Ayrıntılı bilgi için operatörünüzle iletişim kurun.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Yüz Tanıma Kilidi\'ni kullanmak için Ayarlar &gt; Gizlilik bölümünden "<b>"Kamera erişimi"</b>"\'ni açın"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN\'inizi girin. <xliff:g id="NUMBER_1">%d</xliff:g> deneme hakkınız kaldı.</item>
+      <item quantity="one">SIM PIN\'inizi girin. Cihazınızın kilidini açmak için operatörünüzle bağlantı kurmak zorunda kalmadan önce <xliff:g id="NUMBER_0">%d</xliff:g> deneme hakkınız kaldı.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM artık devre dışı. Devam etmek için PUK kodunu girin. SIM kalıcı olarak kullanım dışı kalmadan önce <xliff:g id="_NUMBER_1">%d</xliff:g> deneme hakkınız kaldı. Ayrıntılı bilgi için operatörünüzle iletişim kurun.</item>
+      <item quantity="one">SIM artık devre dışı. Devam etmek için PUK kodunu girin. SIM kalıcı olarak kullanım dışı kalmadan önce <xliff:g id="_NUMBER_0">%d</xliff:g> deneme hakkınız kaldı. Ayrıntılı bilgi için operatörünüzle iletişim kurun.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Varsayılan"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Baloncuk"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Devam etmek için cihazınızın kilidini açın"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-uk/strings.xml b/packages/SystemUI/res-keyguard/values-uk/strings.xml
index 4f32ee4..ae2131c 100644
--- a/packages/SystemUI/res-keyguard/values-uk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-uk/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Недійсна картка."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Заряджено"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Бездротове заряджання"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Заряджання"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Док-станція для заряджання"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Заряджання"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Швидке заряджання"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Повільне заряджання"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Заряджання оптимізовано, щоб захистити акумулятор"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Заряджання тимчасово обмежено"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Натисніть меню, щоб розблокувати."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Мережу заблоковано"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Немає SIM-карти"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Додайте SIM-карту."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM-карта відсутня або недоступна для читання. Додайте SIM-карту."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Непридатна SIM-карта."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM-карту деактивовано назавжди.\n Щоб отримати іншу, зверніться до свого постачальника послуг бездротового зв’язку."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM-карту заблоковано."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM-карту заблоковано PUK-кодом."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Розблокування SIM-карти…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Немає SIM-карти"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Вставте SIM-карту."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM-карта відсутня або недоступна для читання. Вставте SIM-карту."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Непридатна SIM-карта."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Вашу SIM-карту вимкнено назавжди.\n Зверніться до свого постачальника послуг бездротового зв’язку, щоб отримати іншу SIM-карту."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM-карту заблоковано."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM-карту заблоковано PUK-кодом."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Розблокування SIM-карти…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN-код"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Пароль пристрою"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"PIN-код SIM-карти"</string>
@@ -61,16 +61,26 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM-карту \"<xliff:g id="CARRIER">%1$s</xliff:g>\" вимкнено. Щоб продовжити, введіть PUK-код. Щоб дізнатися більше, зв’яжіться з оператором."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Введіть потрібний PIN-код"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Підтвердьте потрібний PIN-код"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Розблокування SIM-карти…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Розблокування SIM-карти…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Введіть PIN-код із 4–8 цифр."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK-код має складатися зі щонайменше 8 цифр."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN-код неправильно введено стільки разів: <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nПовторіть спробу через <xliff:g id="NUMBER_1">%2$d</xliff:g> с."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Пароль неправильно введено стільки разів: <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nПовторіть спробу через <xliff:g id="NUMBER_1">%2$d</xliff:g> с."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Ключ розблокування неправильно намальовано стільки разів: <xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nПовторіть спробу через <xliff:g id="NUMBER_1">%2$d</xliff:g> с."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Неправильний PIN-код SIM-карти. Зв’яжіться зі своїм оператором, щоб розблокувати пристрій."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Неправильний PIN-код SIM-карти. У вас залишилась # спроба. Після цього, щоб розблокувати пристрій, потрібно буде зв’язатися з оператором.}one{Неправильний PIN-код SIM-карти. У вас залишилася # спроба. }few{Неправильний PIN-код SIM-карти. У вас залишилося # спроби. }many{Неправильний PIN-код SIM-карти. У вас залишилося # спроб. }other{Неправильний PIN-код SIM-карти. У вас залишилося # спроби. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Неправильний PIN-код SIM-карти. У вас залишилася <xliff:g id="NUMBER_1">%d</xliff:g> спроба.</item>
+      <item quantity="few">Неправильний PIN-код SIM-карти. У вас залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроби.</item>
+      <item quantity="many">Неправильний PIN-код SIM-карти. У вас залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроб.</item>
+      <item quantity="other">Неправильний PIN-код SIM-карти. У вас залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроби.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM-карту заблоковано. Зв’яжіться з оператором."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Неправильний PUK-код SIM-карти. У вас залишилась # спроба. Після цього SIM-карту буде назавжди заблоковано.}one{Неправильний PUK-код SIM-карти. У вас залишилася # спроба. Після цього SIM-карту буде назавжди заблоковано.}few{Неправильний PUK-код SIM-карти. У вас залишилося # спроби. Після цього SIM-карту буде назавжди заблоковано.}many{Неправильний PUK-код SIM-карти. У вас залишилося # спроб. Після цього SIM-карту буде назавжди заблоковано.}other{Неправильний PUK-код SIM-карти. У вас залишилося # спроби. Після цього SIM-карту буде назавжди заблоковано.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Неправильний PUK-код SIM-карти. У вас залишилася <xliff:g id="NUMBER_1">%d</xliff:g> спроба. Після цього SIM-карту буде назавжди заблоковано.</item>
+      <item quantity="few">Неправильний PUK-код SIM-карти. У вас залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроби. Після цього SIM-карту буде назавжди заблоковано.</item>
+      <item quantity="many">Неправильний PUK-код SIM-карти. У вас залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроб. Після цього SIM-карту буде назавжди заблоковано.</item>
+      <item quantity="other">Неправильний PUK-код SIM-карти. У вас залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроби. Після цього SIM-карту буде назавжди заблоковано.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Помилка введення PIN-коду SIM-карти."</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Помилка введення PUK-коду SIM-карти."</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Змінити метод введення"</string>
@@ -78,17 +88,26 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Після перезавантаження пристрою потрібно ввести ключ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Після перезавантаження пристрою потрібно ввести PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Після перезавантаження пристрою потрібно ввести пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"З міркувань додаткової безпеки скористайтеся ключем"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"З міркувань додаткової безпеки скористайтеся PIN-кодом"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"З міркувань додаткової безпеки скористайтеся паролем"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для додаткового захисту потрібно ввести ключ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для додаткового захисту потрібно ввести PIN-код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для додаткового захисту потрібно ввести пароль"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Адміністратор заблокував пристрій"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Пристрій заблоковано вручну"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не розпізнано"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Для фейсконтролю надайте доступ до камери"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Введіть PIN-код SIM-карти. У вас залишилась # спроба. Після цього, щоб розблокувати пристрій, потрібно буде зв’язатися з оператором.}one{Введіть PIN-код SIM-карти. У вас залишилася # спроба.}few{Введіть PIN-код SIM-карти. У вас залишилося # спроби.}many{Введіть PIN-код SIM-карти. У вас залишилося # спроб.}other{Введіть PIN-код SIM-карти. У вас залишилося # спроби.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. У вас залишилась # спроба. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.}one{SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. У вас залишилася # спроба. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.}few{SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. У вас залишилося # спроби. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.}many{SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. У вас залишилося # спроб. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.}other{SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. У вас залишилося # спроби. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Щоб використовувати фейсконтроль, увімкніть "<b>"Доступ до камери"</b>" в розділі \"Налаштування\" &gt; \"Конфіденційність\""</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Введіть PIN-код SIM-карти. Залишилася <xliff:g id="NUMBER_1">%d</xliff:g> спроба.</item>
+      <item quantity="few">Введіть PIN-код SIM-карти. Залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроби.</item>
+      <item quantity="many">Введіть PIN-код SIM-карти. Залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроб.</item>
+      <item quantity="other">Введіть PIN-код SIM-карти. Залишилося <xliff:g id="NUMBER_1">%d</xliff:g> спроби.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. Залишилася <xliff:g id="_NUMBER_1">%d</xliff:g> спроба. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.</item>
+      <item quantity="few">SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. Залишилося <xliff:g id="_NUMBER_1">%d</xliff:g> спроби. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.</item>
+      <item quantity="many">SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. Залишилося <xliff:g id="_NUMBER_1">%d</xliff:g> спроб. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.</item>
+      <item quantity="other">SIM-карту заблоковано. Щоб продовжити, введіть PUK-код. Залишилося <xliff:g id="_NUMBER_1">%d</xliff:g> спроби. Після цього SIM-карту буде назавжди заблоковано. Щоб дізнатися більше, зверніться до свого оператора.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"За умовчанням"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бульбашковий"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналоговий"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Розблокуйте пристрій, щоб продовжити"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ur/strings.xml b/packages/SystemUI/res-keyguard/values-ur/strings.xml
index 5cc83a1..3e1245e 100644
--- a/packages/SystemUI/res-keyguard/values-ur/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ur/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"غلط کارڈ۔"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"چارج ہوگئی"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • وائرلیس طریقے سے چارج ہو رہا ہے"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • چارج ہو رہا ہے"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • چارجنگ ڈاک"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • چارج ہو رہا ہے"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • تیزی سے چارج ہو رہا ہے"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • آہستہ چارج ہو رہا ہے"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • بیٹری کی حفاظت کے لیے چارجنگ کو بہتر بنایا گیا"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • چارجنگ عارضی طور پر محدود ہے"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"غیر مقفل کرنے کیلئے مینو دبائیں۔"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"نیٹ ورک مقفل ہو گیا"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"‏کوئی SIM نہیں ہے"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"‏ایک SIM شامل کریں۔"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"‏SIM غائب ہے یا پڑھنے لائق نہیں ہے۔ ایک SIM شامل کریں۔"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"‏ناقابل استعمال SIM۔"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"‏آپ کے SIM کو مستقل طور پر غیر فعال کر دیا گیا ہے۔\n کسی دوسرے SIM کیلئے اپنے وائرلیس سروس فراہم کنندہ سے رابطہ کریں۔"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"‏SIM مقفل ہے۔"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"‏آپ کا SIM ‏PUK مقفل ہے۔"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"‏SIM کو غیر مقفل کیا جا رہا ہے…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"‏کوئی SIM کارڈ نہیں ہے"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"‏ایک SIM کارڈ داخل کریں۔"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"‏SIM کارڈ غائب ہے یا پڑھنے کے قابل نہیں ہے۔ ایک SIM کارڈ داخل کریں۔"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"‏ناقابل استعمال SIM کارڈ۔"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"‏آپ کا SIM کارڈ مستقل طور پر غیر فعال کر دیا گیا ہے۔\n کسی دوسرے SIM کارڈ کیلئے اپنے وائرلیس سروس فراہم کنندہ سے رابطہ کریں۔"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"‏SIM کارڈ مقفل ہے۔"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"‏SIM کارڈ PUK مقفل ہے۔"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"‏SIM کارڈ غیر مقفل ہو رہا ہے…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"‏PIN کا علاقہ"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"آلے کا پاس ورڈ"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"‏SIM PIN کا علاقہ"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"‏SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" اب غیر فعال ہے۔ جاری رکھنے کیلئے PUK کوڈ درج کریں۔ تفصیلات کیلئے کیریئر سے رابطہ کریں۔"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"‏پسندیدہ PIN کوڈ درج کریں"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"‏پسندیدہ PIN کوڈ کی توثیق کریں"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"‏SIM کو غیر مقفل کیا جا رہا ہے…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"‏SIM کارڈ غیر مقفل ہو رہا ہے…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"‏ایسا PIN ٹائپ کریں جو 4 تا 8 اعداد پر مشتمل ہو۔"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"‏PUK کوڈ 8 یا زائد اعداد پر مشتمل ہونا چاہیے۔"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"‏آپ نے اپنا PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> بار غلط طریقے سے ٹائپ کیا ہے۔ \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> سیکنڈ میں دوبارہ کوشش کریں۔"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"آپ نے اپنا پاس ورڈ <xliff:g id="NUMBER_0">%1$d</xliff:g> بار غلط طریقے سے ٹائپ کیا ہے۔ \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> سیکنڈ میں دوبارہ کوشش کریں۔"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"آپ نے اپنا غیر مقفل کرنے کا پیٹرن <xliff:g id="NUMBER_0">%1$d</xliff:g> بار غلط طریقے سے ڈرا کیا ہے۔ \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> سیکنڈ میں دوبارہ کوشش کریں۔"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"‏غلط SIM PIN کوڈ، اب آپ کو اپنا آلہ غیر مقفل کرنے کیلئے اپنے کیریئر سے رابطہ کرنا ہوگا۔"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{‏SIM کا غلط PIN کوڈ، اس سے پہلے کہ آپ اپنا آلہ غیر مقفل کرنے کیلئے لازمی طور پر اپنے کیریئر سے رابطہ کریں آپ کے پاس # کوشش بچی ہے۔}other{‏SIM کا غلط PIN کوڈ، آپ کے پاس # کوششیں بچی ہیں۔ }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">‏غلط SIM PIN کوڈ، آپ کے پاس <xliff:g id="NUMBER_1">%d</xliff:g> کوششیں بچی ہیں۔</item>
+      <item quantity="one">‏غلط SIM PIN کوڈ، آپ کے پاس <xliff:g id="NUMBER_0">%d</xliff:g> کوشش بچی ہے، اس کے بعد آپ کو اپنا آلہ غیر مقفل کرنے کیلئے اپنے کیریئر سے رابطہ کرنا ہوگا۔</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"‏SIM ناقابل استعمال ہے۔ اپنے کیریئر سے رابطہ کریں۔"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{‏SIM کا غلط PUK کوڈ، SIM کے مستقل طور پر ناقابل استعمال ہونے سے پہلے آپ کے پاس # کوشش بچی ہے۔}other{‏SIM کا غلط PUK کوڈ، SIM کے مستقل طور پر ناقابل استعمال ہونے سے پہلے آپ کے پاس # کوششیں بچی ہیں۔}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">‏غلط SIM PUK کوڈ، آپ کے پاس <xliff:g id="NUMBER_1">%d</xliff:g> کوششیں بچی ہیں، اس کے بعد SIM مستقل طور پر ناقابل استعمال ہو جائے گا۔</item>
+      <item quantity="one">‏غلط SIM PUK کوڈ، آپ کے پاس <xliff:g id="NUMBER_0">%d</xliff:g> کوشش بچی ہے، اس کے بعد SIM مستقل طور پر ناقابل استعمال ہو جائے گا۔</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"‏SIM PIN کی کارروائی ناکام ہوگئی!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"‏SIM PUK کارروائی ناکام ہو گئی!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"اندراج کا طریقہ سوئچ کریں"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"آلہ دوبارہ چالو ہونے کے بعد پیٹرن درکار ہوتا ہے"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"‏آلہ دوبارہ چالو ہونے کے بعد PIN درکار ہوتا ہے"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"آلہ دوبارہ چالو ہونے کے بعد پاس ورڈ درکار ہوتا ہے"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"اضافی سیکیورٹی کے لئے، اس کے بجائے پیٹرن استعمال کریں"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"‏اضافی سیکیورٹی کے لئے، اس کے بجائے PIN استعمال کریں"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"اضافی سیکیورٹی کے لئے، اس کے بجائے پاس ورڈ استعمال کریں"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"اضافی سیکیورٹی کیلئے پیٹرن درکار ہے"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"‏اضافی سیکیورٹی کیلئے PIN درکار ہے"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"اضافی سیکیورٹی کیلئے پاس ورڈ درکار ہے"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"آلہ منتظم کی جانب سے مقفل ہے"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"آلہ کو دستی طور پر مقفل کیا گیا تھا"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"تسلیم شدہ نہیں ہے"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"فیس اَنلاک استعمال کرنے کیلئے، ترتیبات میں کیمرا تک رسائی کو آن کریں"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{‏SIM کا PIN درج کریں۔ اس سے پہلے کہ آپ اپنا آلہ غیر مقفل کرنے کیلئے لازمی طور پر اپنے کیریئر سے رابطہ کریں آپ کے پاس # کوشش بچی ہے۔}other{‏SIM کا PIN درج کریں۔ آپ کے پاس # کوششیں بچی ہیں۔}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{‏SIM اب غیر فعال ہے۔ جاری رکھنے کیلئے PUK کوڈ درج کریں۔ SIM کے مستقل طور پر ناقابل استعمال ہونے سے پہلے آپ کے پاس # کوشش بچی ہے۔ تفصیلات کیلئے کیریئر سے رابطہ کریں۔}other{‏SIM اب غیر فعال ہے۔ جاری رکھنے کیلئے PUK کوڈ درج کریں۔ SIM کے مستقل طور پر ناقابل استعمال ہونے سے پہلے آپ کے پاس # کوششیں بچی ہیں۔ تفصیلات کیلئے کیریئر سے رابطہ کریں۔}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"فیس اَنلاک کا استعمال کرنے کے لیے، ترتیبات اور رازداری میں "<b>"کیمرے تک رسائی"</b>" کو آن کریں"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">‏SIM کا PIN درج کریں، آپ کے پاس <xliff:g id="NUMBER_1">%d</xliff:g> کوششیں بچی ہیں۔</item>
+      <item quantity="one">‏SIM کا PIN درج کریں، آپ کے پاس <xliff:g id="NUMBER_0">%d</xliff:g> کوشش بچی ہے، اس کے بعد آپ کو اپنا آلہ غیر مقفل کرنے کے لیے اپنے کیریئر سے رابطہ کرنا ہوگا۔</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">‏SIM اب غیر فعال ہے۔ جاری رکھنے کیلئے PUK کوڈ درج کریں۔ SIM کے مستقل طور پر ناقابل استعمال ہونے سے پہلے آپ کے پاس <xliff:g id="_NUMBER_1">%d</xliff:g> کوششیں بچی ہیں۔ تفصیلات کیلئے کیریئر سے رابطہ کریں۔</item>
+      <item quantity="one">‏SIM اب غیر فعال ہے۔ جاری رکھنے کیلئے PUK کوڈ درج کریں۔ SIM کے مستقل طور پر ناقابل استعمال ہونے سے پہلے آپ کے پاس <xliff:g id="_NUMBER_0">%d</xliff:g> کوشش بچی ہے۔ تفصیلات کیلئے کیریئر سے رابطہ کریں۔</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"ڈیفالٹ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"بلبلہ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"اینالاگ"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"جاری رکھنے کے لئے اپنا آلہ غیر مقفل کریں"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-uz/strings.xml b/packages/SystemUI/res-keyguard/values-uz/strings.xml
index 118d7c4..e116919 100644
--- a/packages/SystemUI/res-keyguard/values-uz/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-uz/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"SIM karta yaroqsiz."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Quvvat oldi"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Simsiz quvvatlanyapti"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Quvvat olmoqda"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Quvvatlash dok-stansiyasi"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Quvvat olmoqda"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Tezkor quvvat olmoqda"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Sekin quvvat olmoqda"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Batareyani himoyalash uchun quvvatlash optimallashtirildi"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Quvvatlash vaqtincha cheklangan"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Qulfdan chiqarish uchun Menyu tugmasini bosing."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Tarmoq qulflangan"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"SIM kartasiz"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"SIM karta qoʻshish."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM karta topilmadi yoki oʻqilmadi. SIM karta qoʻshish."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"Ishlamaydigan SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM karta butunlay faolsizlantirildi.\n Boshqa SIM karta olish uchun simsiz aloqa operatoriga murojaat qiling."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM karta qulflandi."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM karta PUK kod bilan qulflangan."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"SIM karta qulfdan chiqarilmoqda…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"SIM karta solinmagan"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Telefonga SIM karta soling."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM karta solinmagan yoki u yaroqsiz. SIM karta soling."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Foydalanib bo‘lmaydigan SIM karta."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"SIM kartangiz butunlay bloklab qo‘yilgan.\n Yangi SIM karta olish uchun aloqa operatoringiz bilan bog‘laning."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM karta qulflangan."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM karta PUK kod bilan qulflangan."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"SIM karta qulfi ochilmoqda…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN kod maydoni"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Qurilma paroli"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM karta PIN kodi maydoni"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"“<xliff:g id="CARRIER">%1$s</xliff:g>” SIM kartasi o‘chirib qo‘yildi. Davom etish uchun PUK kodni kiriting. Tafsilotlar uchun aloqa operatoringizga murojaat qiling."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"So‘ralgan PIN kodni kiriting"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"So‘ralgan PIN kodni tasdiqlang"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"SIM karta qulfdan chiqarilmoqda…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"SIM karta qulfi ochilmoqda…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"4-8 ta raqamdan iborat PIN kodni kiriting."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK kod kamida 8 ta raqamdan iborat bo‘lishi shart."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"PIN kod <xliff:g id="NUMBER_0">%1$d</xliff:g> marta xato kiritildi. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> soniyadan keyin qaytadan urining."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Parol <xliff:g id="NUMBER_0">%1$d</xliff:g> marta xato kiritildi. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> soniyadan keyin qaytadan urining."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Grafik kalit <xliff:g id="NUMBER_0">%1$d</xliff:g> marta xato kiritildi. \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> soniyadan keyin qayta urining."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM kartaning PIN kodi xato. Qurilma qulfini ochish uchun operatoringizga murojaat qiling."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM karta PIN kodi xato kiritildi, yana # marta urinishingiz mumkin, urinishlar tugagandan keyin qurilmangizni qulfdan chiqarish uchun aloqa operatoringizga murojaat qilishingiz kerak.}other{SIM karta PIN kodi xato kiritildi, yana # marta urinishingiz mumkin. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM kartaning PIN kodi noto‘g‘ri. Sizda yana <xliff:g id="NUMBER_1">%d</xliff:g> ta urinish qoldi.</item>
+      <item quantity="one">SIM karta PIN kodi noto‘g‘ri terildi, yana <xliff:g id="NUMBER_0">%d</xliff:g> marta uirinib ko‘rishingiz mumkin, urinishlar tugagandan keyin qurilmangizni qulfdan chiqarish uchun aloqa operatoringiz bilan bog‘lanishingiz kerak.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM kartadan foydalanib bo‘lmaydi. Operatoringizga murojaat qiling."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM karta PUK kodi xato kiritildi, yana # marta urinishdan keyin SIM kartadan umuman foydalanib boʻlmay qoladi.}other{SIM karta PUK kodi xato kiritildi, yana # marta urinishdan keyin SIM kartadan umuman foydalanib boʻlmay qoladi.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM kartaning PUK kodi noto‘g‘ri kiritildi. Yana <xliff:g id="NUMBER_1">%d</xliff:g> ta muvaffaqiyatsiz urinishdan keyin SIM karta butunlay ishdan chiqadi.</item>
+      <item quantity="one">SIM karta PUK kodi noto‘g‘ri terildi, yana <xliff:g id="NUMBER_0">%d</xliff:g> marta urinib ko‘rganingizdan keyin SIM kartadan umuman foydalanib bo‘lmaydi.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM kartani qulfdan chiqarib bo‘lmadi!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM kartani qulfdan chiqarib bo‘lmadi!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Matn kiritish usulini almashtirish"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Qurilma qayta ishga tushganidan keyin grafik kalitni kiritish zarur"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Qurilma qayta ishga tushganidan keyin PIN kodni kiritish zarur"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Qurilma qayta ishga tushganidan keyin parolni kiritish zarur"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Qoʻshimcha xavfsizlik maqsadida oʻrniga grafik kalitdan foydalaning"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Qoʻshimcha xavfsizlik maqsadida oʻrniga PIN koddan foydalaning"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Qoʻshimcha xavfsizlik maqsadida oʻrniga paroldan foydalaning"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Qo‘shimcha xavfsizlik chorasi sifatida grafik kalit talab qilinadi"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Qo‘shimcha xavfsizlik chorasi sifatida PIN kod talab qilinadi"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Qo‘shimcha xavfsizlik chorasi sifatida parol talab qilinadi"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Qurilma administrator tomonidan bloklangan"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Qurilma qo‘lda qulflangan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Aniqlanmadi"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Yuz bilan ochish uchun kamera ruxsatini bering"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{SIM PIN kodini kiriting, qurilmani qulfdan chiqarish uchun sizda # ta urinish bor.}other{SIM karta PIN kodini kiriting. Sizda # ta urinish qoldi.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM karta faolsizlantirildi. Davom etish uchun PUK kodni kiriting. Yana # marta xato qilsangiz, SIM kartangiz butunlay qulflanadi. Batafsil axborot olish uchun aloqa operatoriga murojaat qiling.}other{SIM karta faolsizlantirildi. Davom etish uchun PUK kodni kiriting. Yana # marta xato qilsangiz, SIM kartangiz butunlay qulflanadi. Batafsil axborot olish uchun aloqa operatoriga murojaat qiling.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Yuz bilan ochish uchun Sozlamalar va maxfiylik orqali "<b>"kameraga kirishga ruxsat bering"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">SIM PIN kodini kiriting, sizda <xliff:g id="NUMBER_1">%d</xliff:g> ta urinish bor.</item>
+      <item quantity="one">SIM PIN kodini kiriting, qurilmani qulfdan chiqarish uchun sizda <xliff:g id="NUMBER_0">%d</xliff:g> ta urinish bor.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM karta faolsizlantirildi. Davom etish uchun PUK kodni kiriting. Yana <xliff:g id="_NUMBER_1">%d</xliff:g> marta xato qilsangiz, SIM kartangiz butunlay qulflanadi. Batafsil axborot olish uchun tarmoq operatoriga murojaat qiling.</item>
+      <item quantity="one">SIM karta faolsizlantirildi. Davom etish uchun PUK kodni kiriting. Yana <xliff:g id="_NUMBER_0">%d</xliff:g> marta xato qilsangiz, SIM kartangiz butunlay qulflanadi. Batafsil axborot olish uchun tarmoq operatoriga murojaat qiling.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Odatiy"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Pufaklar"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Davom etish uchun qurilmangizni qulfdan chiqaring"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-vi/strings.xml b/packages/SystemUI/res-keyguard/values-vi/strings.xml
index 9f20cd3..15e35c4 100644
--- a/packages/SystemUI/res-keyguard/values-vi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-vi/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Thẻ không hợp lệ."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Đã sạc đầy"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Đang sạc không dây"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Đang sạc"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Đế sạc"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Đang sạc"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Đang sạc nhanh"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Đang sạc chậm"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Quá trình sạc được tối ưu hoá để bảo vệ pin"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Chức năng sạc tạm thời bị hạn chế"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Nhấn vào Menu để mở khóa."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Mạng đã bị khóa"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Không có SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"Hãy thêm SIM."</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"Không tìm thấy hoặc không đọc được SIM. Hãy thêm SIM."</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM không sử dụng được."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM của bạn đã bị vô hiệu hoá vĩnh viễn.\n Hãy liên hệ với nhà cung cấp dịch vụ viễn thông không dây của bạn để yêu cầu cấp SIM khác."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM này đang bị khoá."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM này đang bị khoá PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Đang mở khoá SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Không có thẻ SIM"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Lắp thẻ SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Thẻ SIM bị thiếu hoặc không thể đọc được. Hãy lắp thẻ SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Thẻ SIM không sử dụng được."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Thẻ SIM của bạn đã bị vô hiệu hóa vĩnh viễn.\n Hãy liên hệ với nhà cung cấp dịch vụ không dây của bạn để lấy thẻ SIM khác."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Thẻ SIM đã bị khóa."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Thẻ SIM đã bị khóa bằng mã PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Đang mở khóa thẻ SIM…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Khu vực mã PIN"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Mật khẩu thiết bị"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Khu vực mã PIN của SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" hiện bị vô hiệu hóa. Hãy nhập mã PUK để tiếp tục. Liên hệ với nhà cung cấp dịch vụ để biết chi tiết."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Nhập mã PIN mong muốn"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Xác nhận mã PIN mong muốn"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Đang mở khoá SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Đang mở khóa thẻ SIM…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Nhập mã PIN có từ 4 đến 8 số."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Mã PUK phải có từ 8 số trở lên."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Bạn đã nhập sai mã PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> lần. \n\nHãy thử lại sau <xliff:g id="NUMBER_1">%2$d</xliff:g> giây."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Bạn đã nhập sai mật khẩu <xliff:g id="NUMBER_0">%1$d</xliff:g> lần. \n\nHãy thử lại sau <xliff:g id="NUMBER_1">%2$d</xliff:g> giây."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Bạn đã vẽ không chính xác hình mở khóa <xliff:g id="NUMBER_0">%1$d</xliff:g> lần. \n\nHãy thử lại sau <xliff:g id="NUMBER_1">%2$d</xliff:g> giây."</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Mã PIN của SIM không chính xác, bây giờ bạn phải liên hệ với nhà cung cấp dịch vụ để mở khóa thiết bị của mình."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Mã PIN của SIM không chính xác. Bạn còn # lần thử trước khi phải liên hệ với nhà cung cấp dịch vụ để mở khóa thiết bị của mình.}other{Mã PIN của SIM không chính xác, bạn còn # lần thử. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">Mã PIN của SIM không chính xác, bạn còn <xliff:g id="NUMBER_1">%d</xliff:g> lần thử.</item>
+      <item quantity="one">Mã PIN của SIM không chính xác, bạn còn <xliff:g id="NUMBER_0">%d</xliff:g> lần thử trước khi bạn phải liên hệ với nhà cung cấp dịch vụ để mở khóa thiết bị của mình.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM không thể sử dụng được. Hãy liên hệ với nhà cung cấp dịch vụ của bạn."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Mã PUK của SIM không chính xác, bạn còn # lần thử trước khi SIM vĩnh viễn không thể sử dụng được.}other{Mã PUK của SIM không chính xác, bạn còn # lần thử trước khi SIM vĩnh viễn không thể sử dụng được.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">Mã PUK của SIM không chính xác, bạn còn <xliff:g id="NUMBER_1">%d</xliff:g> lần thử trước khi SIM vĩnh viễn không thể sử dụng được.</item>
+      <item quantity="one">Mã PUK của SIM không chính xác, bạn còn <xliff:g id="NUMBER_0">%d</xliff:g> lần thử trước khi SIM vĩnh viễn không thể sử dụng được.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Thao tác mã PIN của SIM không thành công!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Thao tác mã PUK của SIM không thành công!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Chuyển phương thức nhập"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Yêu cầu hình mở khóa sau khi thiết bị khởi động lại"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Yêu cầu mã PIN sau khi thiết bị khởi động lại"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Yêu cầu mật khẩu sau khi thiết bị khởi động lại"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Để tăng cường bảo mật, hãy sử dụng hình mở khoá"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Để tăng cường bảo mật, hãy sử dụng mã PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Để tăng cường bảo mật, hãy sử dụng mật khẩu"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Yêu cầu hình mở khóa để bảo mật thêm"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Yêu cầu mã PIN để bảo mật thêm"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Yêu cầu mật khẩu để bảo mật thêm"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Thiết bị đã bị quản trị viên khóa"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Thiết bị đã bị khóa theo cách thủ công"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Không nhận dạng được"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Cho phép truy cập máy ảnh để dùng Mở khóa bằng khuôn mặt"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Hãy nhập mã PIN của SIM. Bạn còn # lần thử trước khi phải liên hệ với nhà cung cấp dịch vụ để mở khóa thiết bị của mình.}other{Nhập mã PIN của SIM. Bạn còn # lần thử.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM hiện đã bị tắt. Hãy nhập mã PUK để tiếp tục. Bạn còn # lần thử trước khi SIM vĩnh viễn không thể sử dụng được. Hãy liên hệ với nhà cung cấp dịch vụ để biết thông tin chi tiết.}other{SIM hiện đã bị tắt. Hãy nhập mã PUK để tiếp tục. Bạn còn # lần thử trước khi SIM vĩnh viễn không thể sử dụng được. Hãy liên hệ với nhà cung cấp dịch vụ để biết thông tin chi tiết.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Để dùng tính năng Mở khoá bằng khuôn mặt, hãy bật tuỳ chọn "<b>"Truy cập máy ảnh"</b>" trong phần Cài đặt &gt; Quyền riêng tư"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">Hãy nhập mã PIN của SIM. Bạn còn <xliff:g id="NUMBER_1">%d</xliff:g> lần thử.</item>
+      <item quantity="one">Hãy nhập mã PIN của SIM. Bạn còn <xliff:g id="NUMBER_0">%d</xliff:g> lần thử trước khi phải liên hệ với nhà mạng để mở khóa thiết bị của mình.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM hiện đã bị tắt. Hãy nhập mã PUK để tiếp tục. Bạn còn <xliff:g id="_NUMBER_1">%d</xliff:g> lần thử trước khi SIM vĩnh viễn không sử dụng được. Hãy liên hệ với nhà cung cấp dịch vụ để biết chi tiết.</item>
+      <item quantity="one">SIM hiện đã bị tắt. Hãy nhập mã PUK để tiếp tục. Bạn còn <xliff:g id="_NUMBER_0">%d</xliff:g> lần thử trước khi SIM vĩnh viễn không thể sử dụng được. Hãy liên hệ với nhà cung cấp dịch vụ để biết chi tiết.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Mặc định"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bong bóng"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Đồng hồ kim"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Mở khoá thiết bị của bạn để tiếp tục"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
index bb9f42c..0deee177 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"SIM 卡无效。"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"已充满电"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在无线充电"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在充电"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在基座上充电"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在充电"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在快速充电"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在慢速充电"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 为保护电池,充电过程已优化"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充电暂时受限"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"按“菜单”即可解锁。"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"网络已锁定"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"没有 SIM 卡"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"请插入 SIM 卡。"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"SIM 卡缺失或无法读取。请插入 SIM 卡。"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM 卡无法使用。"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"您的 SIM 卡已被永久停用。\n请与您的无线服务提供商联系,以便重新获取一张 SIM 卡。"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM 卡已被锁定。"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM 卡已用 PUK 码锁定。"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"正在解锁 SIM 卡…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"没有 SIM 卡"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"请插入 SIM 卡。"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"SIM 卡缺失或无法读取,请插入 SIM 卡。"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM 卡无法使用。"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"您的 SIM 卡已永久停用。\n请与您的无线服务提供商联系,以便重新获取一张 SIM 卡。"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM 卡已锁定。"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM 卡已用 PUK 码锁定。"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"正在解锁 SIM 卡…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN 码区域"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"设备密码"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM 卡 PIN 码区域"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM 卡“<xliff:g id="CARRIER">%1$s</xliff:g>”现已停用,需要输入 PUK 码才能继续使用。要了解详情,请联系您的运营商。"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"请输入所需的 PIN 码"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"请确认所需的 PIN 码"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"正在解锁 SIM 卡…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"正在解锁 SIM 卡…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"请输入 4 到 8 位数的 PIN 码。"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK 码应至少包含 8 位数字。"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"您已经 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次输错 PIN 码。\n\n请在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒后重试。"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"您已经 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次输错密码。\n\n请在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒后重试。"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"您已经 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次画错解锁图案。\n\n请在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒后重试。"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM 卡 PIN 码不正确,您现在必须联系运营商为您解锁设备。"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM 卡 PIN 码不正确,您还可以尝试 # 次,如果仍不正确,则必须联系运营商帮您解锁设备。}other{SIM 卡 PIN 码不正确,您还可以尝试 # 次。}}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM 卡 PIN 码不正确,您还有 <xliff:g id="NUMBER_1">%d</xliff:g> 次尝试机会。</item>
+      <item quantity="one">SIM 卡 PIN 码不正确,您还有 <xliff:g id="NUMBER_0">%d</xliff:g> 次尝试机会。如果仍不正确,则需要联系运营商帮您解锁设备。</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM 卡无法使用,请与您的运营商联系。"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM 卡 PUK 码不正确,您还可以尝试 # 次,如果仍不正确,SIM 卡将永远无法使用。}other{SIM 卡 PUK 码不正确,您还可以尝试 # 次,如果仍不正确,SIM 卡将永远无法使用。}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM 卡 PUK 码不正确,您还有 <xliff:g id="NUMBER_1">%d</xliff:g> 次尝试机会。如果仍不正确,SIM 卡将永远无法使用。</item>
+      <item quantity="one">SIM 卡 PUK 码不正确,您还有 <xliff:g id="NUMBER_0">%d</xliff:g> 次尝试机会。如果仍不正确,SIM 卡将永远无法使用。</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM 卡 PIN 码操作失败!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM 卡 PUK 码操作失败!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"切换输入法"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"重启设备后需要绘制解锁图案"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"重启设备后需要输入 PIN 码"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"重启设备后需要输入密码"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"为增强安全性,请改用图案"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"为增强安全性,请改用 PIN 码"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"为增强安全性,请改用密码"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"需要绘制解锁图案以进一步确保安全"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"需要输入 PIN 码以进一步确保安全"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"需要输入密码以进一步确保安全"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理员已锁定设备"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"此设备已手动锁定"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"无法识别"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"如需使用人脸解锁功能,请在“设置”中开启摄像头使用权限"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{请输入 SIM 卡 PIN 码。您还可以尝试 # 次,如果仍不正确,则必须联系运营商帮您解锁设备。}other{请输入 SIM 卡 PIN 码。您还可以尝试 # 次。}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM 卡现已停用,请输入 PUK 码继续使用。您还可以尝试 # 次,如果仍不正确,SIM 卡将永远无法使用。有关详情,请联系运营商。}other{SIM 卡现已停用,请输入 PUK 码继续使用。您还可以尝试 # 次,如果仍不正确,SIM 卡将永远无法使用。有关详情,请联系运营商。}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"如需使用人脸解锁功能,请在“设置”&gt;“隐私权”中开启"<b>"摄像头使用权限"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">请输入 SIM 卡 PIN 码,您还可以尝试 <xliff:g id="NUMBER_1">%d</xliff:g> 次。</item>
+      <item quantity="one">请输入 SIM 卡 PIN 码,您还可以尝试 <xliff:g id="NUMBER_0">%d</xliff:g> 次。如果仍不正确,则需要联系运营商帮您解锁设备。</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM 卡现已停用,请输入 PUK 码继续使用。您还可以尝试 <xliff:g id="_NUMBER_1">%d</xliff:g> 次。如果仍不正确,该 SIM 卡将永远无法使用。有关详情,请联系您的运营商。</item>
+      <item quantity="one">SIM 卡现已停用,请输入 PUK 码继续使用。您还可以尝试 <xliff:g id="_NUMBER_0">%d</xliff:g> 次。如果仍不正确,该 SIM 卡将永远无法使用。有关详情,请联系您的运营商。</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"默认"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"指针"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"解锁设备才能继续操作"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
index 2ca666f..145bd2b 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"SIM 卡無效。"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"已完成充電"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 無線充電中"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在充電"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在插座上充電"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在充電"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 快速充電中"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 慢速充電中"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 為保護電池,系統已優化充電"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電暫時受限"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"按下 [選單] 即可解鎖。"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"網絡已鎖定"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"沒有 SIM 卡"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"請新增 SIM 卡。"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"找不到 SIM 卡或 SIM 卡無法讀取,請新增 SIM 卡。"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM 卡無法使用。"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM 卡已永久停用。\n請向無線服務供應商索取其他 SIM 卡。"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM 卡已鎖定。"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM 卡已使用 PUK 鎖定。"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"正在解鎖 SIM 卡…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"沒有 SIM 卡"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"請插入 SIM 卡。"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"找不到或無法讀取 SIM 卡。請插入 SIM 卡。"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM 卡無法使用。"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"您的 SIM 卡已永久停用。\n請與您的無線服務供應商聯絡,以取得另一張 SIM 卡。"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM 卡處於上鎖狀態。"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM 卡處於 PUK 上鎖狀態。"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"正在解鎖 SIM 卡…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN 區域"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"裝置密碼"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM 卡 PIN 區域"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM 卡「<xliff:g id="CARRIER">%1$s</xliff:g>」現已停用,請輸入 PUK 碼以繼續。詳情請與流動網絡供應商聯絡。"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"輸入所需的 PIN 碼"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"確認所需的 PIN 碼"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"正在解鎖 SIM 卡…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"正在解鎖 SIM 卡…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"請輸入 4 至 8 位數的 PIN 碼。"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK 碼應由 8 個或以上數字組成。"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"您已輸入錯誤的 PIN 碼 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次。\n\n請在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後再試一次。"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"您已輸入錯誤的密碼 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次。\n\n請在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後再試一次。"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"您已畫錯解鎖圖案 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次。\n\n請在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後再試一次。"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM 卡 PIN 碼不正確,您現在必須聯絡流動網絡供應商為您的裝置解鎖。"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM 卡的 PIN 碼不正確,您還可以再試 # 次。如果仍然輸入錯誤,您必須聯絡流動網絡供應商解鎖您的裝置。}other{SIM 卡的 PIN 碼不正確,您還可以再試 # 次。}}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM 卡的 PIN 碼不正確,您還有 <xliff:g id="NUMBER_1">%d</xliff:g> 次輸入機會。</item>
+      <item quantity="one">SIM 卡的 PIN 碼不正確,您還有 <xliff:g id="NUMBER_0">%d</xliff:g> 次輸入機會。如果仍然輸入錯誤,您必須聯絡流動網絡供應商為您的裝置解鎖。</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM 卡無法使用,請聯絡您的流動網絡供應商。"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM 卡的 PUK 碼不正確,您還可以再試 # 次。如果仍然輪入錯誤,SIM 卡將永久無法使用。}other{SIM 卡的 PUK 碼不正確,您還可以再試 # 次。如果仍然輪入錯誤,SIM 卡將永久無法使用。}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM 卡的 PUK 碼不正確,您還有 <xliff:g id="NUMBER_1">%d</xliff:g> 次輸入機會。如果仍然輸入錯誤,SIM 卡將永久無法使用。</item>
+      <item quantity="one">SIM 卡的 PUK 碼不正確,您還有 <xliff:g id="NUMBER_0">%d</xliff:g> 次輸入機會。如果仍然輸入錯誤,SIM 卡將永久無法使用。</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"無法使用 SIM 卡 PIN 碼解鎖!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"無法使用 SIM 卡 PUK 碼解鎖!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"轉換輸入方法"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後,必須畫出上鎖圖案才能使用"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後,必須輸入 PIN 碼才能使用"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後,必須輸入密碼才能使用"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"為提升安全性,請改用圖案"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"為提升安全性,請改用 PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"為提升安全性,請改用密碼"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請務必畫出上鎖圖案,以進一步確保安全"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請務必輸入 PIN 碼,以進一步確保安全"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請務必輸入密碼,以進一步確保安全"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"裝置已由管理員鎖定"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"使用者已手動將裝置上鎖"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"未能識別"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"如要使用「面孔解鎖」,請在「設定」開啟相機存取權"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{輸入 SIM 卡的 PIN,您還可以再試 # 次。如果仍然輸入錯誤,您必須聯絡流動網絡供應商解鎖您的裝置。}other{輸入 SIM 卡的 PIN。您還可以再試 # 次。}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM 卡已停用。請輸入 PUK 碼以繼續進行。您還可以再試 # 次。如果仍然輸入錯誤,SIM 卡將永久無法使用。詳情請向流動網絡供應商查詢。}other{SIM 卡已停用。請輸入 PUK 碼以繼續進行。您還可以再試 # 次。如果仍然輸入錯誤,SIM 卡將永久無法使用。詳情請向流動網絡供應商查詢。}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"如要使用「面孔解鎖」,請在 [設定] &gt; [私隱] 開啟"<b>"相機存取權"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">輸入 SIM 卡的 PIN,您還可以再試 <xliff:g id="NUMBER_1">%d</xliff:g> 次。</item>
+      <item quantity="one">輸入 SIM 卡的 PIN,您還可以再試 <xliff:g id="NUMBER_0">%d</xliff:g> 次。如果仍然輸入錯誤,您必須聯絡流動網絡供應商解鎖您的裝置。</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM 卡已停用。請輸入 PUK 碼以繼續進行。您還可以再試 <xliff:g id="_NUMBER_1">%d</xliff:g> 次。如果仍然輸入錯誤,SIM 卡將永久無法使用。詳情請與流動網絡供應商聯絡。</item>
+      <item quantity="one">SIM 卡已停用。請輸入 PUK 碼以繼續進行。您還可以再試 <xliff:g id="_NUMBER_0">%d</xliff:g> 次。如果仍然輸入錯誤,SIM 卡將永久無法使用。詳情請與流動網絡供應商聯絡。</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"預設"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"指針"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"解鎖裝置以繼續"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
index bb9a448..f34f78e 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"卡片無效。"</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"充電完成"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 無線充電"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電中"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 正在座架上充電"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 充電中"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 快速充電中"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 慢速充電中"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 為保護電池,充電效能已最佳化"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • 已暫時限制充電"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"按選單鍵解鎖。"</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"網路已鎖定"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"沒有 SIM 卡"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"請新增 SIM 卡。"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"找不到 SIM 卡或 SIM 卡無法讀取,請新增 SIM 卡。"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"SIM 卡無法使用。"</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"SIM 卡已永久停用。\n 請向無線服務供應商索取其他 SIM 卡。"</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"SIM 卡已鎖定。"</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"SIM 卡已使用 PUK 碼鎖定。"</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"正在解鎖 SIM 卡…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"沒有 SIM 卡"</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"請插入 SIM 卡。"</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"找不到或無法讀取 SIM 卡。請插入 SIM 卡。"</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"SIM 卡無法使用。"</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"你的 SIM 卡已遭永久停用。\n請與你的無線網路服務供應商聯絡,以取得別張 SIM 卡。"</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"SIM 卡處於鎖定狀態。"</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"SIM 卡處於 PUK 鎖定狀態。"</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"正在解除 SIM 卡鎖定…"</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"PIN 區"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"裝置密碼"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"SIM 卡 PIN 區"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM 卡「<xliff:g id="CARRIER">%1$s</xliff:g>」現已遭停用,輸入 PUK 碼即可繼續使用。如需瞭解詳情,請與電信業者聯絡。"</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"輸入所需的 PIN 碼"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"確認所需的 PIN 碼"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"正在解鎖 SIM 卡…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"正在解除 SIM 卡鎖定…"</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"請輸入 4 到 8 碼的 PIN 碼。"</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK 碼至少必須為 8 碼。"</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"你已輸入錯誤的 PIN 碼 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次。\n\n請在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後再試一次。"</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"你已輸入錯誤的密碼 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次。\n\n請在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後再試一次。"</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"你已畫出錯誤的解鎖圖案 <xliff:g id="NUMBER_0">%1$d</xliff:g> 次。\n\n請在 <xliff:g id="NUMBER_1">%2$d</xliff:g> 秒後再試一次。"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"SIM 卡的 PIN 碼輸入錯誤,你現在必須請電信業者為裝置解鎖。"</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{SIM 卡 PIN 碼輸入錯誤,您還可以再試 # 次。如果仍然失敗,就必須聯絡電信業者為裝置解鎖。}other{SIM 卡 PIN 碼輸入錯誤,您還可以再試 # 次。}}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="other">SIM 卡的 PIN 碼輸入錯誤,你還可以再試 <xliff:g id="NUMBER_1">%d</xliff:g> 次。</item>
+      <item quantity="one">SIM 卡的 PIN 碼輸入錯誤,你還可以再試 <xliff:g id="NUMBER_0">%d</xliff:g> 次。如果仍然失敗,就必須請電信業者為裝置解鎖。</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM 卡無法使用,請與你的電信業者聯絡。"</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{SIM 卡 PUK 碼輸入錯誤,您還可以再試 # 次,如果仍然失敗,SIM 卡將永久無法使用。}other{SIM 卡 PUK 碼輸入錯誤,您還可以再試 # 次,如果仍然失敗,SIM 卡將永久無法使用。}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="other">SIM 卡的 PUK 碼輸入錯誤,你還可以再試 <xliff:g id="NUMBER_1">%d</xliff:g> 次。如果仍然失敗,SIM 卡將永久無法使用。</item>
+      <item quantity="one">SIM 卡的 PUK 碼輸入錯誤,你還可以再試 <xliff:g id="NUMBER_0">%d</xliff:g> 次。如果仍然失敗,SIM 卡將永久無法使用。</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM 卡 PIN 碼解鎖失敗!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM 卡 PUK 碼解鎖失敗!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"切換輸入法"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後需要畫出解鎖圖案"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後需要輸入 PIN 碼"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後需要輸入密碼"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"為強化安全性,請改用解鎖圖案"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"為強化安全性,請改用 PIN 碼"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"為強化安全性,請改用密碼"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請畫出解鎖圖案,以進一步確保資訊安全"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請輸入 PIN 碼,以進一步確保資訊安全"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請輸入密碼,以進一步確保資訊安全"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理員已鎖定裝置"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"裝置已手動鎖定"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"無法識別"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"如要使用人臉解鎖功能,請在「設定」中開啟相機存取權。"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{請輸入 SIM 卡 PIN 碼,您還可以再試 # 次,如果仍然失敗,就必須聯絡電信業者為裝置解鎖。}other{輸入 SIM 卡 PIN 碼您還可以再試 # 次}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM 卡現在已遭停用。請輸入 PUK 碼以繼續。您還可以再試 # 次,如果仍然失敗,SIM 卡將永久無法使用。詳情請洽詢電信業者。}other{SIM 卡現在已遭停用。請輸入 PUK 碼以繼續。您還可以再試 # 次,如果仍然失敗,SIM 卡將永久無法使用。詳情請洽詢電信業者。}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"如要使用人臉解鎖功能,請前往「設定」&gt;「隱私權」開啟"<b>"攝影機存取權"</b></string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="other">請輸入 SIM 卡的 PIN 碼,你還可以再試 <xliff:g id="NUMBER_1">%d</xliff:g> 次。</item>
+      <item quantity="one">請輸入 SIM 卡的 PIN 碼,你還可以再試 <xliff:g id="NUMBER_0">%d</xliff:g> 次。如果仍然失敗,就必須請電信業者為裝置解鎖。</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="other">SIM 卡現在已遭停用。請輸入 PUK 碼以繼續進行。你還可以再試 <xliff:g id="_NUMBER_1">%d</xliff:g> 次,如果仍然失敗,SIM 卡將永久無法使用。詳情請與電信業者聯絡。</item>
+      <item quantity="one">SIM 卡現在已遭停用。請輸入 PUK 碼以繼續進行。你還可以再試 <xliff:g id="_NUMBER_0">%d</xliff:g> 次,如果仍然失敗,SIM 卡將永久無法使用。詳情請與電信業者聯絡。</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"預設"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"類比"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"解鎖裝置才能繼續操作"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zu/strings.xml b/packages/SystemUI/res-keyguard/values-zu/strings.xml
index 75273fb..76e0580 100644
--- a/packages/SystemUI/res-keyguard/values-zu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zu/strings.xml
@@ -26,21 +26,21 @@
     <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Ikhadi elingavumelekile."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Kushajiwe"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Iyashaja ngaphandle kwentambo"</string>
-    <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Iyashaja"</string>
+    <string name="keyguard_plugged_in_dock" msgid="449722738270908674">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Idokhu Yokushaja"</string>
     <string name="keyguard_plugged_in" msgid="8169926454348380863">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Iyashaja"</string>
     <string name="keyguard_plugged_in_charging_fast" msgid="4386594091107340426">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ishaja kaningi"</string>
     <string name="keyguard_plugged_in_charging_slowly" msgid="217655355424210">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ishaja kancane"</string>
-    <string name="keyguard_plugged_in_charging_limited" msgid="1053130519456324630">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ukushaja kuthuthukisiwe ukuze kuvikelwe ibhethri"</string>
+    <string name="keyguard_plugged_in_charging_limited" msgid="6091488837901216962">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Ukushaja kukhawulelwe okwesikhashana"</string>
     <string name="keyguard_instructions_when_pattern_disabled" msgid="8448804180089936954">"Chofoza Menyu ukuvula."</string>
     <string name="keyguard_network_locked_message" msgid="407096292844868608">"Inethiwekhi ivaliwe"</string>
-    <string name="keyguard_missing_sim_message_short" msgid="685029586173458728">"Ayikho i-SIM"</string>
-    <string name="keyguard_missing_sim_instructions" msgid="7735360104844653246">"engeza i-SIM"</string>
-    <string name="keyguard_missing_sim_instructions_long" msgid="3451467338947610268">"I-SIM ayitholakali noma ayifundeki. engeza i-SIM"</string>
-    <string name="keyguard_permanent_disabled_sim_message_short" msgid="3955052454216046100">"I-SIM engasebenziseki."</string>
-    <string name="keyguard_permanent_disabled_sim_instructions" msgid="5034635040020685428">"I-SIM yakho iyekiswe ukusebenza unomphela.\n Xhumana nomhlinzeki wakho wesevisi ngokungenazintambo ukuze uthole enye i-SIM."</string>
-    <string name="keyguard_sim_locked_message" msgid="7095293254587575270">"I-SIM ikhiyiwe."</string>
-    <string name="keyguard_sim_puk_locked_message" msgid="2503428315518592542">"I-SIM ikhiyiwe nge-PUK."</string>
-    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="8489092646014631659">"Ivula i-SIM…"</string>
+    <string name="keyguard_missing_sim_message_short" msgid="704159478161444907">"Alikho ikhadi le-SIM."</string>
+    <string name="keyguard_missing_sim_instructions" msgid="1162120926141335918">"Faka ikhadi le-SIM."</string>
+    <string name="keyguard_missing_sim_instructions_long" msgid="2712623293749378570">"Ikhadi le-SIM alitholakali noma alifundeki. Sicela ufake ikhadi le-SIM."</string>
+    <string name="keyguard_permanent_disabled_sim_message_short" msgid="5842745213110966962">"Ikhadi le-SIM elingasetshenzisiwe."</string>
+    <string name="keyguard_permanent_disabled_sim_instructions" msgid="2490584154727897806">"Ikhadi le-SIM lakho likhutshazwe unomphela.\n Xhumana nomhlinzeki wakho wokuxhumana okungenazintambo ukuze uthole enye i-SIM khadi."</string>
+    <string name="keyguard_sim_locked_message" msgid="4343544458476911044">"Ikhadi le-SIM livaliwe."</string>
+    <string name="keyguard_sim_puk_locked_message" msgid="6253830777745450550">"Ikhadi le-SIM livalwe nge-PUK."</string>
+    <string name="keyguard_sim_unlock_progress_dialog_message" msgid="2394023844117630429">"Ivula ikhadi le-SIM..."</string>
     <string name="keyguard_accessibility_pin_area" msgid="7403009340414014734">"Indawo yephinikhodi"</string>
     <string name="keyguard_accessibility_password" msgid="3524161948484801450">"Iphasiwedi yedivayisi"</string>
     <string name="keyguard_accessibility_sim_pin_area" msgid="6272116591533888062">"Indawo yephinikhodi ye-SIM"</string>
@@ -61,16 +61,22 @@
     <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"I-SIM ye-\"<xliff:g id="CARRIER">%1$s</xliff:g>\" manje ikhutshaziwe. Faka ikhodi ye-PUK ukuze uqhubeke. Xhumana nenkampani yenethiwekhi ukuze uthole imininingwane."</string>
     <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Faka iphinikhodi oyithandayo"</string>
     <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Qinisekisa iphinikhodi oyithandayo"</string>
-    <string name="kg_sim_unlock_progress_dialog_message" msgid="1123048780346295748">"Ivula i-SIM…"</string>
+    <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Ivula ikhadi le-SIM..."</string>
     <string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"Thayipha i-PIN enezinombolo ezingu-4 kuya kwezingu-8."</string>
     <string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"Ikhodi ye-PUK kufanele ibe yizinombolo ezingu-8 noma eziningi."</string>
     <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"Ubhale iphinikhodi ykho ngendlela engafanele izikhathi ezingu-<xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\nZama futhi emasekhondini angu-<xliff:g id="NUMBER_1">%2$d</xliff:g>."</string>
     <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"Ubhale iphasiwedi yakho ngendlela engafanele <xliff:g id="NUMBER_0">%1$d</xliff:g> izikhathi. \n\nZama futhi emasekhondini angu-<xliff:g id="NUMBER_1">%2$d</xliff:g>."</string>
     <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"Udwebe iphathini yakho yokuvula ngendlela engafanele-<xliff:g id="NUMBER_0">%1$d</xliff:g>. \n\n Zama futhi emasekhondini angu-<xliff:g id="NUMBER_1">%2$d</xliff:g>"</string>
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Ikhodi yephinikhodi ye-SIM engalungile manje kumele uxhumane nenkampini yenethiwekhi yakho ukuvula idivayisi yakho."</string>
-    <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Ikhodi engalungile Yephinikhodi ye-SIM, unomzamo ongu-# osele ngaphambi kokuba uxhumane nenkampani yakho yenethiwekhi ukuvula idivayisi yakho.}one{Ikhodi engalungile Yephinikhodi ye-SIM, unemizamo engu-# esele. }other{Ikhodi engalungile Yephinikhodi ye-SIM, unemizamo engu-# esele. }}"</string>
+    <plurals name="kg_password_wrong_pin_code" formatted="false" msgid="7030584350995485026">
+      <item quantity="one">Ikhodi engalungile yephinikhodi ye-SIM, unemizamo engu-<xliff:g id="NUMBER_1">%d</xliff:g> esele.</item>
+      <item quantity="other">Ikhodi engalungile yephinikhodi ye-SIM, unemizamo engu-<xliff:g id="NUMBER_1">%d</xliff:g> esele.</item>
+    </plurals>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"I-SIM ayisebenziseki. Xhumana nemkampini yenethiwekhi yakho."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Ikhodi ye-PUK ye-SIM engalungile, unemizamo engu-# esele ngaphambi kokuba i-SIM ibe engasasebenziseki unomphela.}one{Ikhodi ye-PUK ye-SIM engalungile, unemizamo engu-# esele ngaphambi kokuthi i-SIM ibe engasasebenziseki unomphela.}other{Ikhodi ye-PUK ye-SIM engalungile, unemizamo engu-# esele ngaphambi kokuthi i-SIM ibe engasasebenziseki unomphela.}}"</string>
+    <plurals name="kg_password_wrong_puk_code" formatted="false" msgid="3937306685604862886">
+      <item quantity="one">Ikhodi ye-PUK ye-SIM engalungile, unemizamo engu-<xliff:g id="NUMBER_1">%d</xliff:g> esele ngaphambi kokuthi i-SIM ingasasebenziseki unaphakade.</item>
+      <item quantity="other">Ikhodi ye-PUK ye-SIM engalungile, unemizamo engu-<xliff:g id="NUMBER_1">%d</xliff:g> esele ngaphambi kokuthi i-SIM ingasasebenziseki unaphakade.</item>
+    </plurals>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"Umsebenzi wephinikhodi ye-SIM wehlulekile!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"Umsebenzi we-PUK ye-SIM wehlulekile!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Shintsha indlela yokufaka"</string>
@@ -78,17 +84,22 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iphethini iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iphinikhodi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iphasiwedi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Ukuze uthole ukuvikeleka okwengeziwe, sebenzisa iphetheni esikhundleni salokho"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Ukuze uthole ukuvikeleka okwengeziwe, sebenzisa i-PIN esikhundleni salokho"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Ukuze uthole ukuvikeleka okwengeziwe, sebenzisa iphasiwedi esikhundleni salokho"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kudingeka iphethini  ngokuvikeleka okungeziwe"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kudingeka iphinikhodi ngokuvikeleka okungeziwe"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Iphasiwedi idingelwa ukuvikela okungeziwe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Idivayisi ikhiywe ngumlawuli"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Idivayisi ikhiywe ngokwenza"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Akwaziwa"</string>
-    <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Ukuze usebenzise Ukuvula Ngobuso, vula ukufinyelela kwekhamera Kumasethingi"</string>
-    <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Faka Iphinikhodi ye-SIM. Unemizamo engu-# esele ngaphambi kokuthi uxhumane nenkampani yakho yenethiwekhi ukuze uvule idivayisi yakho.}one{Faka Iphinikhodi ye-SIM Unemizamo engu-# esele.}other{Faka Iphinikhodi ye-SIM Unemizamo engu-# esele.}}"</string>
-    <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{I-SIM manje ikhutshaziwe. Faka ikhodi ye-PUK ukuze uqhubeke. Unomzamo ongu-# osele ngaphambi kokuthi i-SIM ingasasebenziseki unomphela. Xhumana nenkampani yenethiwekhi ngemininingwane.}one{I-SIM manje ikhutshaziwe. Faka ikhodi ye-PUK ukuze uqhubeke. Unemizamo engu-# esele ngaphambi kokuthi i-SIM ingasasebenziseki unomphela. Xhumana nenkampani yenethiwekhi ngemininingwane.}other{I-SIM manje ikhutshaziwe. Faka ikhodi ye-PUK ukuze uqhubeke. Unemizamo engu-# esele ngaphambi kokuthi i-SIM ingasasebenziseki unomphela. Xhumana nenkampani yenethiwekhi ngemininingwane.}}"</string>
+    <string name="kg_face_sensor_privacy_enabled" msgid="6513157891227284806">"Ukuze usebenzise Ukuvula ngobuso, vula "<b>"Ukufinyelela kwekhamera"</b>" kokuthi Amasethingi &gt; Ubumfihlo"</string>
+    <plurals name="kg_password_default_pin_message" formatted="false" msgid="7730152526369857818">
+      <item quantity="one">Faka i-PIN ye-SIM, unemizamo engu-<xliff:g id="NUMBER_1">%d</xliff:g> esele.</item>
+      <item quantity="other">Faka i-PIN ye-SIM, unemizamo engu-<xliff:g id="NUMBER_1">%d</xliff:g> esele.</item>
+    </plurals>
+    <plurals name="kg_password_default_puk_message" formatted="false" msgid="571308542462946935">
+      <item quantity="one">I-SIM manje ikhutshaziwe. Faka ikhodi ye-PUK ukuze uqhubeke. Unemizamo engu-<xliff:g id="_NUMBER_1">%d</xliff:g> esele ngaphambi kokuthi i-SIM ingasebenziseki unaphakade. Xhumana nenkampani yenethiwekhi ngemininingwane.</item>
+      <item quantity="other">I-SIM manje ikhutshaziwe. Faka ikhodi ye-PUK ukuze uqhubeke. Unemizamo engu-<xliff:g id="_NUMBER_1">%d</xliff:g> esele ngaphambi kokuthi i-SIM ingasebenziseki unaphakade. Xhumana nenkampani yenethiwekhi ngemininingwane.</item>
+    </plurals>
     <string name="clock_title_default" msgid="6342735240617459864">"Okuzenzekelayo"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Ibhamuza"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"I-Analog"</string>
-    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Vula idivayisi yakho ukuze uqhubeke"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index c5ffdc0..992d143 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -29,9 +29,6 @@
          (includes 2x keyguard_security_view_top_margin) -->
     <dimen name="keyguard_security_height">420dp</dimen>
 
-    <!-- Max Height of the sliding KeyguardSecurityContainer
-         (includes 2x keyguard_security_view_top_margin) -->
-
     <!-- pin/password field max height -->
     <dimen name="keyguard_password_height">80dp</dimen>
 
@@ -81,7 +78,7 @@
     <!-- The vertical margin between the date and the owner info. -->
 
     <!-- The translation for disappearing security views after having solved them. -->
-    <dimen name="disappear_y_translation">-32dp</dimen>
+    <dimen name="disappear_y_translation">-50dp</dimen>
 
     <!-- Dimens for animation for the Bouncer PIN view -->
     <dimen name="pin_view_trans_y_entry">120dp</dimen>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background.xml
similarity index 65%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to packages/SystemUI/res/drawable/clipboard_minimized_background.xml
index 0d88113..a179c14 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/packages/SystemUI/res/drawable/clipboard_minimized_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ 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.
@@ -14,8 +14,10 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+    <solid android:color="?androidprv:attr/colorAccentSecondary"/>
+    <corners android:radius="10dp"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/controls_panel_background.xml b/packages/SystemUI/res/drawable/controls_panel_background.xml
index 9092877..fc108a5 100644
--- a/packages/SystemUI/res/drawable/controls_panel_background.xml
+++ b/packages/SystemUI/res/drawable/controls_panel_background.xml
@@ -18,5 +18,5 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
     <solid android:color="#1F1F1F" />
-    <corners android:radius="@dimen/notification_corner_radius" />
+    <corners android:radius="@dimen/controls_panel_corner_radius" />
 </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_camera.xml b/packages/SystemUI/res/drawable/ic_camera.xml
new file mode 100644
index 0000000..ef1406c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_camera.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M7,42Q5.8,42 4.9,41.1Q4,40.2 4,39V13.35Q4,12.15 4.9,11.25Q5.8,10.35 7,10.35H14.35L18,6H30L33.65,10.35H41Q42.2,10.35 43.1,11.25Q44,12.15 44,13.35V39Q44,40.2 43.1,41.1Q42.2,42 41,42ZM7,39H41Q41,39 41,39Q41,39 41,39V13.35Q41,13.35 41,13.35Q41,13.35 41,13.35H7Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM7,39Q7,39 7,39Q7,39 7,39V13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM24,34.7Q27.5,34.7 30,32.225Q32.5,29.75 32.5,26.2Q32.5,22.7 30,20.2Q27.5,17.7 24,17.7Q20.45,17.7 17.975,20.2Q15.5,22.7 15.5,26.2Q15.5,29.75 17.975,32.225Q20.45,34.7 24,34.7ZM24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_content_paste.xml b/packages/SystemUI/res/drawable/ic_content_paste.xml
new file mode 100644
index 0000000..8c8b81e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_content_paste.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48"
+        android:viewportHeight="48"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M9,42Q7.7,42 6.85,41.15Q6,40.3 6,39V9Q6,7.7 6.85,6.85Q7.7,6 9,6H19.1Q19.45,4.25 20.825,3.125Q22.2,2 24,2Q25.8,2 27.175,3.125Q28.55,4.25 28.9,6H39Q40.3,6 41.15,6.85Q42,7.7 42,9V39Q42,40.3 41.15,41.15Q40.3,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H36V13.5H12V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM24,9Q24.85,9 25.425,8.425Q26,7.85 26,7Q26,6.15 25.425,5.575Q24.85,5 24,5Q23.15,5 22.575,5.575Q22,6.15 22,7Q22,7.85 22.575,8.425Q23.15,9 24,9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml
new file mode 100644
index 0000000..08c5aaf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="13dp"
+    android:height="13dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_shortcut_keyguard.xml
similarity index 92%
copy from packages/SystemUI/res/drawable/ic_note_task_button.xml
copy to packages/SystemUI/res/drawable/ic_note_task_shortcut_keyguard.xml
index bb5e224..ee8d4883 100644
--- a/packages/SystemUI/res/drawable/ic_note_task_button.xml
+++ b/packages/SystemUI/res/drawable/ic_note_task_shortcut_keyguard.xml
@@ -19,10 +19,13 @@
     android:viewportHeight="24"
     android:viewportWidth="24">
     <path
-        android:fillColor="#636C6F"
+        android:fillAlpha="1"
+        android:fillColor="@android:color/white"
+        android:fillType="nonZero"
         android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" />
     <path
-        android:fillColor="#636C6F"
-        android:fillType="evenOdd"
+        android:fillAlpha="1"
+        android:fillColor="@android:color/white"
+        android:fillType="nonZero"
         android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" />
 </vector>
diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_shortcut_widget.xml
similarity index 95%
rename from packages/SystemUI/res/drawable/ic_note_task_button.xml
rename to packages/SystemUI/res/drawable/ic_note_task_shortcut_widget.xml
index bb5e224..7590182 100644
--- a/packages/SystemUI/res/drawable/ic_note_task_button.xml
+++ b/packages/SystemUI/res/drawable/ic_note_task_shortcut_widget.xml
@@ -19,10 +19,13 @@
     android:viewportHeight="24"
     android:viewportWidth="24">
     <path
+        android:fillAlpha="1"
         android:fillColor="#636C6F"
+        android:fillType="nonZero"
         android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" />
     <path
+        android:fillAlpha="1"
         android:fillColor="#636C6F"
-        android:fillType="evenOdd"
+        android:fillType="nonZero"
         android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" />
 </vector>
diff --git a/packages/SystemUI/res/drawable/ic_progress_activity.xml b/packages/SystemUI/res/drawable/ic_progress_activity.xml
new file mode 100644
index 0000000..abf0625
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_progress_activity.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M24,44Q19.8,44 16.15,42.45Q12.5,40.9 9.8,38.2Q7.1,35.5 5.55,31.85Q4,28.2 4,24Q4,19.8 5.55,16.15Q7.1,12.5 9.8,9.8Q12.5,7.1 16.15,5.55Q19.8,4 24,4Q24.6,4 25.05,4.45Q25.5,4.9 25.5,5.5Q25.5,6.1 25.05,6.55Q24.6,7 24,7Q16.95,7 11.975,11.975Q7,16.95 7,24Q7,31.05 11.975,36.025Q16.95,41 24,41Q31.05,41 36.025,36.025Q41,31.05 41,24Q41,23.4 41.45,22.95Q41.9,22.5 42.5,22.5Q43.1,22.5 43.55,22.95Q44,23.4 44,24Q44,28.2 42.45,31.85Q40.9,35.5 38.2,38.2Q35.5,40.9 31.85,42.45Q28.2,44 24,44Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_qs_font_scaling.xml b/packages/SystemUI/res/drawable/ic_qs_font_scaling.xml
new file mode 100644
index 0000000..d5b4c9e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_font_scaling.xml
@@ -0,0 +1,25 @@
+<!--
+   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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+         android:width="24dp"
+         android:height="24dp"
+         android:viewportWidth="24"
+         android:viewportHeight="24">
+<path
+    android:pathData="M7,20L7,7L2,7L2,4h13v3h-5v13ZM16,20v-8h-3L13,9h9v3h-3v8Z"
+    android:fillColor="#041E49"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_ring_volume.xml b/packages/SystemUI/res/drawable/ic_ring_volume.xml
new file mode 100644
index 0000000..343fe5d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_ring_volume.xml
@@ -0,0 +1,26 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+  <path
+      android:pathData="M11,7V2H13V7ZM17.6,9.85 L16.2,8.4 19.75,4.85 21.15,6.3ZM6.4,9.85 L2.85,6.3 4.25,4.85 7.8,8.4ZM12,12Q14.95,12 17.812,13.188Q20.675,14.375 22.9,16.75Q23.2,17.05 23.2,17.45Q23.2,17.85 22.9,18.15L20.6,20.4Q20.325,20.675 19.963,20.7Q19.6,20.725 19.3,20.5L16.4,18.3Q16.2,18.15 16.1,17.95Q16,17.75 16,17.5V14.65Q15.05,14.35 14.05,14.175Q13.05,14 12,14Q10.95,14 9.95,14.175Q8.95,14.35 8,14.65V17.5Q8,17.75 7.9,17.95Q7.8,18.15 7.6,18.3L4.7,20.5Q4.4,20.725 4.038,20.7Q3.675,20.675 3.4,20.4L1.1,18.15Q0.8,17.85 0.8,17.45Q0.8,17.05 1.1,16.75Q3.3,14.375 6.175,13.188Q9.05,12 12,12ZM6,15.35Q5.275,15.725 4.6,16.212Q3.925,16.7 3.2,17.3L4.2,18.3L6,16.9ZM18,15.4V16.9L19.8,18.3L20.8,17.35Q20.075,16.7 19.4,16.225Q18.725,15.75 18,15.4ZM6,15.35Q6,15.35 6,15.35Q6,15.35 6,15.35ZM18,15.4Q18,15.4 18,15.4Q18,15.4 18,15.4Z"
+      android:fillColor="?android:attr/colorPrimary"/>
+
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_ring_volume_off.xml b/packages/SystemUI/res/drawable/ic_ring_volume_off.xml
new file mode 100644
index 0000000..74f30d1
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_ring_volume_off.xml
@@ -0,0 +1,34 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+<path
+      android:pathData="M0.8,4.2l8.1,8.1c-2.2,0.5 -5.2,1.6 -7.8,4.4c-0.4,0.4 -0.4,1 0,1.4l2.3,2.3c0.3,0.3 0.9,0.4 1.3,0.1l2.9,-2.2C7.8,18.1 8,17.8 8,17.5v-2.9c0.9,-0.3 1.7,-0.5 2.7,-0.6l8.5,8.5l1.4,-1.4L2.2,2.8L0.8,4.2z"
+    android:fillColor="?android:attr/colorPrimary"/>
+  <path
+      android:pathData="M11,2h2v5h-2z"
+      android:fillColor="?android:attr/colorPrimary"/>
+  <path
+      android:pathData="M21.2,6.3l-1.4,-1.4l-3.6,3.6l1.4,1.4C17.6,9.8 21,6.3 21.2,6.3z"
+      android:fillColor="?android:attr/colorPrimary"/>
+  <path
+      android:pathData="M22.9,16.7c-2.8,-3 -6.2,-4.1 -8.4,-4.5l7.2,7.2l1.3,-1.3C23.3,17.7 23.3,17.1 22.9,16.7z"
+      android:fillColor="?android:attr/colorPrimary"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_speaker_mute.xml b/packages/SystemUI/res/drawable/ic_speaker_mute.xml
new file mode 100644
index 0000000..4e402cf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_speaker_mute.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary"
+    android:autoMirrored="true">
+    <path android:fillColor="#FFFFFFFF"
+        android:pathData="M19.8,22.6 L16.775,19.575Q16.15,19.975 15.45,20.263Q14.75,20.55 14,20.725V18.675Q14.35,18.55 14.688,18.425Q15.025,18.3 15.325,18.125L12,14.8V20L7,15H3V9H6.2L1.4,4.2L2.8,2.8L21.2,21.2ZM19.6,16.8 L18.15,15.35Q18.575,14.575 18.788,13.725Q19,12.875 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,13.3 20.638,14.525Q20.275,15.75 19.6,16.8ZM16.25,13.45 L14,11.2V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,12.375 16.438,12.738Q16.375,13.1 16.25,13.45ZM12,9.2 L9.4,6.6 12,4ZM10,15.15V12.8L8.2,11H5V13H7.85ZM9.1,11.9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_speaker_on.xml b/packages/SystemUI/res/drawable/ic_speaker_on.xml
new file mode 100644
index 0000000..2a90e05
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_speaker_on.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary"
+    android:autoMirrored="true">
+    <path android:fillColor="#FFFFFFFF"
+        android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,13.275 15.838,14.362Q15.175,15.45 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_videocam.xml b/packages/SystemUI/res/drawable/ic_videocam.xml
new file mode 100644
index 0000000..de2bc7b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_videocam.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M7,40Q5.8,40 4.9,39.1Q4,38.2 4,37V11Q4,9.8 4.9,8.9Q5.8,8 7,8H33Q34.2,8 35.1,8.9Q36,9.8 36,11V21.75L44,13.75V34.25L36,26.25V37Q36,38.2 35.1,39.1Q34.2,40 33,40ZM7,37H33Q33,37 33,37Q33,37 33,37V11Q33,11 33,11Q33,11 33,11H7Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37ZM7,37Q7,37 7,37Q7,37 7,37V11Q7,11 7,11Q7,11 7,11Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
index 18fcebb..87b5a4c 100644
--- a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
@@ -16,53 +16,13 @@
 * limitations under the License.
 */
 -->
-<selector
+<shape
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-
-  <item android:state_selected="true">
-    <layer-list>
-      <item
-          android:left="3dp"
-          android:top="3dp"
-          android:right="3dp"
-          android:bottom="3dp">
-        <shape android:shape="oval">
-          <solid android:color="?androidprv:attr/colorSurface"/>
-          <size
-              android:width="@dimen/keyguard_affordance_width"
-              android:height="@dimen/keyguard_affordance_height"/>
-        </shape>
-      </item>
-
-      <item>
-        <shape android:shape="oval">
-          <stroke
-              android:color="@color/control_primary_text"
-              android:width="2dp"/>
-          <size
-              android:width="@dimen/keyguard_affordance_width"
-              android:height="@dimen/keyguard_affordance_height"/>
-        </shape>
-      </item>
-    </layer-list>
-  </item>
-
-  <item>
-    <layer-list>
-      <item
-          android:left="3dp"
-          android:top="3dp"
-          android:right="3dp"
-          android:bottom="3dp">
-        <shape android:shape="oval">
-          <solid android:color="?androidprv:attr/colorSurface"/>
-          <size
-              android:width="@dimen/keyguard_affordance_width"
-              android:height="@dimen/keyguard_affordance_height"/>
-        </shape>
-      </item>
-    </layer-list>
-  </item>
-
-</selector>
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+  <solid android:color="?androidprv:attr/colorSurface"/>
+  <size
+      android:width="@dimen/keyguard_affordance_fixed_width"
+      android:height="@dimen/keyguard_affordance_fixed_height"/>
+  <corners android:radius="@dimen/keyguard_affordance_fixed_radius" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_selected_border.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_selected_border.xml
new file mode 100644
index 0000000..7d03b0d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_selected_border.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 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.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <item android:state_selected="true">
+    <shape android:shape="oval">
+      <stroke
+          android:color="?android:attr/textColorPrimary"
+          android:width="2dp"/>
+      <size
+          android:width="@dimen/keyguard_affordance_fixed_width"
+          android:height="@dimen/keyguard_affordance_fixed_height"/>
+    </shape>
+  </item>
+</selector>
diff --git a/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
new file mode 100644
index 0000000..3807b92
--- /dev/null
+++ b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
@@ -0,0 +1,33 @@
+<?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.
+  ~
+  -->
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/white"/>
+            <corners android:radius="28dp" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="?androidprv:attr/colorSurface" />
+            <corners android:radius="28dp" />
+        </shape>
+    </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml
index 857632e..53122c1 100644
--- a/packages/SystemUI/res/drawable/overlay_badge_background.xml
+++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
+  ~ Copyright (C) 2022 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,8 +14,11 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-        android:shape="oval">
-    <solid android:color="?androidprv:attr/colorSurface"/>
-</shape>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M0,0M48,48"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/qs_footer_edit_circle.xml b/packages/SystemUI/res/drawable/qs_footer_edit_circle.xml
new file mode 100644
index 0000000..2e29cae
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_footer_edit_circle.xml
@@ -0,0 +1,36 @@
+<?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.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       android:inset="@dimen/qs_footer_action_inset">
+    <ripple
+        android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <!-- We make this shape a rounded rectangle instead of a oval so that it can animate -->
+            <!-- properly into an app/dialog. -->
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/white"/>
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
+            </shape>
+        </item>
+
+    </ripple>
+</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/statusbar_chip_bg.xml b/packages/SystemUI/res/drawable/statusbar_chip_bg.xml
new file mode 100644
index 0000000..d7de16d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/statusbar_chip_bg.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <solid android:color="?androidprv:attr/colorAccentPrimary" />
+    <corners android:radius="@dimen/ongoing_appops_chip_bg_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_chip_bg.xml b/packages/SystemUI/res/drawable/statusbar_privacy_chip_bg.xml
similarity index 100%
rename from packages/SystemUI/res/drawable/privacy_chip_bg.xml
rename to packages/SystemUI/res/drawable/statusbar_privacy_chip_bg.xml
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index a3dd334..3505a3e 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -71,8 +71,8 @@
         <com.android.internal.widget.LockPatternView
             android:id="@+id/lockPattern"
             android:layout_gravity="center"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
+            android:layout_width="@dimen/biometric_auth_pattern_view_size"
+            android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
 
         <TextView
             android:id="@+id/error"
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education.xml b/packages/SystemUI/res/layout/activity_rear_display_education.xml
index f5fc48c..094807e 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education.xml
@@ -41,9 +41,10 @@
     </androidx.cardview.widget.CardView>
 
     <TextView
+        android:id="@+id/rear_display_title_text_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="@string/rear_display_fold_bottom_sheet_title"
+        android:text="@string/rear_display_folded_bottom_sheet_title"
         android:textAppearance="@style/TextAppearance.Dialog.Title"
         android:lineSpacingExtra="2sp"
         android:paddingTop="@dimen/rear_display_title_top_padding"
@@ -54,7 +55,7 @@
     <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="@string/rear_display_bottom_sheet_description"
+        android:text="@string/rear_display_folded_bottom_sheet_description"
         android:textAppearance="@style/TextAppearance.Dialog.Body"
         android:lineSpacingExtra="2sp"
         android:translationY="-1.24sp"
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
index 6de06f7..e970bc5 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
@@ -42,9 +42,10 @@
     </androidx.cardview.widget.CardView>
 
     <TextView
+        android:id="@+id/rear_display_title_text_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="@string/rear_display_unfold_bottom_sheet_title"
+        android:text="@string/rear_display_unfolded_bottom_sheet_title"
         android:textAppearance="@style/TextAppearance.Dialog.Title"
         android:lineSpacingExtra="2sp"
         android:paddingTop="@dimen/rear_display_title_top_padding"
@@ -55,21 +56,11 @@
     <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="@string/rear_display_bottom_sheet_description"
+        android:text="@string/rear_display_unfolded_bottom_sheet_description"
         android:textAppearance="@style/TextAppearance.Dialog.Body"
         android:lineSpacingExtra="2sp"
         android:translationY="-1.24sp"
         android:gravity="center_horizontal|top"
     />
 
-    <TextView
-        android:id="@+id/rear_display_warning_text_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/rear_display_bottom_sheet_warning"
-        android:textAppearance="@style/TextAppearance.Dialog.Body"
-        android:lineSpacingExtra="2sp"
-        android:gravity="center_horizontal|top"
-    />
-
 </LinearLayout>
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index 4af9970..147ea82 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -67,8 +67,8 @@
         <com.android.internal.widget.LockPatternView
             android:id="@+id/lockPattern"
             android:layout_gravity="center"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
+            android:layout_width="@dimen/biometric_auth_pattern_view_size"
+            android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
 
         <TextView
             android:id="@+id/error"
diff --git a/packages/SystemUI/res/layout/battery_status_chip.xml b/packages/SystemUI/res/layout/battery_status_chip.xml
new file mode 100644
index 0000000..ff68ac0
--- /dev/null
+++ b/packages/SystemUI/res/layout/battery_status_chip.xml
@@ -0,0 +1,43 @@
+<?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.
+-->
+
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:layout_gravity="center_vertical|end"
+    tools:parentTag="com.android.systemui.statusbar.BatteryStatusChip">
+
+    <LinearLayout
+        android:id="@+id/rounded_container"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/ongoing_appops_chip_height"
+        android:layout_gravity="center"
+        android:background="@drawable/statusbar_chip_bg"
+        android:clipToOutline="true"
+        android:gravity="center"
+        android:maxWidth="@dimen/ongoing_appops_chip_max_width"
+        android:minWidth="@dimen/ongoing_appops_chip_min_width">
+
+        <com.android.systemui.battery.BatteryMeterView
+            android:id="@+id/battery_meter_view"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_marginHorizontal="10dp" />
+
+    </LinearLayout>
+</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml
index bc97e51..0ff944c 100644
--- a/packages/SystemUI/res/layout/chipbar.xml
+++ b/packages/SystemUI/res/layout/chipbar.xml
@@ -23,6 +23,7 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
+    <!-- Extra marginBottom to give room for the drop shadow. -->
     <LinearLayout
         android:id="@+id/chipbar_inner"
         android:orientation="horizontal"
@@ -33,6 +34,8 @@
         android:layout_marginTop="20dp"
         android:layout_marginStart="@dimen/notification_side_paddings"
         android:layout_marginEnd="@dimen/notification_side_paddings"
+        android:translationZ="4dp"
+        android:layout_marginBottom="8dp"
         android:clipToPadding="false"
         android:gravity="center_vertical"
         android:alpha="0.0"
@@ -57,14 +60,13 @@
             />
 
         <!-- At most one of [loading, failure_icon, undo] will be visible at a time. -->
-        <ProgressBar
+        <ImageView
             android:id="@+id/loading"
-            android:indeterminate="true"
             android:layout_width="@dimen/media_ttt_status_icon_size"
             android:layout_height="@dimen/media_ttt_status_icon_size"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
-            android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant"
-            style="?android:attr/progressBarStyleSmall"
+            android:src="@drawable/ic_progress_activity"
+            android:tint="?androidprv:attr/colorAccentPrimaryVariant"
             android:alpha="0.0"
             />
 
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 9134f96..297cf2b 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -32,26 +32,26 @@
         android:elevation="4dp"
         android:background="@drawable/action_chip_container_background"
         android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
-        app:layout_constraintBottom_toBottomOf="@+id/actions_container"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="@+id/actions_container"
-        app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
+        app:layout_constraintEnd_toEndOf="@+id/actions_container"
+        app:layout_constraintBottom_toBottomOf="parent"/>
     <HorizontalScrollView
         android:id="@+id/actions_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
-        android:paddingEnd="@dimen/overlay_action_container_padding_right"
+        android:paddingEnd="@dimen/overlay_action_container_padding_end"
         android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
         android:elevation="4dp"
         android:scrollbars="none"
-        android:layout_marginBottom="4dp"
         app:layout_constraintHorizontal_bias="0"
         app:layout_constraintWidth_percent="1.0"
         app:layout_constraintWidth_max="wrap"
-        app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toEndOf="@+id/preview_border"
-        app:layout_constraintEnd_toEndOf="parent">
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
         <LinearLayout
             android:id="@+id/actions"
             android:layout_width="wrap_content"
@@ -61,52 +61,36 @@
                      android:id="@+id/share_chip"/>
             <include layout="@layout/overlay_action_chip"
                      android:id="@+id/remote_copy_chip"/>
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/edit_chip"/>
         </LinearLayout>
     </HorizontalScrollView>
     <View
         android:id="@+id/preview_border"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:layout_marginStart="@dimen/overlay_offset_x"
-        android:layout_marginBottom="12dp"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginStart="@dimen/overlay_preview_container_margin"
+        android:layout_marginTop="@dimen/overlay_border_width_neg"
+        android:layout_marginEnd="@dimen/overlay_border_width_neg"
+        android:layout_marginBottom="@dimen/overlay_preview_container_margin"
         android:elevation="7dp"
-        app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
-        app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
-        android:background="@drawable/overlay_border"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/clipboard_preview_end"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierMargin="@dimen/overlay_border_width"
-        app:barrierDirection="end"
-        app:constraint_referenced_ids="clipboard_preview"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/clipboard_preview_top"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierDirection="top"
-        app:barrierMargin="@dimen/overlay_border_width_neg"
-        app:constraint_referenced_ids="clipboard_preview"/>
+        android:background="@drawable/overlay_border"
+        app:layout_constraintStart_toStartOf="@id/actions_container_background"
+        app:layout_constraintTop_toTopOf="@id/clipboard_preview"
+        app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
     <FrameLayout
         android:id="@+id/clipboard_preview"
+        android:layout_width="@dimen/clipboard_preview_size"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/overlay_border_width"
+        android:layout_marginBottom="@dimen/overlay_border_width"
+        android:layout_gravity="center"
         android:elevation="7dp"
         android:background="@drawable/overlay_preview_background"
         android:clipChildren="true"
         android:clipToOutline="true"
         android:clipToPadding="true"
-        android:layout_width="@dimen/clipboard_preview_size"
-        android:layout_margin="@dimen/overlay_border_width"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        app:layout_constraintHorizontal_bias="0"
-        app:layout_constraintBottom_toBottomOf="@id/preview_border"
         app:layout_constraintStart_toStartOf="@id/preview_border"
-        app:layout_constraintEnd_toEndOf="@id/preview_border"
-        app:layout_constraintTop_toTopOf="@id/preview_border">
+        app:layout_constraintBottom_toBottomOf="@id/preview_border">
         <TextView android:id="@+id/text_preview"
                   android:textFontWeight="500"
                   android:padding="8dp"
@@ -139,6 +123,45 @@
             android:layout_width="@dimen/clipboard_preview_size"
             android:layout_height="@dimen/clipboard_preview_size"/>
     </FrameLayout>
+    <LinearLayout
+        android:id="@+id/minimized_preview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:elevation="7dp"
+        android:padding="8dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        android:background="@drawable/clipboard_minimized_background">
+        <ImageView
+            android:src="@drawable/ic_content_paste"
+            android:tint="?attr/overlayButtonTextColor"
+            android:layout_width="24dp"
+            android:layout_height="24dp"/>
+        <ImageView
+            android:src="@*android:drawable/ic_chevron_end"
+            android:tint="?attr/overlayButtonTextColor"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:paddingEnd="-8dp"
+            android:paddingStart="-4dp"/>
+    </LinearLayout>
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/clipboard_content_top"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:barrierDirection="top"
+        app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/clipboard_content_end"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:barrierDirection="end"
+        app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
     <FrameLayout
         android:id="@+id/dismiss_button"
         android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
@@ -146,10 +169,10 @@
         android:elevation="10dp"
         android:visibility="gone"
         android:alpha="0"
-        app:layout_constraintStart_toEndOf="@id/clipboard_preview"
-        app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
-        app:layout_constraintTop_toTopOf="@id/clipboard_preview"
-        app:layout_constraintBottom_toTopOf="@id/clipboard_preview"
+        app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
+        app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
+        app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
+        app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
         android:contentDescription="@string/clipboard_dismiss_description">
         <ImageView
             android:id="@+id/dismiss_image"
diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
deleted file mode 100644
index 1a1fc75..0000000
--- a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
+++ /dev/null
@@ -1,160 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<com.android.systemui.screenshot.DraggableConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/clipboard_ui"
-    android:theme="@style/FloatingOverlay"
-    android:alpha="0"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:contentDescription="@string/clipboard_overlay_window_name">
-    <ImageView
-        android:id="@+id/actions_container_background"
-        android:visibility="gone"
-        android:layout_height="0dp"
-        android:layout_width="0dp"
-        android:elevation="4dp"
-        android:background="@drawable/action_chip_container_background"
-        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
-        app:layout_constraintBottom_toBottomOf="@+id/actions_container"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="@+id/actions_container"
-        app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
-    <HorizontalScrollView
-        android:id="@+id/actions_container"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
-        android:paddingEnd="@dimen/overlay_action_container_padding_right"
-        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
-        android:elevation="4dp"
-        android:scrollbars="none"
-        android:layout_marginBottom="4dp"
-        app:layout_constraintHorizontal_bias="0"
-        app:layout_constraintWidth_percent="1.0"
-        app:layout_constraintWidth_max="wrap"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toEndOf="@+id/preview_border"
-        app:layout_constraintEnd_toEndOf="parent">
-        <LinearLayout
-            android:id="@+id/actions"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:animateLayoutChanges="true">
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/share_chip"/>
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/remote_copy_chip"/>
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/edit_chip"/>
-        </LinearLayout>
-    </HorizontalScrollView>
-    <View
-        android:id="@+id/preview_border"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_marginStart="@dimen/overlay_offset_x"
-        android:layout_marginBottom="12dp"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:elevation="7dp"
-        app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
-        app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
-        android:background="@drawable/overlay_border"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/clipboard_preview_end"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierMargin="@dimen/overlay_border_width"
-        app:barrierDirection="end"
-        app:constraint_referenced_ids="clipboard_preview"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/clipboard_preview_top"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierDirection="top"
-        app:barrierMargin="@dimen/overlay_border_width_neg"
-        app:constraint_referenced_ids="clipboard_preview"/>
-    <FrameLayout
-        android:id="@+id/clipboard_preview"
-        android:elevation="7dp"
-        android:background="@drawable/overlay_preview_background"
-        android:clipChildren="true"
-        android:clipToOutline="true"
-        android:clipToPadding="true"
-        android:layout_width="@dimen/clipboard_preview_size"
-        android:layout_margin="@dimen/overlay_border_width"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        app:layout_constraintBottom_toBottomOf="@id/preview_border"
-        app:layout_constraintStart_toStartOf="@id/preview_border"
-        app:layout_constraintEnd_toEndOf="@id/preview_border"
-        app:layout_constraintTop_toTopOf="@id/preview_border">
-        <TextView android:id="@+id/text_preview"
-                  android:textFontWeight="500"
-                  android:padding="8dp"
-                  android:gravity="center|start"
-                  android:ellipsize="end"
-                  android:autoSizeTextType="uniform"
-                  android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
-                  android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
-                  android:textColor="?attr/overlayButtonTextColor"
-                  android:textColorLink="?attr/overlayButtonTextColor"
-                  android:background="?androidprv:attr/colorAccentSecondary"
-                  android:layout_width="@dimen/clipboard_preview_size"
-                  android:layout_height="@dimen/clipboard_preview_size"/>
-        <ImageView
-            android:id="@+id/image_preview"
-            android:scaleType="fitCenter"
-            android:adjustViewBounds="true"
-            android:contentDescription="@string/clipboard_image_preview"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"/>
-        <TextView
-            android:id="@+id/hidden_preview"
-            android:visibility="gone"
-            android:textFontWeight="500"
-            android:padding="8dp"
-            android:gravity="center"
-            android:textSize="14sp"
-            android:textColor="?attr/overlayButtonTextColor"
-            android:background="?androidprv:attr/colorAccentSecondary"
-            android:layout_width="@dimen/clipboard_preview_size"
-            android:layout_height="@dimen/clipboard_preview_size"/>
-    </FrameLayout>
-    <FrameLayout
-        android:id="@+id/dismiss_button"
-        android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
-        android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
-        android:elevation="10dp"
-        android:visibility="gone"
-        android:alpha="0"
-        app:layout_constraintStart_toEndOf="@id/clipboard_preview"
-        app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
-        app:layout_constraintTop_toTopOf="@id/clipboard_preview"
-        app:layout_constraintBottom_toTopOf="@id/clipboard_preview"
-        android:contentDescription="@string/clipboard_dismiss_description">
-        <ImageView
-            android:id="@+id/dismiss_image"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_margin="@dimen/overlay_dismiss_button_margin"
-            android:src="@drawable/overlay_cancel"/>
-    </FrameLayout>
-</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index d689828..dffe40b 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -141,11 +141,14 @@
         android:layout_width="wrap_content"
         android:layout_height="@dimen/large_screen_shade_header_min_height"
         android:gravity="center"
-        app:layout_constraintEnd_toEndOf="@id/end_guide"
-        app:layout_constraintTop_toTopOf="@id/date"
         app:layout_constraintBottom_toBottomOf="@id/date"
-        >
-        <include layout="@layout/ongoing_privacy_chip"/>
+        app:layout_constraintEnd_toEndOf="@id/end_guide"
+        app:layout_constraintTop_toTopOf="@id/date">
+
+        <com.android.systemui.privacy.OngoingPrivacyChip
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+
     </FrameLayout>
 
 </com.android.systemui.util.NoRemeasureMotionLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/controls_fullscreen.xml b/packages/SystemUI/res/layout/controls_fullscreen.xml
index e08e63b..fa70303 100644
--- a/packages/SystemUI/res/layout/controls_fullscreen.xml
+++ b/packages/SystemUI/res/layout/controls_fullscreen.xml
@@ -15,19 +15,11 @@
      limitations under the License.
 -->
 
-<FrameLayout
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/control_detail_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
 
-
-    <LinearLayout
-        android:id="@+id/global_actions_controls"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:paddingHorizontal="@dimen/controls_padding_horizontal" />
-
-</FrameLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index ee3adba..71561c0 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -13,85 +13,94 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<merge
-    xmlns:android="http://schemas.android.com/apk/res/android">
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:orientation="vertical"
+    tools:parentTag="android.widget.LinearLayout">
 
-  <LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:orientation="horizontal"
-      android:layout_marginTop="@dimen/controls_top_margin"
-      android:layout_marginBottom="@dimen/controls_header_bottom_margin">
-
-    <!-- make sure the header stays centered in the layout by adding a spacer -->
-    <Space
-        android:id="@+id/controls_spacer"
-        android:layout_width="@dimen/controls_header_menu_size"
-        android:layout_height="1dp"
-        android:visibility="gone" />
-
-    <ImageView
-        android:id="@+id/controls_close"
-        android:contentDescription="@string/accessibility_desc_close"
-        android:src="@drawable/ic_close"
-        android:background="?android:attr/selectableItemBackgroundBorderless"
-        android:tint="@color/control_primary_text"
-        android:layout_width="@dimen/controls_header_menu_size"
-        android:layout_height="@dimen/controls_header_menu_size"
-        android:padding="12dp"
-        android:visibility="gone" />
-    <!-- need to keep this outer view in order to have a correctly sized anchor
-         for the dropdown menu, as well as dropdown background in the right place -->
     <LinearLayout
-        android:id="@+id/controls_header"
-        android:orientation="horizontal"
-        android:layout_width="0dp"
-        android:layout_weight="1"
-        android:minHeight="48dp"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:gravity="center">
-      <TextView
-          style="@style/Control.Spinner.Header"
-          android:clickable="false"
-          android:id="@+id/app_or_structure_spinner"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:layout_gravity="center" />
-    </LinearLayout>
-    <ImageView
-        android:id="@+id/controls_more"
-        android:src="@drawable/ic_more_vert"
-        android:layout_width="@dimen/controls_header_menu_size"
-        android:layout_height="@dimen/controls_header_menu_size"
-        android:padding="12dp"
-        android:tint="@color/control_more_vert"
-        android:layout_gravity="center"
-        android:contentDescription="@string/accessibility_menu"
-        android:background="?android:attr/selectableItemBackgroundBorderless" />
-  </LinearLayout>
+        android:paddingHorizontal="@dimen/controls_header_horizontal_padding"
+        android:layout_marginBottom="@dimen/controls_header_bottom_margin"
+        android:orientation="horizontal">
 
-  <ScrollView
+        <!-- make sure the header stays centered in the layout by adding a spacer -->
+        <Space
+            android:id="@+id/controls_spacer"
+            android:layout_width="@dimen/controls_header_menu_button_size"
+            android:layout_height="1dp"
+            android:visibility="gone" />
+
+        <ImageView
+            android:id="@+id/controls_close"
+            android:layout_width="@dimen/controls_header_menu_button_size"
+            android:layout_height="@dimen/controls_header_menu_button_size"
+            android:layout_gravity="center_vertical"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:contentDescription="@string/accessibility_desc_close"
+            android:padding="12dp"
+            android:src="@drawable/ic_close"
+            android:tint="@color/control_primary_text"
+            android:visibility="gone"
+            tools:visibility="visible" />
+
+        <!-- need to keep this outer view in order to have a correctly sized anchor
+             for the dropdown menu, as well as dropdown background in the right place -->
+        <LinearLayout
+            android:id="@+id/controls_header"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:minHeight="48dp"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/app_or_structure_spinner"
+                style="@style/Control.Spinner.Header"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:clickable="false"
+                tools:text="Test app" />
+        </LinearLayout>
+
+        <ImageView
+            android:id="@+id/controls_more"
+            android:layout_width="@dimen/controls_header_menu_button_size"
+            android:layout_height="@dimen/controls_header_menu_button_size"
+            android:layout_gravity="center_vertical"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:contentDescription="@string/accessibility_menu"
+            android:padding="12dp"
+            android:src="@drawable/ic_more_vert"
+            android:tint="@color/control_more_vert" />
+    </LinearLayout>
+
+    <ScrollView
         android:id="@+id/controls_scroll_view"
         android:layout_width="match_parent"
         android:layout_height="0dp"
+        android:layout_marginHorizontal="@dimen/controls_content_margin_horizontal"
         android:layout_weight="1"
-        android:orientation="vertical"
         android:clipChildren="true"
+        android:orientation="vertical"
+        android:paddingHorizontal="16dp"
         android:scrollbars="none">
-    <include layout="@layout/global_actions_controls_list_view" />
 
-  </ScrollView>
+        <include layout="@layout/global_actions_controls_list_view" />
 
-  <FrameLayout
-      android:id="@+id/controls_panel"
-      android:layout_width="match_parent"
-      android:layout_height="0dp"
-      android:layout_weight="1"
-      android:layout_marginLeft="@dimen/global_actions_side_margin"
-      android:layout_marginRight="@dimen/global_actions_side_margin"
-      android:background="@drawable/controls_panel_background"
-      android:padding="@dimen/global_actions_side_margin"
-      android:visibility="gone"
-      />
+    </ScrollView>
+
+    <FrameLayout
+        android:id="@+id/controls_panel"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginHorizontal="@dimen/controls_content_margin_horizontal"
+        android:layout_weight="1"
+        android:background="@drawable/controls_panel_background"
+        android:visibility="gone"
+        tools:visibility="visible" />
 </merge>
diff --git a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml
index de96e97..fb78b49 100644
--- a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml
@@ -20,7 +20,7 @@
     android:layout_width="wrap_content"
     android:paddingVertical="@dimen/dream_overlay_complication_home_controls_padding">
 
-    <ImageView
+    <com.android.systemui.animation.view.LaunchableImageView
         android:id="@+id/home_controls_chip"
         android:layout_height="@dimen/keyguard_affordance_fixed_height"
         android:layout_width="@dimen/keyguard_affordance_fixed_width"
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 9add32c..885e5e2 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -57,6 +57,7 @@
             android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
             android:layout_height="match_parent"
             android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+            android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop"
             android:src="@drawable/ic_alarm"
             android:tint="@android:color/white"
             android:visibility="gone"
@@ -67,6 +68,7 @@
             android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
             android:layout_height="match_parent"
             android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+            android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop"
             android:src="@drawable/ic_qs_dnd_on"
             android:tint="@android:color/white"
             android:visibility="gone"
@@ -77,6 +79,7 @@
             android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
             android:layout_height="match_parent"
             android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+            android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop"
             android:src="@drawable/ic_signal_wifi_off"
             android:visibility="gone"
             android:contentDescription="@string/dream_overlay_status_bar_wifi_off" />
diff --git a/packages/SystemUI/res/layout/emergency_cryptkeeper_text.xml b/packages/SystemUI/res/layout/emergency_cryptkeeper_text.xml
deleted file mode 100644
index 0a1730a..0000000
--- a/packages/SystemUI/res/layout/emergency_cryptkeeper_text.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2016 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-
-<com.android.systemui.statusbar.policy.EmergencyCryptkeeperText
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/emergency_cryptkeeper_text"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:textAppearance="@style/TextAppearance.StatusBar.Clock"
-        android:paddingStart="6dp"
-        android:singleLine="true"
-        android:ellipsize="marquee"
-        android:gravity="center_vertical|start"
-        />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/font_scaling_dialog.xml b/packages/SystemUI/res/layout/font_scaling_dialog.xml
new file mode 100644
index 0000000..27c1e9d
--- /dev/null
+++ b/packages/SystemUI/res/layout/font_scaling_dialog.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+<com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/font_scaling_slider"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    app:max="6"
+    app:progress="0"
+    app:iconStartContentDescription="@string/font_scaling_smaller"
+    app:iconEndContentDescription="@string/font_scaling_larger"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/global_actions_grid_lite.xml b/packages/SystemUI/res/layout/global_actions_grid_lite.xml
index 5588fd3..a64c9ae 100644
--- a/packages/SystemUI/res/layout/global_actions_grid_lite.xml
+++ b/packages/SystemUI/res/layout/global_actions_grid_lite.xml
@@ -33,7 +33,7 @@
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       android:layout_weight="1">
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <com.android.systemui.common.ui.view.LaunchableConstraintLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:id="@android:id/list"
@@ -55,6 +55,6 @@
           app:flow_horizontalGap="@dimen/global_actions_lite_padding"
           app:flow_verticalGap="@dimen/global_actions_lite_padding"
           app:flow_horizontalStyle="packed"/>
-    </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.android.systemui.common.ui.view.LaunchableConstraintLayout>
   </com.android.systemui.globalactions.GlobalActionsLayoutLite>
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 6120863..2871cdf 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -59,7 +59,7 @@
 
     </LinearLayout>
 
-    <com.android.systemui.common.ui.view.LaunchableImageView
+    <com.android.systemui.animation.view.LaunchableImageView
         android:id="@+id/start_button"
         android:layout_height="@dimen/keyguard_affordance_fixed_height"
         android:layout_width="@dimen/keyguard_affordance_fixed_width"
@@ -67,11 +67,12 @@
         android:scaleType="center"
         android:tint="?android:attr/textColorPrimary"
         android:background="@drawable/keyguard_bottom_affordance_bg"
+        android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
         android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset"
         android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
         android:visibility="gone" />
 
-    <com.android.systemui.common.ui.view.LaunchableImageView
+    <com.android.systemui.animation.view.LaunchableImageView
         android:id="@+id/end_button"
         android:layout_height="@dimen/keyguard_affordance_fixed_height"
         android:layout_width="@dimen/keyguard_affordance_fixed_width"
@@ -79,6 +80,7 @@
         android:scaleType="center"
         android:tint="?android:attr/textColorPrimary"
         android:background="@drawable/keyguard_bottom_affordance_bg"
+        android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
         android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset"
         android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
         android:visibility="gone" />
diff --git a/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml b/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
index 6f33623..07c428b 100644
--- a/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
+++ b/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
@@ -24,7 +24,7 @@
     android:layout_gravity="end">
     <!-- We add a background behind the UserAvatarView with the same color and with a circular shape
          so that this view can be expanded into a Dialog or an Activity. -->
-    <FrameLayout
+    <com.android.systemui.animation.LaunchableFrameLayout
         android:id="@+id/kg_multi_user_avatar_with_background"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -42,5 +42,5 @@
             systemui:framePadding="0dp"
             systemui:frameWidth="0dp">
         </com.android.systemui.statusbar.phone.UserAvatarView>
-    </FrameLayout>
+    </com.android.systemui.animation.LaunchableFrameLayout>
 </FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml
new file mode 100644
index 0000000..89d88fe
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml
@@ -0,0 +1,50 @@
+<?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.
+  ~
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:minHeight="52dp"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:background="@drawable/keyguard_settings_popup_menu_background"
+    android:paddingStart="16dp"
+    android:paddingEnd="24dp"
+    android:paddingVertical="16dp">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="20dp"
+        android:layout_height="20dp"
+        android:layout_marginEnd="16dp"
+        android:tint="?android:attr/textColorPrimary"
+        android:importantForAccessibility="no"
+        tools:ignore="UseAppTint" />
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:textColor="?android:attr/textColorPrimary"
+        android:textSize="14sp"
+        android:maxLines="1"
+        android:ellipsize="end" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml b/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
index aaa372a..e39f1a9 100644
--- a/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
+++ b/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
@@ -18,6 +18,7 @@
 
 <!-- LinearLayout -->
 <com.android.systemui.statusbar.policy.KeyguardUserDetailItemView
+        android:id="@+id/user_item"
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:systemui="http://schemas.android.com/apk/res-auto"
         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/large_screen_shade_header.xml b/packages/SystemUI/res/layout/large_screen_shade_header.xml
deleted file mode 100644
index 3029a27..0000000
--- a/packages/SystemUI/res/layout/large_screen_shade_header.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/split_shade_status_bar"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/large_screen_shade_header_height"
-    android:minHeight="@dimen/large_screen_shade_header_min_height"
-    android:clickable="false"
-    android:focusable="true"
-    android:paddingLeft="@dimen/large_screen_shade_header_left_padding"
-    android:paddingRight="@dimen/qs_panel_padding"
-    android:visibility="gone"
-    android:theme="@style/Theme.SystemUI.QuickSettings.Header">
-
-    <com.android.systemui.statusbar.policy.Clock
-        android:id="@+id/clock"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:minWidth="48dp"
-        android:minHeight="@dimen/large_screen_shade_header_min_height"
-        android:gravity="start|center_vertical"
-        android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
-        android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
-        android:singleLine="true"
-        android:textAppearance="@style/TextAppearance.QS.Status" />
-
-    <com.android.systemui.statusbar.policy.DateView
-        android:id="@+id/date"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="start|center_vertical"
-        android:gravity="center_vertical"
-        android:singleLine="true"
-        android:textAppearance="@style/TextAppearance.QS.Status"
-        systemui:datePattern="@string/abbrev_wday_month_day_no_year_alarm" />
-
-    <FrameLayout
-        android:id="@+id/rightLayout"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:gravity="end">
-
-        <LinearLayout
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_gravity="end|center_vertical">
-
-            <include
-                android:id="@+id/carrier_group"
-                layout="@layout/qs_carrier_group"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:layout_gravity="end|center_vertical"
-                android:layout_marginStart="8dp"
-                android:focusable="false"
-                android:minHeight="@dimen/large_screen_shade_header_min_height"
-                android:minWidth="48dp" />
-
-            <com.android.systemui.statusbar.phone.StatusIconContainer
-                android:id="@+id/statusIcons"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:paddingEnd="@dimen/signal_cluster_battery_padding" />
-
-            <com.android.systemui.battery.BatteryMeterView
-                android:id="@+id/batteryRemainingIcon"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                systemui:textAppearance="@style/TextAppearance.QS.Status" />
-            <FrameLayout
-                android:id="@+id/privacy_container"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:minHeight="48dp"
-                android:layout_weight="1"
-                android:paddingStart="16dp">
-
-                <include layout="@layout/ongoing_privacy_chip" />
-
-            </FrameLayout>
-        </LinearLayout>
-    </FrameLayout>
-
-</LinearLayout>
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index b76de5a..e182a6a 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -24,6 +24,7 @@
     android:orientation="vertical">
 
     <LinearLayout
+        android:id="@+id/media_metadata_section"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:gravity="start|center_vertical"
diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml
new file mode 100644
index 0000000..c54c4e4
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_recommendation_view.xml
@@ -0,0 +1,87 @@
+<?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
+  -->
+<!-- Layout for media recommendation item inside QSPanel carousel -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Album cover -->
+    <ImageView
+        android:id="@+id/media_cover"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:translationZ="0dp"
+        android:scaleType="centerCrop"
+        android:adjustViewBounds="true"
+        android:clipToOutline="true"
+        android:background="@drawable/bg_smartspace_media_item"/>
+
+    <!-- App icon -->
+    <com.android.internal.widget.CachingIconView
+        android:id="@+id/media_rec_app_icon"
+        android:layout_width="@dimen/qs_media_rec_icon_top_margin"
+        android:layout_height="@dimen/qs_media_rec_icon_top_margin"
+        android:layout_marginStart="@dimen/qs_media_info_spacing"
+        android:layout_marginTop="@dimen/qs_media_info_spacing"/>
+
+    <!-- Artist name -->
+    <TextView
+        android:id="@+id/media_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/qs_media_info_spacing"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_rec_album_title_bottom_margin"
+        android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+        android:singleLine="true"
+        android:textSize="12sp"
+        android:gravity="top"
+        android:layout_gravity="bottom"
+        android:importantForAccessibility="no"/>
+
+    <!-- Album name -->
+    <TextView
+        android:id="@+id/media_subtitle"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_rec_album_subtitle_height"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginStart="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_info_spacing"
+        android:fontFamily="@*android:string/config_headlineFontFamily"
+        android:singleLine="true"
+        android:textSize="11sp"
+        android:gravity="center_vertical"
+        android:layout_gravity="bottom"
+        android:importantForAccessibility="no"/>
+
+    <!-- Seek Bar -->
+    <SeekBar
+        android:id="@+id/media_progress_bar"
+        android:layout_width="match_parent"
+        android:layout_height="12dp"
+        android:layout_gravity="bottom"
+        android:maxHeight="@dimen/qs_media_enabled_seekbar_height"
+        android:thumb="@android:color/transparent"
+        android:splitTrack="false"
+        android:clickable="false"
+        android:progressTint="?android:attr/textColorPrimary"
+        android:progressBackgroundTint="?android:attr/textColorTertiary"
+        android:paddingTop="5dp"
+        android:paddingBottom="5dp"
+        android:paddingStart="0dp"
+        android:paddingEnd="0dp"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginStart="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_info_spacing"/>
+</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_recommendations.xml b/packages/SystemUI/res/layout/media_recommendations.xml
new file mode 100644
index 0000000..65fc19c
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_recommendations.xml
@@ -0,0 +1,75 @@
+<?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
+  -->
+
+<!-- Layout for media recommendations inside QSPanel carousel -->
+<com.android.systemui.util.animation.TransitionLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/media_recommendations_updated"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:forceHasOverlappingRendering="false"
+    android:background="@drawable/qs_media_background"
+    android:theme="@style/MediaPlayer">
+
+    <!-- This view just ensures the full media player is a certain height. -->
+    <View
+        android:id="@+id/sizing_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_expanded" />
+
+    <TextView
+        android:id="@+id/media_rec_title"
+        style="@style/MediaPlayer.Recommendation.Header"
+        android:text="@string/controls_media_smartspace_rec_header"/>
+
+    <FrameLayout
+        android:id="@+id/media_cover1_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        >
+
+        <include
+            layout="@layout/media_recommendation_view"/>
+
+    </FrameLayout>
+
+
+    <FrameLayout
+        android:id="@+id/media_cover2_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        >
+
+        <include
+            layout="@layout/media_recommendation_view"/>
+
+    </FrameLayout>
+
+    <FrameLayout
+        android:id="@+id/media_cover3_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        >
+
+        <include
+            layout="@layout/media_recommendation_view"/>
+
+    </FrameLayout>
+
+    <include
+        layout="@layout/media_long_press_menu" />
+
+</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 95aefab..9d91419 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -106,7 +106,7 @@
         app:layout_constrainedWidth="true"
         app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
         app:layout_constraintHeight_min="@dimen/min_clickable_item_size">
-        <LinearLayout
+        <com.android.systemui.animation.view.LaunchableLinearLayout
             android:id="@+id/media_seamless_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
@@ -135,7 +135,7 @@
                 android:textDirection="locale"
                 android:textSize="12sp"
                 android:lineHeight="16sp" />
-        </LinearLayout>
+        </com.android.systemui.animation.view.LaunchableLinearLayout>
     </LinearLayout>
 
     <!-- Song name -->
@@ -147,6 +147,14 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content" />
 
+    <!-- Explicit Indicator -->
+    <com.android.internal.widget.CachingIconView
+        android:id="@+id/media_explicit_indicator"
+        android:layout_width="@dimen/qs_media_explicit_indicator_icon_size"
+        android:layout_height="@dimen/qs_media_explicit_indicator_icon_size"
+        android:src="@drawable/ic_media_explicit_indicator"
+        />
+
     <!-- Artist name -->
     <TextView
         android:id="@+id/header_artist"
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index 21d12c2..02186fc 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -27,13 +27,28 @@
         android:layout_height="wrap_content"
         />
 
-    <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"
+    <FrameLayout
+        android:id="@+id/icon_container_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:alpha="0.0"
-        />
+        >
+        <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/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml
index c949ba0..238fc84 100644
--- a/packages/SystemUI/res/layout/ongoing_call_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml
@@ -23,7 +23,7 @@
     android:layout_gravity="center_vertical|start"
     android:layout_marginStart="5dp"
 >
-    <LinearLayout
+    <com.android.systemui.animation.view.LaunchableLinearLayout
         android:id="@+id/ongoing_call_chip_background"
         android:layout_width="wrap_content"
         android:layout_height="@dimen/ongoing_appops_chip_height"
@@ -55,5 +55,5 @@
             android:textColor="?android:attr/colorPrimary"
         />
 
-    </LinearLayout>
+    </com.android.systemui.animation.view.LaunchableLinearLayout>
 </FrameLayout>
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
index 5aa6080..2c7467d 100644
--- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -16,15 +16,15 @@
 -->
 
 
-<com.android.systemui.privacy.OngoingPrivacyChip
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/privacy_chip"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_height="match_parent"
     android:layout_width="wrap_content"
     android:layout_gravity="center_vertical|end"
-    android:focusable="true"
     android:clipChildren="false"
     android:clipToPadding="false"
+    tools:parentTag="com.android.systemui.privacy.OngoingPrivacyChip">
     >
 
         <LinearLayout
@@ -34,8 +34,9 @@
             android:paddingStart="10dp"
             android:paddingEnd="10dp"
             android:gravity="center"
+            android:clipToOutline="true"
+            android:clipToPadding="false"
             android:layout_gravity="center"
             android:minWidth="@dimen/ongoing_appops_chip_min_width"
-            android:maxWidth="@dimen/ongoing_appops_chip_max_width"
-            />
-</com.android.systemui.privacy.OngoingPrivacyChip>
\ No newline at end of file
+            android:maxWidth="@dimen/ongoing_appops_chip_max_width" />
+</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index b1d3ed05..745cfc6 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -64,7 +64,7 @@
                     android:layout_width="@dimen/qs_footer_action_button_size"
                     android:layout_height="@dimen/qs_footer_action_button_size"
                     android:layout_gravity="center_vertical|end"
-                    android:background="?android:attr/selectableItemBackground"
+                    android:background="@drawable/qs_footer_edit_circle"
                     android:clickable="true"
                     android:contentDescription="@string/accessibility_quick_settings_edit"
                     android:focusable="true"
diff --git a/packages/SystemUI/res/layout/qs_user_detail_item.xml b/packages/SystemUI/res/layout/qs_user_detail_item.xml
index 7c86bc7..ad129e8 100644
--- a/packages/SystemUI/res/layout/qs_user_detail_item.xml
+++ b/packages/SystemUI/res/layout/qs_user_detail_item.xml
@@ -18,6 +18,7 @@
 
 <!-- LinearLayout -->
 <com.android.systemui.qs.tiles.UserDetailItemView
+        android:id="@+id/user_item"
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:systemui="http://schemas.android.com/apk/res-auto"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/quick_qs_status_icons.xml b/packages/SystemUI/res/layout/quick_qs_status_icons.xml
deleted file mode 100644
index 542a1c9..0000000
--- a/packages/SystemUI/res/layout/quick_qs_status_icons.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/quick_qs_status_icons"
-    android:layout_width="match_parent"
-    android:layout_height="@*android:dimen/quick_qs_offset_height"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:minHeight="@dimen/qs_header_row_min_height"
-    android:clickable="false"
-    android:focusable="true"
-    android:theme="@style/Theme.SystemUI.QuickSettings.Header">
-
-    <LinearLayout
-        android:id="@+id/clock_container"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:layout_gravity="center_vertical|start"
-        android:gravity="center_vertical|start"
-        >
-
-        <com.android.systemui.statusbar.policy.Clock
-            android:id="@+id/clock"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:minHeight="@dimen/qs_header_row_min_height"
-            android:gravity="center_vertical|start"
-            android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
-            android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
-            android:singleLine="true"
-            android:textAppearance="@style/TextAppearance.QS.Status" />
-
-        <com.android.systemui.statusbar.policy.VariableDateView
-            android:id="@+id/date_clock"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_marginStart="@dimen/status_bar_left_clock_end_padding"
-            android:gravity="center_vertical|start"
-            android:singleLine="true"
-            android:textAppearance="@style/TextAppearance.QS.Status"
-            systemui:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm"
-            systemui:shortDatePattern="@string/abbrev_month_day_no_year"
-        />
-    </LinearLayout>
-
-    <include layout="@layout/qs_carrier_group"
-        android:id="@+id/carrier_group"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:minHeight="@dimen/qs_header_row_min_height"
-        android:minWidth="48dp"
-        android:layout_marginStart="8dp"
-        android:layout_gravity="end|center_vertical"
-        android:focusable="false"/>
-
-    <View
-        android:id="@+id/separator"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_gravity="center"
-        android:layout_marginStart="8dp"
-        android:layout_marginEnd="8dp"
-        android:visibility="gone"
-        />
-
-    <FrameLayout
-        android:id="@+id/rightLayout"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="end"
-        >
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_gravity="center_vertical|end"
-        >
-        <com.android.systemui.statusbar.phone.StatusIconContainer
-            android:id="@+id/statusIcons"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:paddingEnd="@dimen/signal_cluster_battery_padding" />
-
-        <com.android.systemui.battery.BatteryMeterView
-            android:id="@+id/batteryRemainingIcon"
-            android:layout_height="match_parent"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            systemui:textAppearance="@style/TextAppearance.QS.Status"
-            android:paddingEnd="2dp" />
-
-    </LinearLayout>
-    </FrameLayout>
-
-</LinearLayout>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 9fc3f40..1749ed4 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -33,32 +33,16 @@
     android:paddingStart="0dp"
     android:elevation="4dp" >
 
-    <!-- Date and privacy. Only visible in QS when not in split shade -->
-    <include layout="@layout/quick_status_bar_header_date_privacy"/>
-
-    <RelativeLayout
-        android:id="@+id/qs_container"
+    <com.android.systemui.qs.QuickQSPanel
+        android:id="@+id/quick_qs_panel"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_gravity="top"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
         android:clipChildren="false"
-        android:clipToPadding="false">
-        <!-- Time, icons and Carrier (only in QS when not in split shade) -->
-        <include layout="@layout/quick_qs_status_icons"/>
-
-        <com.android.systemui.qs.QuickQSPanel
-            android:id="@+id/quick_qs_panel"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_below="@id/quick_qs_status_icons"
-            android:layout_marginTop="@dimen/qqs_layout_margin_top"
-            android:accessibilityTraversalAfter="@id/quick_qs_status_icons"
-            android:clipChildren="false"
-            android:clipToPadding="false"
-            android:focusable="true"
-            android:paddingBottom="@dimen/qqs_layout_padding_bottom"
-            android:importantForAccessibility="no">
-        </com.android.systemui.qs.QuickQSPanel>
-    </RelativeLayout>
+        android:clipToPadding="false"
+        android:focusable="true"
+        android:paddingBottom="@dimen/qqs_layout_padding_bottom"
+        android:importantForAccessibility="no">
+    </com.android.systemui.qs.QuickQSPanel>
 
 </com.android.systemui.qs.QuickStatusBarHeader>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
deleted file mode 100644
index 8b5d953..0000000
--- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-** Copyright 2017, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/quick_status_bar_date_privacy"
-    android:layout_width="match_parent"
-    android:layout_height="@*android:dimen/quick_qs_offset_height"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:gravity="center"
-    android:layout_gravity="top"
-    android:orientation="horizontal"
-    android:importantForAccessibility="no"
-    android:clickable="true"
-    android:minHeight="48dp">
-
-    <FrameLayout
-        android:id="@+id/date_container"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:minHeight="48dp"
-        android:layout_weight="1"
-        android:gravity="center_vertical|start" >
-
-        <com.android.systemui.statusbar.policy.VariableDateView
-            android:id="@+id/date"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:gravity="center_vertical"
-            android:singleLine="true"
-            android:textAppearance="@style/TextAppearance.QS.Status"
-            systemui:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm"
-            systemui:shortDatePattern="@string/abbrev_month_day_no_year"
-        />
-    </FrameLayout>
-
-    <!-- We want this to be centered (to align with notches). In order to do that, the following
-         has to hold (in portrait):
-         * date_container and privacy_container must have the same width and weight
-         -->
-    <android.widget.Space
-        android:id="@+id/space"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_gravity="center_vertical|center_horizontal"
-        android:visibility="gone" />
-
-    <FrameLayout
-        android:id="@+id/privacy_container"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:minHeight="48dp"
-        android:layout_weight="1"
-        android:gravity="center_vertical|end" >
-
-        <include layout="@layout/ongoing_privacy_chip" />
-
-    </FrameLayout>
-</LinearLayout>
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/screenshot_detection_notice.xml b/packages/SystemUI/res/layout/screenshot_detection_notice.xml
new file mode 100644
index 0000000..fc936c0
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_detection_notice.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/screenshot_detection_notice"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:padding="12dp"
+    android:visibility="gone">
+
+    <TextView
+        android:id="@+id/screenshot_detection_notice_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:lineHeight="24sp"
+        android:textSize="18sp" />
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index e4e0bd4..7e9202c 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -27,26 +27,26 @@
         android:elevation="4dp"
         android:background="@drawable/action_chip_container_background"
         android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
-        app:layout_constraintBottom_toBottomOf="@+id/actions_container"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="@+id/actions_container"
-        app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
+        app:layout_constraintEnd_toEndOf="@+id/actions_container"
+        app:layout_constraintBottom_toTopOf="@id/guideline"/>
     <HorizontalScrollView
         android:id="@+id/actions_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
-        android:layout_marginBottom="4dp"
-        android:paddingEnd="@dimen/overlay_action_container_padding_right"
+        android:paddingEnd="@dimen/overlay_action_container_padding_end"
         android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
         android:elevation="4dp"
         android:scrollbars="none"
         app:layout_constraintHorizontal_bias="0"
         app:layout_constraintWidth_percent="1.0"
         app:layout_constraintWidth_max="wrap"
-        app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"
         app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
-        app:layout_constraintEnd_toEndOf="parent">
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
         <LinearLayout
             android:id="@+id/screenshot_actions"
             android:layout_width="wrap_content"
@@ -64,35 +64,24 @@
         android:id="@+id/screenshot_preview_border"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:layout_marginStart="@dimen/overlay_offset_x"
-        android:layout_marginBottom="12dp"
+        android:layout_marginStart="@dimen/overlay_preview_container_margin"
+        android:layout_marginTop="@dimen/overlay_border_width_neg"
+        android:layout_marginEnd="@dimen/overlay_border_width_neg"
+        android:layout_marginBottom="@dimen/overlay_preview_container_margin"
         android:elevation="7dp"
         android:alpha="0"
         android:background="@drawable/overlay_border"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"
-        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_end"
-        app:layout_constraintTop_toTopOf="@id/screenshot_preview_top"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/screenshot_preview_end"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierMargin="@dimen/overlay_border_width"
-        app:barrierDirection="end"
-        app:constraint_referenced_ids="screenshot_preview"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/screenshot_preview_top"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierDirection="top"
-        app:barrierMargin="@dimen/overlay_border_width_neg"
-        app:constraint_referenced_ids="screenshot_preview"/>
+        app:layout_constraintStart_toStartOf="@id/actions_container_background"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
     <ImageView
         android:id="@+id/screenshot_preview"
         android:visibility="invisible"
         android:layout_width="@dimen/overlay_x_scale"
-        android:layout_margin="@dimen/overlay_border_width"
         android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/overlay_border_width"
+        android:layout_marginBottom="@dimen/overlay_border_width"
         android:layout_gravity="center"
         android:elevation="7dp"
         android:contentDescription="@string/screenshot_edit_description"
@@ -100,20 +89,14 @@
         android:background="@drawable/overlay_preview_background"
         android:adjustViewBounds="true"
         android:clickable="true"
-        app:layout_constraintHorizontal_bias="0"
-        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
         app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
-        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
-        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/>
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/>
     <ImageView
         android:id="@+id/screenshot_badge"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:padding="4dp"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
         android:visibility="gone"
-        android:background="@drawable/overlay_badge_background"
         android:elevation="8dp"
-        android:src="@drawable/overlay_cancel"
         app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
     <FrameLayout
@@ -144,57 +127,32 @@
         app:layout_constraintTop_toTopOf="@id/screenshot_preview"
         android:elevation="7dp"/>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_end="0dp" />
+
+    <FrameLayout
         android:id="@+id/screenshot_message_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
-        android:layout_marginVertical="4dp"
-        android:paddingHorizontal="@dimen/overlay_action_container_padding_right"
+        android:layout_marginTop="4dp"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
         android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
         android:elevation="4dp"
         android:background="@drawable/action_chip_container_background"
         android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/guideline"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent">
-
-        <ImageView
-            android:id="@+id/screenshot_message_icon"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:paddingEnd="4dp"
-            android:src="@drawable/ic_work_app_badge"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"/>
-
-        <TextView
-            android:id="@+id/screenshot_message_content"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
-            app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
-
-        <FrameLayout
-            android:id="@+id/message_dismiss_button"
-            android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
-            android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
-            app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            android:contentDescription="@string/screenshot_dismiss_work_profile">
-            <ImageView
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:layout_margin="@dimen/overlay_dismiss_button_margin"
-                android:src="@drawable/overlay_cancel"/>
-        </FrameLayout>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
+        app:layout_constraintWidth_max="450dp"
+        app:layout_constraintHorizontal_bias="0"
+        >
+        <include layout="@layout/screenshot_work_profile_first_run" />
+        <include layout="@layout/screenshot_detection_notice" />
+    </FrameLayout>
 </com.android.systemui.screenshot.DraggableConstraintLayout>
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
new file mode 100644
index 0000000..392d845
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/work_profile_first_run"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:paddingStart="16dp"
+    android:paddingEnd="4dp"
+    android:paddingVertical="16dp"
+    android:visibility="gone">
+    <ImageView
+        android:id="@+id/screenshot_message_icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_marginEnd="12dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/ic_work_app_badge"/>
+
+    <TextView
+        android:id="@+id/screenshot_message_content"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_gravity="start|center_vertical"
+        android:textSize="18sp"
+        android:textColor="?android:attr/textColorPrimary"
+        android:lineHeight="24sp"
+        />
+
+    <FrameLayout
+        android:id="@+id/message_dismiss_button"
+        android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+        android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+        android:contentDescription="@string/screenshot_dismiss_work_profile">
+        <ImageView
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="center"
+            android:src="@drawable/overlay_cancel"/>
+    </FrameLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml b/packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml
new file mode 100644
index 0000000..52d1d4f
--- /dev/null
+++ b/packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml
@@ -0,0 +1,73 @@
+<?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.
+  -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:tools="http://schemas.android.com/tools"
+       android:id="@+id/seekbar_frame"
+       android:layout_width="match_parent"
+       android:layout_height="wrap_content"
+       android:clipChildren="false"
+       android:gravity="center_vertical"
+       android:orientation="horizontal"
+       tools:parentTag="android.widget.LinearLayout">
+
+    <FrameLayout
+        android:id="@+id/icon_start_frame"
+        android:layout_width="@dimen/min_clickable_item_size"
+        android:layout_height="@dimen/min_clickable_item_size"
+        android:clipChildren="false"
+        android:focusable="true" >
+        <ImageView
+            android:id="@+id/icon_start"
+            android:layout_width="@dimen/seekbar_icon_size"
+            android:layout_height="@dimen/seekbar_icon_size"
+            android:layout_gravity="center"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:adjustViewBounds="true"
+            android:focusable="false"
+            android:src="@drawable/ic_remove"
+            android:tint="?android:attr/textColorPrimary"
+            android:tintMode="src_in" />
+    </FrameLayout>
+
+    <SeekBar
+        android:id="@+id/seekbar"
+        style="@android:style/Widget.Material.SeekBar.Discrete"
+        android:layout_width="0dp"
+        android:layout_height="48dp"
+        android:layout_gravity="center_vertical"
+        android:layout_weight="1" />
+
+    <FrameLayout
+        android:id="@+id/icon_end_frame"
+        android:layout_width="@dimen/min_clickable_item_size"
+        android:layout_height="@dimen/min_clickable_item_size"
+        android:clipChildren="false"
+        android:focusable="true" >
+        <ImageView
+            android:id="@+id/icon_end"
+            android:layout_width="@dimen/seekbar_icon_size"
+            android:layout_height="@dimen/seekbar_icon_size"
+            android:layout_gravity="center"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:adjustViewBounds="true"
+            android:focusable="false"
+            android:src="@drawable/ic_add"
+            android:tint="?android:attr/textColorPrimary"
+            android:tintMode="src_in" />
+    </FrameLayout>
+
+</merge>
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index 3b71dc3..64aa629 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -149,11 +149,4 @@
         </FrameLayout>
     </LinearLayout>
 
-    <ViewStub
-        android:id="@+id/emergency_cryptkeeper_text"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout="@layout/emergency_cryptkeeper_text"
-    />
-
 </com.android.systemui.statusbar.phone.PhoneStatusBarView>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 159323a..3c860a9 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -26,6 +26,11 @@
     android:layout_height="match_parent"
     android:background="@android:color/transparent">
 
+    <com.android.systemui.common.ui.view.LongPressHandlingView
+        android:id="@+id/keyguard_long_press"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
     <ViewStub
         android:id="@+id/keyguard_qs_user_switch_stub"
         android:layout="@layout/keyguard_qs_user_switch"
diff --git a/packages/SystemUI/res/layout/status_bar_notification_footer.xml b/packages/SystemUI/res/layout/status_bar_notification_footer.xml
index bbb8df1c..db94c92 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_footer.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_footer.xml
@@ -26,6 +26,17 @@
         android:id="@+id/content"
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
+        <TextView
+            android:id="@+id/unlock_prompt_footer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:layout_gravity="center_horizontal"
+            android:gravity="center"
+            android:drawablePadding="8dp"
+            android:visibility="gone"
+            android:textAppearance="?android:attr/textAppearanceButton"
+            android:text="@string/unlock_to_see_notif_text"/>
         <com.android.systemui.statusbar.notification.row.FooterViewButton
             style="@style/TextAppearance.NotificationSectionHeaderButton"
             android:id="@+id/manage_text"
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/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
index fa9d739..7eaed43 100644
--- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
+++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
@@ -46,7 +46,7 @@
           app:layout_constraintEnd_toEndOf="parent"
           app:flow_horizontalBias="0.5"
           app:flow_verticalAlign="center"
-          app:flow_wrapMode="chain"
+          app:flow_wrapMode="chain2"
           app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap"
           app:flow_verticalGap="44dp"
           app:flow_horizontalStyle="packed"/>
diff --git a/packages/SystemUI/res/raw/biometricprompt_rear_landscape_base.json b/packages/SystemUI/res/raw/biometricprompt_rear_landscape_base.json
new file mode 100644
index 0000000..49c1c40
--- /dev/null
+++ b/packages/SystemUI/res/raw/biometricprompt_rear_landscape_base.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Rear_Landscape_Base_Foldable","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 18","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[169.478,169.749,0],"ix":2,"l":2},"a":{"a":0,"k":[-48.123,-30.19,0],"ix":1,"l":2},"s":{"a":0,"k":[132,132,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".grey400","cl":"grey400","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.741176486015,0.75686275959,0.776470601559,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"black circle matte","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey904","cl":"grey904","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,35.536,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.552,0.087],[0,0]],"o":[[0,0],[0,-3.287],[0,0],[0,0]],"v":[[-2.301,8.869],[-2.301,-3.772],[2.301,-9.806],[2.301,9.806]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"black circle matte 2","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue401","cl":"blue401","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,-27.655,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.286],[0,0],[-2.552,0.086],[0,0]],"o":[[0,0],[0,-3.286],[0,0],[-2.552,-0.086]],"v":[[-2.301,16.282],[-2.301,-16.281],[2.301,-22.313],[2.301,22.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"black circle matte 3","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Finger 2","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-129,"s":[-67]},{"t":-29,"s":[0]}],"ix":10},"p":{"a":0,"k":[-75.352,41.307,0],"ix":2,"l":2},"a":{"a":0,"k":[94.648,211.307,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.72,-5.642],[0,0],[-9.394,-0.562],[-0.298,-0.038]],"o":[[-5.153,4.329],[3.882,-16.05],[0.31,0.019],[-0.044,0.75]],"v":[[0.863,12.222],[-8.931,14.755],[8.005,-15.108],[8.931,-15.021]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156875134,0.454901963472,0.376470595598,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[81.486,130.081],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 9","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.459,6.045],[-5.153,4.329],[-0.044,0.75],[3.116,-24.664],[5.23,-22.052],[8.666,11.92],[-2.9,9.135]],"o":[[0,0],[6.72,-5.642],[12.723,1.335],[-2.369,18.762],[-13.993,-5.333],[2.255,-5.502],[1.843,-5.815]],"v":[[-9.99,-18.348],[-0.196,-20.881],[7.872,-48.124],[21.578,-9.331],[12.104,48.124],[-22.574,21.555],[-14.791,-0.206]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725507259,0.384313732386,0.282352954149,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[82.545,163.184],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 8","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"black circle matte 4","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".grey903","cl":"grey903","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.345,-92.442,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.07,0],[0,0],[-8.27,0],[0,0]],"o":[[0,0],[0,8.269],[0,0],[-14.024,-17.379]],"v":[[-29.778,-14.252],[-29.778,-0.721],[-14.805,14.252],[29.778,14.252]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"black circle matte 5","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".grey902","cl":"grey902","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-15.947,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[154.053,139.81,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.3,0.367],[0,0],[-2.364,0.157],[0,0]],"o":[[0,0],[2.3,-0.367],[0,0],[-2.364,-0.157]],"v":[[-3.5,75.533],[-3.5,-75.533],[3.5,-76.312],[3.5,76.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[113.225,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 7","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,8.269],[0,0],[2.181,-0.187],[0,0],[-2.23,0],[0,42.252],[10.593,13.127],[0,0]],"o":[[0,0],[-2.23,0],[0,0],[2.181,0.187],[42.252,0],[0,-18.182],[0,0],[-8.27,0]],"v":[[-34.946,-62.973],[-34.946,-76.504],[-41.558,-76.201],[-41.558,76.201],[-34.946,76.504],[41.558,0],[24.61,-48],[-19.973,-48]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[156.824,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".black 2","cl":"black","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48.123,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-129,"s":[0,0,100]},{"t":-79,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".grey700","cl":"grey700","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-56.481,-59.936,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.767,0],[0,0],[0,-3.767],[0,0],[-3.767,0],[0,0],[0,3.767],[0,0]],"o":[[0,0],[-3.767,0],[0,0],[0,3.767],[0,0],[3.767,0],[0,0],[0,-3.767]],"v":[[46.055,-14.479],[-46.056,-14.479],[-52.876,-7.659],[-52.876,7.658],[-46.056,14.479],[46.055,14.479],[52.876,7.658],[52.876,-7.659]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".grey901","cl":"grey901","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[16.485,2.727,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.184,0],[0,0],[0,0],[0,0],[0,-4.375]],"o":[[0,4.184],[0,0],[0,0],[0,0],[4.375,0],[0,0]],"v":[[114.116,92.129],[106.54,99.705],[7.788,99.705],[7.788,-99.704],[106.161,-99.704],[114.116,-91.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[5.707,0],[0,0],[1.894,-1.05],[0.886,0.346],[0,0],[2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0],[-1.133,-0.038],[0,0],[0,-1.459],[0,0],[-1.133,-0.038],[0,0],[-5.708,0],[0,0],[-1.894,1.05],[-0.846,-0.289],[0,0],[-2.166,0],[0,0],[0,5.706],[0,0]],"o":[[0,0],[-2.166,0],[-0.883,0.354],[0,0],[-1.895,-1.05],[0,0],[-5.708,0],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.707],[0,0],[2.165,0],[0.833,-0.334],[0,0],[1.894,1.05],[0,0],[5.707,0],[0,0],[0,-5.707]],"v":[[106.16,-102.082],[8.455,-102.082],[2.265,-100.48],[-0.488,-100.468],[-0.519,-100.48],[-6.71,-102.082],[-104.116,-102.082],[-114.45,-91.748],[-114.45,-36.119],[-116.494,-33.44],[-116.494,-18.979],[-114.45,-16.3],[-114.45,-0.877],[-116.494,1.802],[-116.494,28.704],[-114.45,31.383],[-114.45,91.749],[-104.116,102.083],[-6.495,102.083],[-0.305,100.481],[2.294,100.425],[2.395,100.481],[9.872,102.083],[106.161,102.083],[116.494,91.75],[116.494,-91.748]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.529411792755,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
\ No newline at end of file
diff --git a/packages/SystemUI/res/raw/biometricprompt_rear_portrait_base.json b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_base.json
new file mode 100644
index 0000000..9ea0d35
--- /dev/null
+++ b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_base.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Rear_Portrait_Base_Foldable","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 18","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[169.478,169.749,0],"ix":2,"l":2},"a":{"a":0,"k":[-48.123,-30.19,0],"ix":1,"l":2},"s":{"a":0,"k":[132,132,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".grey400","cl":"grey400","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.741176486015,0.75686275959,0.776470601559,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"black circle matte","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey904","cl":"grey904","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,35.536,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.552,0.087],[0,0]],"o":[[0,0],[0,-3.287],[0,0],[0,0]],"v":[[-2.301,8.869],[-2.301,-3.772],[2.301,-9.806],[2.301,9.806]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"black circle matte 2","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue401","cl":"blue401","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,-27.655,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.286],[0,0],[-2.552,0.086],[0,0]],"o":[[0,0],[0,-3.286],[0,0],[-2.552,-0.086]],"v":[[-2.301,16.282],[-2.301,-16.281],[2.301,-22.313],[2.301,22.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"black circle matte 3","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Finger 3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-2,"ix":10},"p":{"a":0,"k":[260.134,83.782,0],"ix":2,"l":2},"a":{"a":0,"k":[302.634,38.782,0],"ix":1,"l":2},"s":{"a":0,"k":[178,178,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.262,5.076],[0,0],[-0.424,-7.095],[-0.028,-0.225]],"o":[[3.269,-3.892],[-12.123,2.932],[0.015,0.234],[0.567,-0.034]],"v":[[9.232,0.652],[11.145,-6.746],[-11.412,6.046],[-11.346,6.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156875134,0.454901963472,0.376470595598,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[241.281,55.033],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.565,-1.102],[3.269,-3.892],[0.566,-0.033],[-18.63,2.353],[-16.656,3.951],[9.004,6.546],[6.9,-2.19]],"o":[[0,0],[-4.262,5.076],[1.008,9.61],[14.171,-1.79],[-4.028,-10.569],[-4.156,1.703],[-4.392,1.392]],"v":[[-13.858,-7.546],[-15.771,-0.148],[-36.349,5.946],[-7.047,16.299],[36.349,9.142],[16.281,-17.051],[-0.156,-11.172]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725507259,0.384313732386,0.282352954149,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[266.285,55.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 4","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"black circle matte 4","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".grey903","cl":"grey903","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.345,-92.442,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.07,0],[0,0],[-8.27,0],[0,0]],"o":[[0,0],[0,8.269],[0,0],[-14.024,-17.379]],"v":[[-29.778,-14.252],[-29.778,-0.721],[-14.805,14.252],[29.778,14.252]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"black circle matte 5","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".grey902","cl":"grey902","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-15.947,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[154.053,139.81,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.3,0.367],[0,0],[-2.364,0.157],[0,0]],"o":[[0,0],[2.3,-0.367],[0,0],[-2.364,-0.157]],"v":[[-3.5,75.533],[-3.5,-75.533],[3.5,-76.312],[3.5,76.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[113.225,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 7","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,8.269],[0,0],[2.181,-0.187],[0,0],[-2.23,0],[0,42.252],[10.593,13.127],[0,0]],"o":[[0,0],[-2.23,0],[0,0],[2.181,0.187],[42.252,0],[0,-18.182],[0,0],[-8.27,0]],"v":[[-34.946,-62.973],[-34.946,-76.504],[-41.558,-76.201],[-41.558,76.201],[-34.946,76.504],[41.558,0],[24.61,-48],[-19.973,-48]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[156.824,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".black 2","cl":"black","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48.123,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-129,"s":[0,0,100]},{"t":-79,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".grey700","cl":"grey700","parent":16,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-56.481,-59.936,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.767,0],[0,0],[0,-3.767],[0,0],[-3.767,0],[0,0],[0,3.767],[0,0]],"o":[[0,0],[-3.767,0],[0,0],[0,3.767],[0,0],[3.767,0],[0,0],[0,-3.767]],"v":[[46.055,-14.479],[-46.056,-14.479],[-52.876,-7.659],[-52.876,7.658],[-46.056,14.479],[46.055,14.479],[52.876,7.658],[52.876,-7.659]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":".grey901","cl":"grey901","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[16.485,2.727,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.184,0],[0,0],[0,0],[0,0],[0,-4.375]],"o":[[0,4.184],[0,0],[0,0],[0,0],[4.375,0],[0,0]],"v":[[114.116,92.129],[106.54,99.705],[7.788,99.705],[7.788,-99.704],[106.161,-99.704],[114.116,-91.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[5.707,0],[0,0],[1.894,-1.05],[0.886,0.346],[0,0],[2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0],[-1.133,-0.038],[0,0],[0,-1.459],[0,0],[-1.133,-0.038],[0,0],[-5.708,0],[0,0],[-1.894,1.05],[-0.846,-0.289],[0,0],[-2.166,0],[0,0],[0,5.706],[0,0]],"o":[[0,0],[-2.166,0],[-0.883,0.354],[0,0],[-1.895,-1.05],[0,0],[-5.708,0],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.707],[0,0],[2.165,0],[0.833,-0.334],[0,0],[1.894,1.05],[0,0],[5.707,0],[0,0],[0,-5.707]],"v":[[106.16,-102.082],[8.455,-102.082],[2.265,-100.48],[-0.488,-100.468],[-0.519,-100.48],[-6.71,-102.082],[-104.116,-102.082],[-114.45,-91.748],[-114.45,-36.119],[-116.494,-33.44],[-116.494,-18.979],[-114.45,-16.3],[-114.45,-0.877],[-116.494,1.802],[-116.494,28.704],[-114.45,31.383],[-114.45,91.749],[-104.116,102.083],[-6.495,102.083],[-0.305,100.481],[2.294,100.425],[2.395,100.481],[9.872,102.083],[106.161,102.083],[116.494,91.75],[116.494,-91.748]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.529411792755,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
\ No newline at end of file
diff --git a/packages/SystemUI/res/raw/biometricprompt_rear_portrait_reverse_base.json b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_reverse_base.json
new file mode 100644
index 0000000..f2b2593
--- /dev/null
+++ b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_reverse_base.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Rear_Portrait_Reverse_Base_Foldable","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 18","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":270,"ix":10},"p":{"a":0,"k":[169.478,169.749,0],"ix":2,"l":2},"a":{"a":0,"k":[-48.123,-30.19,0],"ix":1,"l":2},"s":{"a":0,"k":[132,132,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".grey400","cl":"grey400","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.741176486015,0.75686275959,0.776470601559,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"black circle matte","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey904","cl":"grey904","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,35.536,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.552,0.087],[0,0]],"o":[[0,0],[0,-3.287],[0,0],[0,0]],"v":[[-2.301,8.869],[-2.301,-3.772],[2.301,-9.806],[2.301,9.806]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"black circle matte 2","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue401","cl":"blue401","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,-27.655,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.286],[0,0],[-2.552,0.086],[0,0]],"o":[[0,0],[0,-3.286],[0,0],[-2.552,-0.086]],"v":[[-2.301,16.282],[-2.301,-16.281],[2.301,-22.313],[2.301,22.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"black circle matte 3","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Finger 2","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-75.352,41.307,0],"ix":2,"l":2},"a":{"a":0,"k":[94.648,211.307,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.72,-5.642],[0,0],[-9.394,-0.562],[-0.298,-0.038]],"o":[[-5.153,4.329],[3.882,-16.05],[0.31,0.019],[-0.044,0.75]],"v":[[0.863,12.222],[-8.931,14.755],[8.005,-15.108],[8.931,-15.021]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156875134,0.454901963472,0.376470595598,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[81.486,130.081],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 9","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.459,6.045],[-5.153,4.329],[-0.044,0.75],[3.116,-24.664],[5.23,-22.052],[8.666,11.92],[-2.9,9.135]],"o":[[0,0],[6.72,-5.642],[12.723,1.335],[-2.369,18.762],[-13.993,-5.333],[2.255,-5.502],[1.843,-5.815]],"v":[[-9.99,-18.348],[-0.196,-20.881],[7.872,-48.124],[21.578,-9.331],[12.104,48.124],[-22.574,21.555],[-14.791,-0.206]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725507259,0.384313732386,0.282352954149,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[82.545,163.184],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 8","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"black circle matte 4","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".grey903","cl":"grey903","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.345,-92.442,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.07,0],[0,0],[-8.27,0],[0,0]],"o":[[0,0],[0,8.269],[0,0],[-14.024,-17.379]],"v":[[-29.778,-14.252],[-29.778,-0.721],[-14.805,14.252],[29.778,14.252]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"black circle matte 5","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".grey902","cl":"grey902","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-15.947,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[154.053,139.81,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.3,0.367],[0,0],[-2.364,0.157],[0,0]],"o":[[0,0],[2.3,-0.367],[0,0],[-2.364,-0.157]],"v":[[-3.5,75.533],[-3.5,-75.533],[3.5,-76.312],[3.5,76.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[113.225,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 7","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,8.269],[0,0],[2.181,-0.187],[0,0],[-2.23,0],[0,42.252],[10.593,13.127],[0,0]],"o":[[0,0],[-2.23,0],[0,0],[2.181,0.187],[42.252,0],[0,-18.182],[0,0],[-8.27,0]],"v":[[-34.946,-62.973],[-34.946,-76.504],[-41.558,-76.201],[-41.558,76.201],[-34.946,76.504],[41.558,0],[24.61,-48],[-19.973,-48]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[156.824,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".black 2","cl":"black","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48.123,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-129,"s":[0,0,100]},{"t":-79,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".grey700","cl":"grey700","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-56.481,-59.936,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.767,0],[0,0],[0,-3.767],[0,0],[-3.767,0],[0,0],[0,3.767],[0,0]],"o":[[0,0],[-3.767,0],[0,0],[0,3.767],[0,0],[3.767,0],[0,0],[0,-3.767]],"v":[[46.055,-14.479],[-46.056,-14.479],[-52.876,-7.659],[-52.876,7.658],[-46.056,14.479],[46.055,14.479],[52.876,7.658],[52.876,-7.659]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".grey901","cl":"grey901","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[16.485,2.727,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.184,0],[0,0],[0,0],[0,0],[0,-4.375]],"o":[[0,4.184],[0,0],[0,0],[0,0],[4.375,0],[0,0]],"v":[[114.116,92.129],[106.54,99.705],[7.788,99.705],[7.788,-99.704],[106.161,-99.704],[114.116,-91.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[5.707,0],[0,0],[1.894,-1.05],[0.886,0.346],[0,0],[2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0],[-1.133,-0.038],[0,0],[0,-1.459],[0,0],[-1.133,-0.038],[0,0],[-5.708,0],[0,0],[-1.894,1.05],[-0.846,-0.289],[0,0],[-2.166,0],[0,0],[0,5.706],[0,0]],"o":[[0,0],[-2.166,0],[-0.883,0.354],[0,0],[-1.895,-1.05],[0,0],[-5.708,0],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.707],[0,0],[2.165,0],[0.833,-0.334],[0,0],[1.894,1.05],[0,0],[5.707,0],[0,0],[0,-5.707]],"v":[[106.16,-102.082],[8.455,-102.082],[2.265,-100.48],[-0.488,-100.468],[-0.519,-100.48],[-6.71,-102.082],[-104.116,-102.082],[-114.45,-91.748],[-114.45,-36.119],[-116.494,-33.44],[-116.494,-18.979],[-114.45,-16.3],[-114.45,-0.877],[-116.494,1.802],[-116.494,28.704],[-114.45,31.383],[-114.45,91.749],[-104.116,102.083],[-6.495,102.083],[-0.305,100.481],[2.294,100.425],[2.395,100.481],[9.872,102.083],[106.161,102.083],[116.494,91.75],[116.494,-91.748]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.529411792755,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 49ef330..4f38e60 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -40,6 +40,10 @@
     <dimen name="biometric_dialog_button_negative_max_width">140dp</dimen>
     <dimen name="biometric_dialog_button_positive_max_width">116dp</dimen>
 
+    <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->
+    <dimen name="biometric_auth_pattern_view_size">248dp</dimen>
+    <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen>
+
     <dimen name="global_actions_power_dialog_item_height">130dp</dimen>
     <dimen name="global_actions_power_dialog_item_bottom_margin">35dp</dimen>
 
@@ -59,5 +63,7 @@
     <dimen name="large_dialog_width">348dp</dimen>
 
     <dimen name="qs_panel_padding_top">@dimen/qqs_layout_margin_top</dimen>
-    <dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen>
+
+    <dimen name="controls_header_horizontal_padding">12dp</dimen>
+    <dimen name="controls_content_margin_horizontal">16dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index aefd998..a0e721e 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -29,11 +29,11 @@
 
     <style name="AuthCredentialPatternContainerStyle">
         <item name="android:gravity">center</item>
-        <item name="android:maxHeight">320dp</item>
-        <item name="android:maxWidth">320dp</item>
-        <item name="android:minHeight">200dp</item>
-        <item name="android:minWidth">200dp</item>
-        <item name="android:paddingHorizontal">60dp</item>
+        <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:paddingHorizontal">32dp</item>
         <item name="android:paddingVertical">20dp</item>
     </style>
 
diff --git a/packages/SystemUI/res/values-sw360dp/dimens.xml b/packages/SystemUI/res/values-sw360dp/dimens.xml
index 65ca70b..03365b3 100644
--- a/packages/SystemUI/res/values-sw360dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw360dp/dimens.xml
@@ -25,5 +25,8 @@
 
     <!-- Home Controls -->
     <dimen name="global_actions_side_margin">12dp</dimen>
+
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">298dp</dimen>
 </resources>
 
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/packages/SystemUI/res/values-sw392dp-land/dimens.xml
similarity index 70%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to packages/SystemUI/res/values-sw392dp-land/dimens.xml
index 0d88113..1e26a69 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/packages/SystemUI/res/values-sw392dp-land/dimens.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2022 The Android Open Source Project
   ~
@@ -14,8 +13,9 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+
+<resources>
+    <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->
+    <dimen name="biometric_auth_pattern_view_size">248dp</dimen>
+    <dimen name="biometric_auth_pattern_view_max_size">248dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw392dp/dimens.xml b/packages/SystemUI/res/values-sw392dp/dimens.xml
index 78279ca..96af3c1 100644
--- a/packages/SystemUI/res/values-sw392dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw392dp/dimens.xml
@@ -24,5 +24,8 @@
 
     <!-- Home Controls -->
     <dimen name="global_actions_side_margin">16dp</dimen>
+
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">298dp</dimen>
 </resources>
 
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/packages/SystemUI/res/values-sw410dp-land/dimens.xml
similarity index 70%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to packages/SystemUI/res/values-sw410dp-land/dimens.xml
index 0d88113..c4d9b9b 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/packages/SystemUI/res/values-sw410dp-land/dimens.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2022 The Android Open Source Project
   ~
@@ -14,8 +13,9 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+
+<resources>
+    <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->
+    <dimen name="biometric_auth_pattern_view_size">248dp</dimen>
+    <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw410dp/dimens.xml b/packages/SystemUI/res/values-sw410dp/dimens.xml
index 7da47e5..ff6e005 100644
--- a/packages/SystemUI/res/values-sw410dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw410dp/dimens.xml
@@ -27,4 +27,6 @@
     <dimen name="global_actions_grid_item_side_margin">12dp</dimen>
     <dimen name="global_actions_grid_item_height">72dp</dimen>
 
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 6c7cab5..2a27b47 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -28,6 +28,7 @@
 
     <!-- QS-->
     <dimen name="qs_panel_padding_top">16dp</dimen>
+    <dimen name="qs_panel_padding">24dp</dimen>
     <dimen name="qs_content_horizontal_padding">24dp</dimen>
     <dimen name="qs_horizontal_margin">24dp</dimen>
     <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2,
@@ -44,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-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml
index 8148d3d..c535c64 100644
--- a/packages/SystemUI/res/values-sw600dp-land/styles.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml
@@ -18,10 +18,10 @@
 
     <style name="AuthCredentialPatternContainerStyle">
         <item name="android:gravity">center</item>
-        <item name="android:maxHeight">420dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">200dp</item>
-        <item name="android:minWidth">200dp</item>
+        <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
         <item name="android:paddingHorizontal">120dp</item>
         <item name="android:paddingVertical">40dp</item>
     </style>
diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml
index 771de08..32eefa7 100644
--- a/packages/SystemUI/res/values-sw600dp-port/styles.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml
@@ -26,10 +26,10 @@
 
     <style name="AuthCredentialPatternContainerStyle">
         <item name="android:gravity">center</item>
-        <item name="android:maxHeight">420dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">200dp</item>
-        <item name="android:minWidth">200dp</item>
+        <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
         <item name="android:paddingHorizontal">180dp</item>
         <item name="android:paddingVertical">80dp</item>
     </style>
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index f4434e8..ea3c012 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -36,4 +36,15 @@
     <integer name="qs_security_footer_maxLines">1</integer>
 
     <bool name="config_use_large_screen_shade_header">true</bool>
+
+    <!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a
+    string with two parts: the ID of the slot and the comma-delimited list of affordance IDs,
+    separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The
+    default is displayed by System UI as long as the user hasn't made a different choice for that
+    slot. If the user did make a choice, even if the choice is the "None" option, the default is
+    ignored. -->
+    <string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
+        <item>bottom_start:home</item>
+        <item>bottom_end:create_note</item>
+    </string-array>
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 599bf30..59becc6 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>
@@ -30,10 +31,6 @@
     <!-- Margin on the left side of the carrier text on Keyguard -->
     <dimen name="keyguard_carrier_text_margin">24dp</dimen>
 
-    <!-- The width/height of the phone/camera/unlock icon on keyguard. -->
-    <dimen name="keyguard_affordance_height">80dp</dimen>
-    <dimen name="keyguard_affordance_width">120dp</dimen>
-
     <!-- Screen pinning request width -->
     <dimen name="screen_pinning_request_width">400dp</dimen>
     <!-- Screen pinning request bottom button circle widths -->
@@ -55,9 +52,6 @@
     <!-- Text size for user name in user switcher -->
     <dimen name="kg_user_switcher_text_size">18sp</dimen>
 
-    <dimen name="controls_header_bottom_margin">12dp</dimen>
-    <dimen name="controls_top_margin">24dp</dimen>
-
     <dimen name="global_actions_grid_item_layout_height">80dp</dimen>
 
     <dimen name="qs_brightness_margin_bottom">16dp</dimen>
@@ -68,7 +62,6 @@
     <dimen name="qs_security_footer_background_inset">0dp</dimen>
 
     <dimen name="qs_panel_padding_top">8dp</dimen>
-    <dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen>
 
     <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
     <dimen name="large_dialog_width">472dp</dimen>
@@ -92,4 +85,9 @@
     <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
 
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
+
+    <dimen name="controls_header_horizontal_padding">12dp</dimen>
+    <dimen name="controls_content_margin_horizontal">24dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 3fc59e3..8583f05 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -17,17 +17,14 @@
 */
 -->
 <resources>
-    <dimen name="controls_padding_horizontal">205dp</dimen>
     <dimen name="split_shade_notifications_scrim_margin_bottom">24dp</dimen>
     <dimen name="notification_panel_margin_bottom">64dp</dimen>
 
     <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">251dp</dimen>
+    <dimen name="qs_media_session_height_expanded">184dp</dimen>
     <dimen name="qs_content_horizontal_padding">40dp</dimen>
     <dimen name="qs_horizontal_margin">40dp</dimen>
     <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2,
@@ -36,10 +33,12 @@
     <dimen name="qs_tiles_page_horizontal_margin">20dp</dimen>
 
     <!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
-    <dimen name="qs_media_rec_icon_top_margin">27dp</dimen>
-    <dimen name="qs_media_rec_album_size">152dp</dimen>
+    <dimen name="qs_media_rec_icon_top_margin">16dp</dimen>
+    <dimen name="qs_media_rec_album_size">112dp</dimen>
     <dimen name="qs_media_rec_album_side_margin">16dp</dimen>
 
+    <dimen name="controls_panel_corner_radius">40dp</dimen>
+
     <dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen>
 
     <!-- Roughly the same distance as media on LS to media on QS. We will translate by this value
diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml
index f9ed67d..6a70ebd 100644
--- a/packages/SystemUI/res/values-sw720dp-land/styles.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml
@@ -18,10 +18,10 @@
 
     <style name="AuthCredentialPatternContainerStyle">
         <item name="android:gravity">center</item>
-        <item name="android:maxHeight">420dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">200dp</item>
-        <item name="android:minWidth">200dp</item>
+        <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
         <item name="android:paddingHorizontal">120dp</item>
         <item name="android:paddingVertical">40dp</item>
     </style>
diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
index 8b41a44..9248d58 100644
--- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
@@ -33,5 +33,7 @@
      side -->
     <dimen name="qs_tiles_page_horizontal_margin">60dp</dimen>
 
+    <dimen name="controls_panel_corner_radius">46dp</dimen>
+
     <dimen name="notification_section_divider_height">16dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml
index 78d299c..0a46e08 100644
--- a/packages/SystemUI/res/values-sw720dp-port/styles.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml
@@ -26,10 +26,10 @@
 
     <style name="AuthCredentialPatternContainerStyle">
         <item name="android:gravity">center</item>
-        <item name="android:maxHeight">420dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">200dp</item>
-        <item name="android:minWidth">200dp</item>
+        <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
         <item name="android:paddingHorizontal">240dp</item>
         <item name="android:paddingVertical">120dp</item>
     </style>
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index 0705017..2086459 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -19,8 +19,12 @@
     <!-- gap on either side of status bar notification icons -->
     <dimen name="status_bar_icon_padding">1dp</dimen>
 
-    <dimen name="controls_padding_horizontal">75dp</dimen>
+    <dimen name="controls_header_horizontal_padding">28dp</dimen>
+    <dimen name="controls_content_margin_horizontal">40dp</dimen>
 
     <dimen name="large_screen_shade_header_height">56dp</dimen>
+
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
 </resources>
 
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/packages/SystemUI/res/values-sw800dp/dimens.xml
similarity index 68%
copy from libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
copy to packages/SystemUI/res/values-sw800dp/dimens.xml
index 0d88113..0d82217 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml
+++ b/packages/SystemUI/res/values-sw800dp/dimens.xml
@@ -14,8 +14,11 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid android:color="@color/letterbox_education_accent_primary"/>
-    <corners android:radius="12dp"/>
-</shape>
\ No newline at end of file
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds. -->
+<resources>
+
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index f46266b..e346fe4 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -214,5 +214,12 @@
         <attr name="biometricsEnrollProgressHelp" format="reference|color" />
         <attr name="biometricsEnrollProgressHelpWithTalkback" format="reference|color" />
     </declare-styleable>
+
+    <declare-styleable name="SeekBarWithIconButtonsView_Layout">
+        <attr name="max" format="integer" />
+        <attr name="progress" format="integer" />
+        <attr name="iconStartContentDescription" format="reference" />
+        <attr name="iconEndContentDescription" format="reference" />
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7d72598..3f84ddb 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -81,7 +81,7 @@
 
     <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
     <string name="quick_settings_tiles_stock" translatable="false">
-        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream
+        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling
     </string>
 
     <!-- The tiles to display in QuickSettings -->
@@ -437,6 +437,11 @@
          This name is in the ComponentName flattened format (package/class)  -->
     <string name="config_screenshotEditor" translatable="false"></string>
 
+    <!-- ComponentName for the file browsing app that the system would expect to be used in work
+         profile. The icon for this app will be shown to the user when informing them that a
+         screenshot has been saved to work profile. If blank, a default icon will be shown. -->
+    <string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string>
+
     <!-- Remote copy default activity.  Must handle REMOTE_COPY_ACTION intents.
      This name is in the ComponentName flattened format (package/class)  -->
     <string name="config_remoteCopyPackage" translatable="false"></string>
@@ -445,7 +450,7 @@
     <integer name="watch_heap_limit">256000</integer>
 
     <!-- SystemUI Plugins that can be loaded on user builds. -->
-    <string-array name="config_pluginWhitelist" translatable="false">
+    <string-array name="config_pluginAllowlist" translatable="false">
         <item>com.android.systemui</item>
     </string-array>
 
@@ -649,7 +654,7 @@
         <item>26</item> <!-- MOUTH_COVERING_DETECTED -->
     </integer-array>
 
-    <!-- Which device wake-ups will trigger face auth. These values correspond with
+    <!-- Which device wake-ups will trigger passive auth. These values correspond with
          PowerManager#WakeReason. -->
     <integer-array name="config_face_auth_wake_up_triggers">
         <item>1</item> <!-- WAKE_REASON_POWER_BUTTON -->
@@ -658,11 +663,22 @@
         <item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
         <item>9</item> <!-- WAKE_REASON_LID -->
         <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
+        <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
         <item>15</item> <!-- WAKE_REASON_TAP -->
         <item>16</item> <!-- WAKE_REASON_LIFT -->
         <item>17</item> <!-- WAKE_REASON_BIOMETRIC -->
     </integer-array>
 
+    <!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN)
+         means systemui will try listening on all postures.
+         0 : DEVICE_POSTURE_UNKNOWN
+         1 : DEVICE_POSTURE_CLOSED
+         2 : DEVICE_POSTURE_HALF_OPENED
+         3 : DEVICE_POSTURE_OPENED
+         4 : DEVICE_POSTURE_FLIPPED
+    -->
+    <integer name="config_face_auth_supported_posture">0</integer>
+
     <!-- Whether the communal service should be enabled -->
     <bool name="config_communalServiceEnabled">false</bool>
 
@@ -809,4 +825,12 @@
         <item>bottom_end:wallet</item>
     </string-array>
 
+    <!-- Package name for the app that implements the wallpaper picker. -->
+    <string name="config_wallpaperPickerPackage" translatable="false">
+        com.android.wallpaper
+    </string>
+
+    <!-- Whether the floating rotation button should be on the left/right in the device's natural
+         orientation -->
+    <bool name="floating_rotation_button_position_left">true</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ecb6560..bf949a0 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -43,12 +43,18 @@
     <dimen name="navigation_edge_panel_height">268dp</dimen>
     <!-- The threshold to drag to trigger the edge action -->
     <dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
+    <!-- The drag distance to consider evaluating gesture -->
+    <dimen name="navigation_edge_action_min_distance_to_start_animation">24dp</dimen>
     <!-- The threshold to progress back animation for edge swipe -->
     <dimen name="navigation_edge_action_progress_threshold">412dp</dimen>
     <!-- The minimum display position of the arrow on the screen -->
     <dimen name="navigation_edge_arrow_min_y">64dp</dimen>
     <!-- The amount by which the arrow is shifted to avoid the finger-->
     <dimen name="navigation_edge_finger_offset">64dp</dimen>
+    <!-- The threshold to dynamically activate the edge action -->
+    <dimen name="navigation_edge_action_reactivation_drag_threshold">32dp</dimen>
+    <!-- The threshold to dynamically deactivate the edge action -->
+    <dimen name="navigation_edge_action_deactivation_drag_threshold">32dp</dimen>
 
     <!-- The thickness of the arrow -->
     <dimen name="navigation_edge_arrow_thickness">4dp</dimen>
@@ -56,37 +62,61 @@
     <dimen name="navigation_edge_minimum_x_delta_for_switch">32dp</dimen>
 
     <!-- entry state -->
+    <item name="navigation_edge_entry_scale" format="float" type="dimen">0.98</item>
     <dimen name="navigation_edge_entry_margin">4dp</dimen>
-    <dimen name="navigation_edge_entry_background_width">8dp</dimen>
-    <dimen name="navigation_edge_entry_background_height">60dp</dimen>
-    <dimen name="navigation_edge_entry_edge_corners">30dp</dimen>
-    <dimen name="navigation_edge_entry_far_corners">30dp</dimen>
-    <dimen name="navigation_edge_entry_arrow_length">10dp</dimen>
-    <dimen name="navigation_edge_entry_arrow_height">7dp</dimen>
+    <item name="navigation_edge_entry_background_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_entry_background_width">0dp</dimen>
+    <dimen name="navigation_edge_entry_background_height">48dp</dimen>
+    <dimen name="navigation_edge_entry_edge_corners">6dp</dimen>
+    <dimen name="navigation_edge_entry_far_corners">6dp</dimen>
+    <item name="navigation_edge_entry_arrow_alpha" format="float" type="dimen">0.0</item>
+    <dimen name="navigation_edge_entry_arrow_length">8.6dp</dimen>
+    <dimen name="navigation_edge_entry_arrow_height">5dp</dimen>
 
     <!-- pre-threshold -->
     <dimen name="navigation_edge_pre_threshold_margin">4dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_background_width">64dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_background_height">60dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_edge_corners">22dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_far_corners">26dp</dimen>
+    <item name="navigation_edge_pre_threshold_background_alpha" format="float" type="dimen">1.0
+    </item>
+    <item name="navigation_edge_pre_threshold_scale" format="float" type="dimen">0.98</item>
+    <dimen name="navigation_edge_pre_threshold_background_width">51dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_background_height">46dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_edge_corners">16dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_far_corners">20dp</dimen>
+    <item name="navigation_edge_pre_threshold_arrow_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_pre_threshold_arrow_length">8dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_arrow_height">5.6dp</dimen>
 
-    <!-- post-threshold / active -->
+    <!-- active (post-threshold) -->
+    <item name="navigation_edge_active_scale" format="float" type="dimen">1.0</item>
     <dimen name="navigation_edge_active_margin">14dp</dimen>
-    <dimen name="navigation_edge_active_background_width">60dp</dimen>
-    <dimen name="navigation_edge_active_background_height">60dp</dimen>
-    <dimen name="navigation_edge_active_edge_corners">30dp</dimen>
-    <dimen name="navigation_edge_active_far_corners">30dp</dimen>
-    <dimen name="navigation_edge_active_arrow_length">8dp</dimen>
-    <dimen name="navigation_edge_active_arrow_height">9dp</dimen>
+    <item name="navigation_edge_active_background_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_active_background_width">48dp</dimen>
+    <dimen name="navigation_edge_active_background_height">48dp</dimen>
+    <dimen name="navigation_edge_active_edge_corners">24dp</dimen>
+    <dimen name="navigation_edge_active_far_corners">24dp</dimen>
+    <item name="navigation_edge_active_arrow_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_active_arrow_length">6.4dp</dimen>
+    <dimen name="navigation_edge_active_arrow_height">7.2dp</dimen>
 
+    <!-- committed -->
+    <item name="navigation_edge_committed_scale" format="float" type="dimen">0.85</item>
+    <item name="navigation_edge_committed_alpha" format="float" type="dimen">0</item>
+
+    <!-- cancelled -->
+    <dimen name="navigation_edge_cancelled_background_width">0dp</dimen>
+
+    <item name="navigation_edge_stretch_scale" format="float" type="dimen">1.0</item>
     <dimen name="navigation_edge_stretch_margin">18dp</dimen>
-    <dimen name="navigation_edge_stretch_background_width">74dp</dimen>
-    <dimen name="navigation_edge_stretch_background_height">60dp</dimen>
-    <dimen name="navigation_edge_stretch_edge_corners">30dp</dimen>
-    <dimen name="navigation_edge_stretch_far_corners">30dp</dimen>
-    <dimen name="navigation_edge_stretched_arrow_length">7dp</dimen>
-    <dimen name="navigation_edge_stretched_arrow_height">10dp</dimen>
+    <dimen name="navigation_edge_stretch_background_width">60dp</dimen>
+    <item name="navigation_edge_stretch_background_alpha" format="float" type="dimen">
+        @dimen/navigation_edge_entry_background_alpha
+    </item>
+    <dimen name="navigation_edge_stretch_background_height">48dp</dimen>
+    <dimen name="navigation_edge_stretch_edge_corners">24dp</dimen>
+    <dimen name="navigation_edge_stretch_far_corners">24dp</dimen>
+    <item name="navigation_edge_strech_arrow_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_stretched_arrow_length">5.6dp</dimen>
+    <dimen name="navigation_edge_stretched_arrow_height">8dp</dimen>
 
     <dimen name="navigation_edge_cancelled_arrow_length">12dp</dimen>
     <dimen name="navigation_edge_cancelled_arrow_height">0dp</dimen>
@@ -154,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>
 
@@ -334,15 +373,22 @@
     <dimen name="overlay_action_chip_spacing">8dp</dimen>
     <dimen name="overlay_action_chip_text_size">14sp</dimen>
     <dimen name="overlay_offset_x">16dp</dimen>
+    <!-- Used for both start and bottom margin of the preview, relative to the action container -->
+    <dimen name="overlay_preview_container_margin">8dp</dimen>
     <dimen name="overlay_action_container_margin_horizontal">8dp</dimen>
+    <dimen name="overlay_action_container_margin_bottom">6dp</dimen>
     <dimen name="overlay_bg_protection_height">242dp</dimen>
     <dimen name="overlay_action_container_corner_radius">18dp</dimen>
     <dimen name="overlay_action_container_padding_vertical">4dp</dimen>
     <dimen name="overlay_action_container_padding_right">8dp</dimen>
+    <dimen name="overlay_action_container_padding_end">8dp</dimen>
     <dimen name="overlay_dismiss_button_tappable_size">48dp</dimen>
     <dimen name="overlay_dismiss_button_margin">8dp</dimen>
+    <!-- must be kept aligned with overlay_border_width_neg, below;
+         overlay_border_width = overlay_border_width_neg * -1 -->
     <dimen name="overlay_border_width">4dp</dimen>
-    <!-- need a negative margin for some of the constraints. should be overlay_border_width * -1 -->
+    <!-- some constraints use a negative margin. must be aligned with overlay_border_width, above;
+         overlay_border_width_neg = overlay_border_width * -1 -->
     <dimen name="overlay_border_width_neg">-4dp</dimen>
 
     <dimen name="clipboard_preview_size">@dimen/overlay_x_scale</dimen>
@@ -570,8 +616,7 @@
     <dimen name="qs_dual_tile_padding_horizontal">6dp</dimen>
     <dimen name="qs_panel_elevation">4dp</dimen>
     <dimen name="qs_panel_padding_bottom">@dimen/footer_actions_height</dimen>
-    <dimen name="qs_panel_padding_top">48dp</dimen>
-    <dimen name="qs_panel_padding_top_combined_headers">80dp</dimen>
+    <dimen name="qs_panel_padding_top">80dp</dimen>
 
     <dimen name="qs_data_usage_text_size">14sp</dimen>
     <dimen name="qs_data_usage_usage_text_size">36sp</dimen>
@@ -755,12 +800,10 @@
     <dimen name="go_to_full_shade_appearing_translation">200dp</dimen>
 
     <!-- The width/height of the keyguard bottom area icon view on keyguard. -->
-    <dimen name="keyguard_affordance_height">48dp</dimen>
-    <dimen name="keyguard_affordance_width">48dp</dimen>
-
     <dimen name="keyguard_affordance_fixed_height">48dp</dimen>
     <dimen name="keyguard_affordance_fixed_width">48dp</dimen>
     <dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
+
     <!-- Amount the button should shake when it's not long-pressed for long enough. -->
     <dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>
 
@@ -966,6 +1009,10 @@
     <!-- Biometric Auth Credential values -->
     <dimen name="biometric_auth_icon_size">48dp</dimen>
 
+    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+    <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
+    <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen>
+
     <!-- Starting text size in sp of batteryLevel for wireless charging animation -->
     <item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen">
         0
@@ -1030,8 +1077,6 @@
 
     <dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
 
-    <!-- Size of the RAT type for CellularTile -->
-
     <!-- Size of media cards in the QSPanel carousel -->
     <dimen name="qs_media_padding">16dp</dimen>
     <dimen name="qs_media_album_radius">14dp</dimen>
@@ -1046,6 +1091,7 @@
     <dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
     <dimen name="qs_media_enabled_seekbar_height">2dp</dimen>
     <dimen name="qs_media_app_icon_size">24dp</dimen>
+    <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen>
 
     <dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen>
     <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen>
@@ -1060,8 +1106,13 @@
     <!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
     <dimen name="qs_media_rec_icon_top_margin">16dp</dimen>
     <dimen name="qs_media_rec_album_size">88dp</dimen>
+    <dimen name="qs_media_rec_album_width">110dp</dimen>
+    <dimen name="qs_media_rec_album_height_expanded">108dp</dimen>
+    <dimen name="qs_media_rec_album_height_collapsed">77dp</dimen>
     <dimen name="qs_media_rec_album_side_margin">16dp</dimen>
     <dimen name="qs_media_rec_album_bottom_margin">8dp</dimen>
+    <dimen name="qs_media_rec_album_title_bottom_margin">22dp</dimen>
+    <dimen name="qs_media_rec_album_subtitle_height">12dp</dimen>
 
     <!-- Media tap-to-transfer chip for sender device -->
     <dimen name="media_ttt_chip_outer_padding">16dp</dimen>
@@ -1079,6 +1130,7 @@
          (112 - 40) / 2 = 36dp -->
     <dimen name="media_ttt_generic_icon_padding">36dp</dimen>
     <dimen name="media_ttt_receiver_vert_translation">40dp</dimen>
+    <dimen name="media_ttt_receiver_icon_bottom_margin">10dp</dimen>
 
     <!-- Window magnification -->
     <dimen name="magnification_border_drag_size">35dp</dimen>
@@ -1101,11 +1153,13 @@
 
     <!-- Home Controls -->
     <dimen name="controls_header_menu_size">48dp</dimen>
-    <dimen name="controls_header_bottom_margin">24dp</dimen>
+    <dimen name="controls_header_menu_button_size">48dp</dimen>
+    <dimen name="controls_header_bottom_margin">16dp</dimen>
+    <dimen name="controls_header_horizontal_padding">12dp</dimen>
     <dimen name="controls_header_app_icon_size">24dp</dimen>
     <dimen name="controls_top_margin">48dp</dimen>
-    <dimen name="controls_padding_horizontal">0dp</dimen>
-    <dimen name="control_header_text_size">20sp</dimen>
+    <dimen name="controls_content_margin_horizontal">0dp</dimen>
+    <dimen name="control_header_text_size">24sp</dimen>
     <dimen name="control_item_text_size">16sp</dimen>
     <dimen name="control_menu_item_text_size">16sp</dimen>
     <dimen name="control_menu_item_min_height">56dp</dimen>
@@ -1136,6 +1190,8 @@
     <item name="controls_task_view_width_percentage" translatable="false" format="float" type="dimen">1.0</item>
     <dimen name="controls_task_view_right_margin">0dp</dimen>
 
+    <dimen name="controls_panel_corner_radius">42dp</dimen>
+
     <!-- Home Controls activity view detail panel-->
     <dimen name="controls_activity_view_corner_radius">@*android:dimen/config_bottomDialogCornerRadius</dimen>
 
@@ -1279,6 +1335,15 @@
     <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
     <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
 
+    <!-- LOCKSCREEN -> DREAMING transition: Amount to shift lockscreen content on entering -->
+    <dimen name="lockscreen_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+
+    <!-- GONE -> DREAMING transition: Amount to shift lockscreen content on entering -->
+    <dimen name="gone_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+
+    <!-- LOCKSCREEN -> OCCLUDED transition: Amount to shift lockscreen content on entering -->
+    <dimen name="lockscreen_to_occluded_transition_lockscreen_translation_y">-40dp</dimen>
+
     <!-- The amount of vertical offset for the keyguard during the full shade transition. -->
     <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
 
@@ -1347,6 +1412,9 @@
     <dimen name="padding_above_predefined_icon_for_small">4dp</dimen>
     <dimen name="padding_between_suppressed_layout_items">8dp</dimen>
 
+    <!-- Seekbar with icon buttons -->
+    <dimen name="seekbar_icon_size">24dp</dimen>
+
     <!-- Accessibility floating menu -->
     <dimen name="accessibility_floating_menu_elevation">3dp</dimen>
     <dimen name="accessibility_floating_menu_stroke_width">1dp</dimen>
@@ -1617,6 +1685,8 @@
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+    <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen>
+    <dimen name="dream_overlay_status_bar_marginTop">22dp</dimen>
 
     <!-- Default device corner radius, used for assist UI -->
     <dimen name="config_rounded_mask_size">0px</dimen>
@@ -1628,4 +1698,10 @@
     <dimen name="rear_display_animation_height">200dp</dimen>
     <dimen name="rear_display_title_top_padding">24dp</dimen>
     <dimen name="rear_display_title_bottom_padding">16dp</dimen>
+
+    <!--
+    Vertical distance between the pointer and the popup menu that shows up on the lock screen when
+    it is long-pressed.
+    -->
+    <dimen name="keyguard_long_press_settings_popup_vertical_offset">96dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index c5ffc94..6354752 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -33,8 +33,6 @@
     <!-- Whether to show chipbar UI whenever the device is unlocked by ActiveUnlock. -->
     <bool name="flag_active_unlock_chipbar">true</bool>
 
-    <bool name="flag_smartspace">false</bool>
-
     <!--  Whether the user switcher chip shows in the status bar. When true, the multi user
       avatar will no longer show on the lockscreen -->
     <bool name="flag_user_switcher_chip">false</bool>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 35b7c3c..48f257a 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -240,7 +240,13 @@
     <!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
     <string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
     <!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
-    <string name="screenshot_work_profile_notification" translatable="false">Work screenshots are saved in the work <xliff:g id="app" example="Files">%1$s</xliff:g> app</string>
+    <string name="screenshot_work_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the work profile</string>
+    <!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
+    <string name="screenshot_default_files_app_name">Files</string>
+    <!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
+    <string name="screenshot_detected_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> detected this screenshot.</string>
+    <!-- A notice shown to the user to indicate that multiple apps have detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
+    <string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string>
 
     <!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
     <string name="screenrecord_name">Screen Recorder</string>
@@ -473,6 +479,8 @@
     <string name="accessibility_desc_notification_shade">Notification shade.</string>
     <!-- Content description for the quick settings panel (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_desc_quick_settings">Quick settings.</string>
+    <!-- Content description for the split notification shade that also includes QS (not shown on the screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_desc_qs_notification_shade">Quick settings and Notification shade.</string>
     <!-- Content description for the lock screen (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_desc_lock_screen">Lock screen.</string>
     <!-- Content description for the work profile lock screen. This prevents work profile apps from being used, but personal apps can be used as normal (not shown on the screen). [CHAR LIMIT=NONE] -->
@@ -654,6 +662,8 @@
     <string name="quick_settings_inversion_label">Color inversion</string>
     <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_color_correction_label">Color correction</string>
+    <!-- QuickSettings: Label for font size scaling. [CHAR LIMIT=NONE] -->
+    <string name="quick_settings_font_scaling_label">Font size</string>
     <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_more_user_settings">Manage users</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] -->
@@ -1453,10 +1463,10 @@
     <string name="notification_conversation_summary_low">No sound or vibration and appears lower in conversation section</string>
 
     <!-- [CHAR LIMIT=150] Notification Importance title: normal importance level summary -->
-    <string name="notification_channel_summary_default">May ring or vibrate based on phone settings</string>
+    <string name="notification_channel_summary_default">May ring or vibrate based on device settings</string>
 
     <!-- [CHAR LIMIT=150] Conversation Notification Importance title: normal conversation level, with bubbling summary -->
-    <string name="notification_channel_summary_default_with_bubbles">May ring or vibrate based on phone settings. Conversations from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubble by default.</string>
+    <string name="notification_channel_summary_default_with_bubbles">May ring or vibrate based on device settings. Conversations from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubble by default.</string>
 
     <!-- [CHAR LIMIT=150] Notification Importance title: automatic importance level summary -->
     <string name="notification_channel_summary_automatic">Have the system determine if this notification should make sound or vibration</string>
@@ -2175,6 +2185,14 @@
     <!-- Title of the overlay warning the user to interact with the device or it will go to sleep. [CHAR LIMIT=25] -->
     <string name="inattentive_sleep_warning_title">Standby</string>
 
+    <!-- Font scaling -->
+    <!-- Font scaling: Quick Settings dialog title [CHAR LIMIT=30] -->
+    <string name="font_scaling_dialog_title">Font Size</string>
+    <!-- Content Description for the icon button to make fonts smaller. [CHAR LIMIT=30] -->
+    <string name="font_scaling_smaller">Make smaller</string>
+    <!-- Content Description for the icon button to make fonts larger. [CHAR LIMIT=30] -->
+    <string name="font_scaling_larger">Make larger</string>
+
     <!-- Window Magnification strings -->
     <!-- Title for Magnification Window [CHAR LIMIT=NONE] -->
     <string name="magnification_window_title">Magnification Window</string>
@@ -2236,6 +2254,18 @@
     <!-- Removed control in management screen [CHAR LIMIT=20] -->
     <string name="controls_removed">Removed</string>
 
+    <!-- Title for the dialog presented to the user to authorize this app to display a Device
+         controls panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=30] -->
+    <string name="controls_panel_authorization_title">Add <xliff:g id="appName" example="My app">%s</xliff:g>?</string>
+
+    <!-- Shows in a dialog presented to the user to authorize this app to display a Device controls
+         panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] -->
+    <string name="controls_panel_authorization">When you add <xliff:g id="appName" example="My app">%s</xliff:g>, it can add controls and content to this panel. In some apps, you can choose which controls show up here.</string>
+
+    <!-- Shows in a dialog presented to the user to authorize this app removal from a Device
+         controls panel [CHAR LIMIT=NONE] -->
+    <string name="controls_panel_remove_app_authorization">Remove controls for <xliff:g example="My app" id="appName">%s</xliff:g>?</string>
+
     <!-- a11y state description for a control that is currently favorited [CHAR LIMIT=NONE] -->
     <string name="accessibility_control_favorite">Favorited</string>
     <!-- a11y state description for a control that is currently favorited with its position [CHAR LIMIT=NONE] -->
@@ -2276,6 +2306,8 @@
     <string name="controls_dialog_title">Add to device controls</string>
     <!-- Controls dialog add to favorites [CHAR LIMIT=40] -->
     <string name="controls_dialog_ok">Add</string>
+    <!-- Controls dialog remove app from a panel [CHAR LIMIT=40] -->
+    <string name="controls_dialog_remove">Remove</string>
     <!-- Controls dialog message. Indicates app that suggested this control [CHAR LIMIT=NONE] -->
     <string name="controls_dialog_message">Suggested by <xliff:g id="app" example="System UI">%s</xliff:g></string>
     <!-- Controls tile secondary label when device is locked and user does not want access to controls from lockscreen [CHAR LIMIT=20] -->
@@ -2350,6 +2382,8 @@
     <string name="controls_media_smartspace_rec_item_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
     <!-- Description for Smartspace recommendation's media item which doesn't have artist info, including information for the media's title and the source app [CHAR LIMIT=NONE]-->
     <string name="controls_media_smartspace_rec_item_no_artist_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%2$s</xliff:g></string>
+    <!-- Header title for Smartspace recommendation card within media controls. [CHAR_LIMIT=30] -->
+    <string name="controls_media_smartspace_rec_header">For You</string>
 
     <!--- ****** Media tap-to-transfer ****** -->
     <!-- Text for a button to undo the media transfer. [CHAR LIMIT=20] -->
@@ -2357,13 +2391,19 @@
     <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] -->
     <string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
     <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to transfer media from the different device and back onto the current device. [CHAR LIMIT=75] -->
-    <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string>
+    <string name="media_move_closer_to_end_cast">To play here, move closer to <xliff:g id="deviceName" example="tablet">%1$s</xliff:g></string>
     <!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
     <string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
-    <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] -->
+    <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] -->
     <string name="media_transfer_failed">Something went wrong. Try again.</string>
     <!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] -->
     <string name="media_transfer_loading">Loading</string>
+    <!-- Default name of the device. [CHAR LIMIT=30] -->
+    <string name="media_ttt_default_device_type">tablet</string>
+    <!-- Description of media transfer icon of unknown app appears in receiver devices. [CHAR LIMIT=NONE]-->
+    <string name="media_transfer_receiver_content_description_unknown_app">Casting your media</string>
+    <!-- Description of media transfer icon appears in receiver devices. [CHAR LIMIT=NONE]-->
+    <string name="media_transfer_receiver_content_description_with_app_name">Casting <xliff:g id="app_label" example="Spotify">%1$s</xliff:g></string>
 
     <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
     <string name="controls_error_timeout">Inactive, check app</string>
@@ -2383,6 +2423,10 @@
     <string name="controls_menu_add">Add controls</string>
     <!-- Controls menu, edit [CHAR_LIMIT=30] -->
     <string name="controls_menu_edit">Edit controls</string>
+    <!-- Controls menu, add another app [CHAR LIMIT=30] -->
+    <string name="controls_menu_add_another_app">Add app</string>
+    <!-- Controls menu, remove app [CHAR_LIMIT=30] -->
+    <string name="controls_menu_remove">Remove app</string>
 
     <!-- Title for the media output dialog with media related devices [CHAR LIMIT=50] -->
     <string name="media_output_dialog_add_output">Add outputs</string>
@@ -2412,6 +2456,10 @@
     <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
     <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] -->
     <string name="media_output_group_title_speakers_and_displays">Speakers &amp; Displays</string>
+    <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
+    <string name="media_output_group_title_suggested_device">Suggested Devices</string>
+    <!-- Sub status indicates device need premium account. [CHAR LIMIT=NONE] -->
+    <string name="media_output_status_require_premium">Requires premium account</string>
 
     <!-- Media Output Broadcast Dialog -->
     <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
@@ -2755,20 +2803,68 @@
     <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
     <string name="rear_display_bottom_sheet_cancel">Cancel</string>
     <!-- Text for the user to confirm they flipped the device around. [CHAR_LIMIT=NONE] -->
-    <string name="rear_display_bottom_sheet_confirm">Flip now</string>
+    <string name="rear_display_bottom_sheet_confirm">Switch screens now</string>
     <!-- Text for education page title to guide user to unfold phone. [CHAR_LIMIT=50] -->
-    <string name="rear_display_fold_bottom_sheet_title">Unfold phone for a better selfie</string>
-    <!-- Text for education page title to guide user to flip to the front display. [CHAR_LIMIT=50] -->
-    <string name="rear_display_unfold_bottom_sheet_title">Flip to front display for a better selfie?</string>
+    <string name="rear_display_folded_bottom_sheet_title">Unfold phone</string>
+    <!-- Text for education page title to guide user to switch to the front display. [CHAR_LIMIT=50] -->
+    <string name="rear_display_unfolded_bottom_sheet_title">Switch screens?</string>
     <!-- Text for education page description to suggest user to use rear selfie capture. [CHAR_LIMIT=NONE] -->
-    <string name="rear_display_bottom_sheet_description">Use the rear-facing camera for a wider photo with higher resolution.</string>
-    <!-- Text for education page description to warn user that the display will turn off if the button is clicked. [CHAR_LIMIT=NONE] -->
-    <string name="rear_display_bottom_sheet_warning"><b>&#x2731; This screen will turn off</b></string>
+    <string name="rear_display_folded_bottom_sheet_description">For higher resolution, use the rear camera</string>
+    <!-- Text for unfolded education page description to suggest user to use rear selfie capture. [CHAR_LIMIT=NONE] -->
+    <string name="rear_display_unfolded_bottom_sheet_description">For higher resolution, flip the phone</string>
     <!-- Text for education page content description for folded animation. [CHAR_LIMIT=NONE] -->
     <string name="rear_display_accessibility_folded_animation">Foldable device being unfolded</string>
     <!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] -->
     <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string>
 
+    <!-- Title for notification of low stylus battery with percentage. "percentage" is
+        the value of the battery capacity remaining [CHAR LIMIT=none]-->
+    <string name="stylus_battery_low_percentage"><xliff:g id="percentage" example="16%">%s</xliff:g> battery remaining</string>
+    <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]-->
+    <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string>
+
     <!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] -->
     <string name="stylus_battery_low">Stylus battery low</string>
+
+    <!-- Label for a lock screen shortcut to start the camera in video mode. [CHAR_LIMIT=16] -->
+    <string name="video_camera">Video camera</string>
+
+    <!-- Switch to work profile dialer app for placing a call dialog. -->
+    <!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work
+    profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] -->
+    <string name="call_from_work_profile_title">Can\'t call from this profile</string>
+    <!-- Text for switch to work profile for call dialog to guide users to make call from work
+    profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=NONE]
+    -->
+    <string name="call_from_work_profile_text">Your work policy allows you to make phone calls only from the work profile</string>
+    <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work
+    profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] -->
+    <string name="call_from_work_profile_action">Switch to work profile</string>
+    <!-- Label for the close button on switch to work profile dialog. Switch to work profile dialog guide users to make call from work
+    profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] -->
+    <string name="call_from_work_profile_close">Close</string>
+
+    <!--
+    Label for a menu item in a menu that is shown when the user wishes to configure the lock screen.
+    Clicking on this menu item takes the user to a screen where they can modify the settings of the
+    lock screen.
+
+    [CHAR LIMIT=32]
+    -->
+    <string name="lock_screen_settings">Lock screen settings</string>
+
+    <!-- Content description for Wi-Fi not available icon on dream [CHAR LIMIT=NONE]-->
+    <string name="wifi_unavailable_dream_overlay_content_description">Wi-Fi not available</string>
+
+    <!-- Content description for camera blocked icon on dream [CHAR LIMIT=NONE] -->
+    <string name="camera_blocked_dream_overlay_content_description">Camera blocked</string>
+
+    <!-- Content description for camera and microphone blocked icon on dream [CHAR LIMIT=NONE] -->
+    <string name="camera_and_microphone_blocked_dream_overlay_content_description">Camera and microphone blocked</string>
+
+    <!-- Content description for camera and microphone disabled icon on dream [CHAR LIMIT=NONE] -->
+    <string name="microphone_blocked_dream_overlay_content_description">Microphone blocked</string>
+
+    <!-- Content description for priority mode icon on dream [CHAR LIMIT=NONE] -->
+    <string name="priority_mode_dream_overlay_content_description">Priority mode on</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b11b6d6..58b0234 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -251,11 +251,12 @@
 
     <style name="AuthCredentialPatternContainerStyle">
         <item name="android:gravity">center</item>
-        <item name="android:maxHeight">420dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">200dp</item>
-        <item name="android:minWidth">200dp</item>
-        <item name="android:padding">20dp</item>
+        <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+        <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
+        <item name="android:paddingHorizontal">32dp</item>
+        <item name="android:paddingVertical">20dp</item>
     </style>
 
     <style name="AuthCredentialPinPasswordContainerStyle">
@@ -677,6 +678,17 @@
 
     <style name="MediaPlayer.Recommendation"/>
 
+    <style name="MediaPlayer.Recommendation.Header">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginTop">@dimen/qs_media_padding</item>
+        <item name="android:layout_marginStart">@dimen/qs_media_padding</item>
+        <item name="android:fontFamily">=@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
     <style name="MediaPlayer.Recommendation.AlbumContainer">
         <item name="android:layout_width">@dimen/qs_media_rec_album_size</item>
         <item name="android:layout_height">@dimen/qs_media_rec_album_size</item>
@@ -685,6 +697,12 @@
         <item name="android:layout_marginBottom">@dimen/qs_media_rec_album_bottom_margin</item>
     </style>
 
+    <style name="MediaPlayer.Recommendation.AlbumContainer.Updated">
+        <item name="android:layout_width">@dimen/qs_media_rec_album_width</item>
+        <item name="android:background">@drawable/qs_media_light_source</item>
+        <item name="android:layout_marginTop">@dimen/qs_media_info_spacing</item>
+    </style>
+
     <style name="MediaPlayer.Recommendation.Album">
         <item name="android:backgroundTint">@color/media_player_album_bg</item>
     </style>
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index c809551..7020d54 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -318,4 +318,14 @@
         <item>Off</item>
         <item>On</item>
     </string-array>
+
+    <!-- State names for font scaling tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_font_scaling">
+        <item>Unavailable</item>
+        <item>Off</item>
+        <item>On</item>
+    </string-array>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml
index 06d425c..bf576dc 100644
--- a/packages/SystemUI/res/xml/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,105 +14,73 @@
   ~ limitations under the License.
   -->
 
-<ConstraintSet
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<ConstraintSet xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/large_screen_header_constraint">
 
-    <Constraint
-        android:id="@+id/clock">
+    <Constraint android:id="@+id/clock">
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="0dp"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/date"
-            app:layout_constraintHorizontal_bias="0"
-        />
-        <Transform
-            android:scaleX="1"
-            android:scaleY="1"
-            />
+            app:layout_constraintStart_toEndOf="@id/begin_guide"
+            app:layout_constraintTop_toTopOf="parent" />
+        <PropertySet android:alpha="1" />
     </Constraint>
 
-    <Constraint
-        android:id="@+id/date">
+    <Constraint android:id="@+id/date">
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="0dp"
+            android:layout_marginStart="8dp"
+            app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toEndOf="@id/clock"
-            app:layout_constraintEnd_toStartOf="@id/carrier_group"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintHorizontal_bias="0"
-        />
+            app:layout_constraintTop_toTopOf="parent" />
+        <PropertySet android:alpha="1" />
     </Constraint>
 
-    <Constraint
-        android:id="@+id/carrier_group">
+    <Constraint android:id="@+id/carrier_group">
         <Layout
-            app:layout_constraintWidth_min="48dp"
             android:layout_width="0dp"
             android:layout_height="0dp"
-            app:layout_constrainedWidth="true"
             android:layout_gravity="end|center_vertical"
-            android:layout_marginStart="8dp"
-            app:layout_constraintStart_toEndOf="@id/date"
-            app:layout_constraintEnd_toStartOf="@id/statusIcons"
-            app:layout_constraintTop_toTopOf="@id/clock"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintHorizontal_bias="1"
-            />
-        <PropertySet
-            android:alpha="1"
-        />
+            app:layout_constraintEnd_toStartOf="@id/statusIcons"
+            app:layout_constraintStart_toEndOf="@id/date"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintWidth_default="wrap"
+            app:layout_constraintWidth_min="48dp" />
+        <PropertySet android:alpha="1" />
     </Constraint>
 
-    <Constraint
-        android:id="@+id/statusIcons">
+    <Constraint android:id="@+id/statusIcons">
         <Layout
-            app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
             android:layout_width="wrap_content"
             android:layout_height="@dimen/large_screen_shade_header_min_height"
-            app:layout_constraintStart_toEndOf="@id/carrier_group"
-            app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
-            app:layout_constraintTop_toTopOf="@id/clock"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintHorizontal_bias="1"
-            />
-        <PropertySet
-            android:alpha="1"
-        />
+            app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/carrier_group"/>
+        <PropertySet android:alpha="1" />
     </Constraint>
 
-    <Constraint
-        android:id="@+id/batteryRemainingIcon">
+    <Constraint android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="0dp"
             app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
-            app:layout_constraintStart_toEndOf="@id/statusIcons"
-            app:layout_constraintEnd_toStartOf="@id/privacy_container"
-            app:layout_constraintTop_toTopOf="@id/clock"
             app:layout_constraintBottom_toBottomOf="parent"
-        />
-        <PropertySet
-            android:alpha="1"
-        />
+            app:layout_constraintEnd_toStartOf="@id/privacy_container"
+            app:layout_constraintTop_toTopOf="parent" />
+        <PropertySet android:alpha="1" />
     </Constraint>
 
-    <Constraint
-        android:id="@+id/privacy_container">
+    <Constraint android:id="@+id/privacy_container">
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/large_screen_shade_header_min_height"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toTopOf="@id/date"
-            app:layout_constraintBottom_toBottomOf="@id/date"
-            app:layout_constraintStart_toEndOf="@id/batteryRemainingIcon"
-            app:layout_constraintHorizontal_bias="1"
-        />
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/end_guide"
+            app:layout_constraintTop_toTopOf="parent" />
     </Constraint>
-
 </ConstraintSet>
\ No newline at end of file
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml b/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml
new file mode 100644
index 0000000..d3be3c7
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml
@@ -0,0 +1,64 @@
+<?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
+  -->
+<ConstraintSet
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    >
+
+    <Constraint
+        android:id="@+id/sizing_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_collapsed"
+        />
+
+    <Constraint
+        android:id="@+id/media_rec_title"
+        style="@style/MediaPlayer.Recommendation.Header"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <Constraint
+        android:id="@+id/media_cover1_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginStart="@dimen/qs_media_padding"
+        app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/>
+
+
+    <Constraint
+        android:id="@+id/media_cover2_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+        app:layout_constraintStart_toEndOf="@id/media_cover1_container"
+        app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/>
+
+    <Constraint
+        android:id="@+id/media_cover3_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
+        android:layout_marginEnd="@dimen/qs_media_padding"
+        app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+        app:layout_constraintStart_toEndOf="@id/media_cover2_container"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml b/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml
new file mode 100644
index 0000000..88c7055
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml
@@ -0,0 +1,71 @@
+<?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
+  -->
+<ConstraintSet
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    >
+
+    <Constraint
+        android:id="@+id/sizing_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_expanded"
+        />
+
+    <Constraint
+        android:id="@+id/media_rec_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qs_media_padding"
+        android:layout_marginStart="@dimen/qs_media_padding"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+        android:singleLine="true"
+        android:textSize="14sp"
+        android:textColor="@color/notification_primary_text_color"/>
+
+    <Constraint
+        android:id="@+id/media_cover1_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        android:layout_height="@dimen/qs_media_rec_album_height_expanded"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginStart="@dimen/qs_media_padding"
+        app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/>
+
+
+    <Constraint
+        android:id="@+id/media_cover2_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        android:layout_height="@dimen/qs_media_rec_album_height_expanded"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+        app:layout_constraintStart_toEndOf="@id/media_cover1_container"
+        app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/>
+
+    <Constraint
+        android:id="@+id/media_cover3_container"
+        style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+        android:layout_height="@dimen/qs_media_rec_album_height_expanded"
+        android:layout_marginEnd="@dimen/qs_media_padding"
+        app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+        app:layout_constraintStart_toEndOf="@id/media_cover2_container"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index 1eb621e..5129fc0 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -66,6 +66,22 @@
         app:layout_constraintTop_toBottomOf="@id/icon"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintHorizontal_bias="0" />
+
+    <Constraint
+        android:id="@+id/media_explicit_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_padding"
+        android:layout_marginTop="@dimen/qs_media_icon_offset"
+        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintEnd_toStartOf="@id/header_artist"
+        app:layout_constraintTop_toTopOf="@id/header_artist"
+        app:layout_constraintBottom_toBottomOf="@id/header_artist"
+        app:layout_constraintVertical_bias="0"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed" />
+
     <Constraint
         android:id="@+id/header_artist"
         android:layout_width="wrap_content"
@@ -75,9 +91,8 @@
         app:layout_constraintEnd_toStartOf="@id/action_button_guideline"
         app:layout_constrainedWidth="true"
         app:layout_constraintTop_toBottomOf="@id/header_title"
-        app:layout_constraintStart_toStartOf="@id/header_title"
-        app:layout_constraintVertical_bias="0"
-        app:layout_constraintHorizontal_bias="0" />
+        app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
+        app:layout_constraintVertical_bias="0" />
 
     <Constraint
         android:id="@+id/actionPlayPause"
diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml
index 7de0a5e..0cdc0f9 100644
--- a/packages/SystemUI/res/xml/media_session_expanded.xml
+++ b/packages/SystemUI/res/xml/media_session_expanded.xml
@@ -58,6 +58,21 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintBottom_toTopOf="@id/header_artist"
         app:layout_constraintHorizontal_bias="0" />
+
+    <Constraint
+        android:id="@+id/media_explicit_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_padding"
+        android:layout_marginTop="0dp"
+        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintEnd_toStartOf="@id/header_artist"
+        app:layout_constraintTop_toTopOf="@id/header_artist"
+        app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed"/>
+
     <Constraint
         android:id="@+id/header_artist"
         android:layout_width="wrap_content"
@@ -67,10 +82,9 @@
         android:layout_marginTop="0dp"
         app:layout_constrainedWidth="true"
         app:layout_constraintEnd_toStartOf="@id/actionPlayPause"
-        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
         app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
-        app:layout_constraintVertical_bias="0"
-        app:layout_constraintHorizontal_bias="0" />
+        app:layout_constraintVertical_bias="0" />
 
     <Constraint
         android:id="@+id/actionPlayPause"
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index e56e5d5..00a0444 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -48,8 +48,7 @@
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintEnd_toStartOf="@id/barrier"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintBaseline_toBaselineOf="@id/clock"
             app:layout_constraintHorizontal_bias="0"
         />
     </Constraint>
diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml
index d97031f..52a98984 100644
--- a/packages/SystemUI/res/xml/qs_header.xml
+++ b/packages/SystemUI/res/xml/qs_header.xml
@@ -41,9 +41,6 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
             app:layout_constraintBottom_toBottomOf="@id/carrier_group"
-            app:layout_constraintEnd_toStartOf="@id/carrier_group"
-            app:layout_constraintHorizontal_bias="0"
-            app:layout_constraintHorizontal_chainStyle="spread_inside"
         />
         <Transform
             android:scaleX="2.57"
@@ -62,18 +59,18 @@
         />
     </Constraint>
 
+    <!-- LargeScreenShadeHeaderController helps with managing clock width to layout this view -->
     <Constraint
         android:id="@+id/carrier_group">
         <Layout
-            app:layout_constraintWidth_min="48dp"
-            android:layout_width="wrap_content"
+            android:layout_width="0dp"
             android:layout_height="@dimen/large_screen_shade_header_min_height"
-            app:layout_constraintStart_toEndOf="@id/clock"
+            app:layout_constraintWidth_min="48dp"
+            app:layout_constraintWidth_default="wrap"
+            app:layout_constraintStart_toStartOf="@id/clock"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
             app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_bias="1"
             app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon"
-            app:layout_constraintHorizontal_chainStyle="spread_inside"
             />
         <PropertySet
             android:alpha="1"
diff --git a/packages/SystemUI/screenshot/AndroidManifest.xml b/packages/SystemUI/screenshot/AndroidManifest.xml
index a405836..ba3dc8c 100644
--- a/packages/SystemUI/screenshot/AndroidManifest.xml
+++ b/packages/SystemUI/screenshot/AndroidManifest.xml
@@ -20,6 +20,7 @@
     <application>
         <activity
             android:name="com.android.systemui.testing.screenshot.ScreenshotActivity"
+            android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
             android:exported="true"
             android:theme="@style/Theme.SystemUI.Screenshot" />
     </application>
diff --git a/packages/SystemUI/scripts/token_alignment/.eslintrc.json b/packages/SystemUI/scripts/token_alignment/.eslintrc.json
new file mode 100644
index 0000000..69dc00e
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/.eslintrc.json
@@ -0,0 +1,31 @@
+{
+    "env": {
+        "es2021": true,
+        "node": true
+    },
+    "parserOptions": {
+        "ecmaVersion": "latest",
+        "sourceType": "module"
+    },
+    "plugins": ["prettier", "@typescript-eslint", "eslint-plugin-simple-import-sort", "import"],
+    "extends": ["prettier", "eslint:recommended", "plugin:@typescript-eslint/recommended"],
+    "rules": {
+        "prettier/prettier": ["error"],
+        "no-unused-vars": "off",
+        "@typescript-eslint/no-unused-vars": [
+            "warn",
+            {
+                "argsIgnorePattern": "^_",
+                "varsIgnorePattern": "^_",
+                "caughtErrorsIgnorePattern": "^_"
+            }
+        ],
+        "no-multiple-empty-lines": ["error", { "max": 2 }],
+        "no-multi-spaces": "error",
+        "simple-import-sort/imports": "error",
+        "simple-import-sort/exports": "error",
+        "import/first": "error",
+        "import/newline-after-import": "error",
+        "import/no-duplicates": "error"
+    }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/.gitignore b/packages/SystemUI/scripts/token_alignment/.gitignore
new file mode 100644
index 0000000..96ce14f
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/.gitignore
@@ -0,0 +1,2 @@
+vscode
+node_modules
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/.prettierrc b/packages/SystemUI/scripts/token_alignment/.prettierrc
new file mode 100644
index 0000000..20f02f9
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/.prettierrc
@@ -0,0 +1,9 @@
+{
+   "tabWidth": 4,
+   "printWidth": 100,
+   "semi": true,
+   "singleQuote": true,
+    "bracketSameLine": true,
+    "bracketSpacing": true,
+    "arrowParens": "always"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts b/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts
new file mode 100644
index 0000000..80e075c
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts
@@ -0,0 +1,297 @@
+// Copyright 2022 Google LLC
+
+// 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.
+
+type IElementComment =
+    | { commentNode: undefined; textContent: undefined; hidden: undefined }
+    | { commentNode: Node; textContent: string; hidden: boolean };
+
+interface ITag {
+    attrs?: Record<string, string | number>;
+    tagName: string;
+}
+
+export interface INewTag extends ITag {
+    content?: string | number;
+    comment?: string;
+}
+
+export type IUpdateTag = Partial<Omit<INewTag, 'tagName'>>;
+
+export default class DOM {
+    static addEntry(containerElement: Element, tagOptions: INewTag) {
+        const doc = containerElement.ownerDocument;
+        const exists = this.alreadyHasEntry(containerElement, tagOptions);
+
+        if (exists) {
+            console.log('Ignored adding entry already available: ', exists.outerHTML);
+            return;
+        }
+
+        let insertPoint: Node | null = containerElement.lastElementChild; //.childNodes[containerElement.childNodes.length - 1];
+
+        if (!insertPoint) {
+            console.log('Ignored adding entry in empity parent: ', containerElement.outerHTML);
+            return;
+        }
+
+        const { attrs, comment, content, tagName } = tagOptions;
+
+        if (comment) {
+            const commentNode = doc.createComment(comment);
+            this.insertAfterIdented(commentNode, insertPoint);
+            insertPoint = commentNode;
+        }
+
+        const newEl = doc.createElement(tagName);
+        if (content) newEl.innerHTML = content.toString();
+        if (attrs)
+            Object.entries(attrs).forEach(([attr, value]) =>
+                newEl.setAttribute(attr, value.toString())
+            );
+        this.insertAfterIdented(newEl, insertPoint);
+
+        return true;
+    }
+
+    static insertBeforeIndented(newNode: Node, referenceNode: Node) {
+        const paddingNode = referenceNode.previousSibling;
+        const ownerDoc = referenceNode.ownerDocument;
+        const containerNode = referenceNode.parentNode;
+
+        if (!paddingNode || !ownerDoc || !containerNode) return;
+
+        const currentPadding = paddingNode.textContent || '';
+        const textNode = referenceNode.ownerDocument.createTextNode(currentPadding);
+
+        containerNode.insertBefore(newNode, referenceNode);
+        containerNode.insertBefore(textNode, newNode);
+    }
+
+    static insertAfterIdented(newNode: Node, referenceNode: Node) {
+        const paddingNode = referenceNode.previousSibling;
+        const ownerDoc = referenceNode.ownerDocument;
+        const containerNode = referenceNode.parentNode;
+
+        if (!paddingNode || !ownerDoc || !containerNode) return;
+
+        const currentPadding = paddingNode.textContent || '';
+        const textNode = ownerDoc.createTextNode(currentPadding);
+
+        containerNode.insertBefore(newNode, referenceNode.nextSibling);
+        containerNode.insertBefore(textNode, newNode);
+    }
+
+    static getElementComment(el: Element): IElementComment {
+        const commentNode = el.previousSibling?.previousSibling;
+
+        const out = { commentNode: undefined, textContent: undefined, hidden: undefined };
+
+        if (!commentNode) return out;
+
+        const textContent = commentNode.textContent || '';
+        const hidden = textContent.substring(textContent.length - 6) == '@hide ';
+
+        if (!(commentNode && commentNode.nodeName == '#comment')) return out;
+
+        return { commentNode, textContent, hidden: hidden };
+    }
+
+    static duplicateEntryWithChange(
+        templateElement: Element,
+        options: Omit<IUpdateTag, 'content'>
+    ) {
+        const exists = this.futureEntryAlreadyExist(templateElement, options);
+        if (exists) {
+            console.log('Ignored duplicating entry already available: ', exists.outerHTML);
+            return;
+        }
+
+        const { commentNode } = this.getElementComment(templateElement);
+        let insertPoint: Node = templateElement;
+
+        if (commentNode) {
+            const newComment = commentNode.cloneNode();
+            this.insertAfterIdented(newComment, insertPoint);
+            insertPoint = newComment;
+        }
+
+        const newEl = templateElement.cloneNode(true) as Element;
+        this.insertAfterIdented(newEl, insertPoint);
+
+        this.updateElement(newEl, options);
+        return true;
+    }
+
+    static replaceStringInAttributeValueOnQueried(
+        root: Element,
+        query: string,
+        attrArray: string[],
+        replaceMap: Map<string, string>
+    ): boolean {
+        let updated = false;
+        const queried = [...Array.from(root.querySelectorAll(query)), root];
+
+        queried.forEach((el) => {
+            attrArray.forEach((attr) => {
+                if (el.hasAttribute(attr)) {
+                    const currentAttrValue = el.getAttribute(attr);
+
+                    if (!currentAttrValue) return;
+
+                    [...replaceMap.entries()].some(([oldStr, newStr]) => {
+                        if (
+                            currentAttrValue.length >= oldStr.length &&
+                            currentAttrValue.indexOf(oldStr) ==
+                                currentAttrValue.length - oldStr.length
+                        ) {
+                            el.setAttribute(attr, currentAttrValue.replace(oldStr, newStr));
+                            updated = true;
+                            return true;
+                        }
+                        return false;
+                    });
+                }
+            });
+        });
+
+        return updated;
+    }
+
+    static updateElement(el: Element, updateOptions: IUpdateTag) {
+        const exists = this.futureEntryAlreadyExist(el, updateOptions);
+        if (exists) {
+            console.log('Ignored updating entry already available: ', exists.outerHTML);
+            return;
+        }
+
+        const { comment, attrs, content } = updateOptions;
+
+        if (comment) {
+            const { commentNode } = this.getElementComment(el);
+            if (commentNode) {
+                commentNode.textContent = comment;
+            }
+        }
+
+        if (attrs) {
+            for (const attr in attrs) {
+                const value = attrs[attr];
+
+                if (value != undefined) {
+                    el.setAttribute(attr, `${value}`);
+                } else {
+                    el.removeAttribute(attr);
+                }
+            }
+        }
+
+        if (content != undefined) {
+            el.innerHTML = `${content}`;
+        }
+
+        return true;
+    }
+
+    static elementToOptions(el: Element): ITag {
+        return {
+            attrs: this.getAllElementAttributes(el),
+            tagName: el.tagName,
+        };
+    }
+
+    static getAllElementAttributes(el: Element): Record<string, string> {
+        return el
+            .getAttributeNames()
+            .reduce(
+                (acc, attr) => ({ ...acc, [attr]: el.getAttribute(attr) || '' }),
+                {} as Record<string, string>
+            );
+    }
+
+    static futureEntryAlreadyExist(el: Element, updateOptions: IUpdateTag) {
+        const currentElOptions = this.elementToOptions(el);
+
+        if (!el.parentElement) {
+            console.log('Checked el has no parent');
+            process.exit();
+        }
+
+        return this.alreadyHasEntry(el.parentElement, {
+            ...currentElOptions,
+            ...updateOptions,
+            attrs: { ...currentElOptions.attrs, ...updateOptions.attrs },
+        });
+    }
+
+    static alreadyHasEntry(
+        containerElement: Element,
+        { attrs, tagName }: Pick<INewTag, 'attrs' | 'tagName'>
+    ) {
+        const qAttrs = attrs
+            ? Object.entries(attrs)
+                  .map(([a, v]) => `[${a}="${v}"]`)
+                  .join('')
+            : '';
+
+        return containerElement.querySelector(tagName + qAttrs);
+    }
+
+    static replaceContentTextOnQueried(
+        root: Element,
+        query: string,
+        replacePairs: Array<[string, string]>
+    ) {
+        let updated = false;
+        let queried = Array.from(root.querySelectorAll(query));
+
+        if (queried.length == 0) queried = [...Array.from(root.querySelectorAll(query)), root];
+
+        queried.forEach((el) => {
+            replacePairs.forEach(([oldStr, newStr]) => {
+                if (el.innerHTML == oldStr) {
+                    el.innerHTML = newStr;
+                    updated = true;
+                }
+            });
+        });
+
+        return updated;
+    }
+
+    static XMLDocToString(doc: XMLDocument) {
+        let str = '';
+
+        doc.childNodes.forEach((node) => {
+            switch (node.nodeType) {
+                case 8: // comment
+                    str += `<!--${node.nodeValue}-->\n`;
+                    break;
+
+                case 3: // text
+                    str += node.textContent;
+                    break;
+
+                case 1: // element
+                    str += (node as Element).outerHTML;
+                    break;
+
+                default:
+                    console.log('Unhandled node type: ' + node.nodeType);
+                    break;
+            }
+        });
+
+        return str;
+    }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts b/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts
new file mode 100644
index 0000000..359e3ab
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts
@@ -0,0 +1,112 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import { exec } from 'child_process';
+import { parse } from 'csv-parse';
+import { promises as fs } from 'fs';
+import jsdom from 'jsdom';
+
+const DOMParser = new jsdom.JSDOM('').window.DOMParser as typeof window.DOMParser;
+
+type TFileList = string[];
+
+export type TCSVRecord = Array<string | boolean | number>;
+
+class _FileIO {
+    public parser = new DOMParser();
+    public saved: string[] = [];
+
+    public loadXML = async (path: string): Promise<XMLDocument> => {
+        try {
+            const src = await this.loadFileAsText(path);
+            return this.parser.parseFromString(src, 'text/xml') as XMLDocument;
+        } catch (error) {
+            console.log(`Failed to parse XML file '${path}'.`, error);
+            process.exit();
+        }
+    };
+
+    public loadFileAsText = async (path: string): Promise<string> => {
+        try {
+            return await fs.readFile(path, { encoding: 'utf8' });
+        } catch (error) {
+            console.log(`Failed to read file '${path}'.`, error);
+            process.exit();
+        }
+    };
+
+    public saveFile = async (data: string, path: string) => {
+        try {
+            await fs.writeFile(path, data, { encoding: 'utf8' });
+            this.saved.push(path);
+        } catch (error) {
+            console.log(error);
+            console.log(`Failed to write file '${path}'.`);
+            process.exit();
+        }
+    };
+
+    public loadFileList = async (path: string): Promise<TFileList> => {
+        const src = await this.loadFileAsText(path);
+
+        try {
+            return JSON.parse(src) as TFileList;
+        } catch (error) {
+            console.log(error);
+            console.log(`Failed to parse JSON file '${path}'.`);
+            process.exit();
+        }
+    };
+
+    public loadCSV = (path: string): Promise<Array<TCSVRecord>> => {
+        return new Promise((resolve, reject) => {
+            this.loadFileAsText(path).then((src) => {
+                parse(
+                    src,
+                    {
+                        delimiter: '	',
+                    },
+                    (err, records) => {
+                        if (err) {
+                            reject(err);
+                            return;
+                        }
+
+                        resolve(records);
+                    }
+                );
+            });
+        });
+    };
+
+    formatSaved = () => {
+        const cmd = `idea format ${this.saved.join(' ')}`;
+
+        exec(cmd, (error, out, stderr) => {
+            if (error) {
+                console.log(error.message);
+                return;
+            }
+
+            if (stderr) {
+                console.log(stderr);
+                return;
+            }
+
+            console.log(out);
+        });
+    };
+}
+
+export const FileIO = new _FileIO();
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts b/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts
new file mode 100644
index 0000000..8d50644
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts
@@ -0,0 +1,70 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import { FileIO, TCSVRecord } from './FileIO';
+import ProcessArgs from './processArgs';
+
+interface IInputMigItem {
+    migrationToken: string;
+    materialToken: string;
+    newDefaultValue?: string;
+    newComment?: string;
+}
+
+interface IAditionalKeys {
+    step: ('update' | 'duplicate' | 'add' | 'ignore')[];
+    isHidden: boolean;
+    replaceToken: string;
+}
+
+export type IMigItem = Omit<IInputMigItem, 'materialToken' | 'migrationToken'> & IAditionalKeys;
+
+export type IMigrationMap = Map<string, IMigItem>;
+
+function isMigrationRecord(record: TCSVRecord): record is string[] {
+    return !record.some((value) => typeof value != 'string') || record.length != 5;
+}
+
+export const loadMIgrationList = async function (): Promise<IMigrationMap> {
+    const out: IMigrationMap = new Map();
+    const csv = await FileIO.loadCSV('resources/migrationList.csv');
+
+    csv.forEach((record, i) => {
+        if (i == 0) return; // header
+
+        if (typeof record[0] != 'string') return;
+
+        if (!isMigrationRecord(record)) {
+            console.log(`Failed to validade CSV record as string[5].`, record);
+            process.exit();
+        }
+
+        const [originalToken, materialToken, newDefaultValue, newComment, migrationToken] = record;
+
+        if (out.has(originalToken)) {
+            console.log('Duplicated entry on Migration CSV file: ', originalToken);
+            return;
+        }
+
+        out.set(originalToken, {
+            replaceToken: ProcessArgs.isDebug ? migrationToken : materialToken,
+            ...(!!newDefaultValue && { newDefaultValue }),
+            ...(!!newComment && { newComment }),
+            step: [],
+            isHidden: false,
+        });
+    });
+
+    return new Map([...out].sort((a, b) => b[0].length - a[0].length));
+};
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts b/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts
new file mode 100644
index 0000000..be0e232
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts
@@ -0,0 +1,21 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+const myArgs = process.argv.slice(2);
+
+const ProcessArgs = {
+    isDebug: myArgs.includes('debug'),
+};
+
+export default ProcessArgs;
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts b/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts
new file mode 100644
index 0000000..368d4cb
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts
@@ -0,0 +1,102 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import DOM, { INewTag, IUpdateTag } from './DOMFuncs';
+import { FileIO } from './FileIO';
+import { IMigItem, IMigrationMap } from './migrationList';
+
+export type TResultExistingEval = ['update' | 'duplicate', IUpdateTag] | void;
+export type TResultMissingEval = INewTag | void;
+
+interface IProcessXML {
+    attr?: string;
+    containerQuery?: string;
+    evalExistingEntry?: TEvalExistingEntry;
+    evalMissingEntry?: TEvalMissingEntry;
+    hidable?: boolean;
+    path: string;
+    step: number;
+    tagName: string;
+}
+
+export type TEvalExistingEntry = (
+    attrname: string,
+    migItem: IMigItem,
+    qItem: Element
+) => TResultExistingEval;
+
+export type TEvalMissingEntry = (originalToken: string, migItem: IMigItem) => TResultMissingEval;
+
+export async function processQueriedEntries(
+    migrationMap: IMigrationMap,
+    {
+        attr = 'name',
+        containerQuery = '*',
+        evalExistingEntry,
+        path,
+        step,
+        tagName,
+        evalMissingEntry,
+    }: IProcessXML
+) {
+    const doc = await FileIO.loadXML(path);
+
+    const containerElement =
+        (containerQuery && doc.querySelector(containerQuery)) || doc.documentElement;
+
+    migrationMap.forEach((migItem, originalToken) => {
+        migItem.step[step] = 'ignore';
+
+        const queryTiems = containerElement.querySelectorAll(
+            `${tagName}[${attr}="${originalToken}"]`
+        );
+
+        if (evalMissingEntry) {
+            const addinOptions = evalMissingEntry(originalToken, migItem);
+
+            if (queryTiems.length == 0 && containerElement && addinOptions) {
+                DOM.addEntry(containerElement, addinOptions);
+                migItem.step[step] = 'add';
+                return;
+            }
+        }
+
+        if (evalExistingEntry)
+            queryTiems.forEach((qEl) => {
+                const attrName = qEl.getAttribute(attr);
+                const migItem = migrationMap.get(attrName || '');
+
+                if (!attrName || !migItem) return;
+
+                const updateOptions = evalExistingEntry(attrName, migItem, qEl);
+
+                if (!updateOptions) return;
+
+                const [processType, processOptions] = updateOptions;
+
+                switch (processType) {
+                    case 'update':
+                        if (DOM.updateElement(qEl, processOptions)) migItem.step[step] = 'update';
+                        break;
+
+                    case 'duplicate':
+                        if (DOM.duplicateEntryWithChange(qEl, processOptions))
+                            migItem.step[step] = 'duplicate';
+                        break;
+                }
+            });
+    });
+
+    await FileIO.saveFile(doc.documentElement.outerHTML, path);
+}
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts b/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts
new file mode 100644
index 0000000..2c6f632
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts
@@ -0,0 +1,21 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+if (!process?.env?.ANDROID_BUILD_TOP) {
+    console.log(
+        "Error: Couldn't find 'ANDROID_BUILD_TOP' environment variable. Make sure to run 'lunch' in this terminal"
+    );
+}
+
+export const repoPath = process?.env?.ANDROID_BUILD_TOP;
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts b/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts
new file mode 100644
index 0000000..6679c5a
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts
@@ -0,0 +1,27 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+export function groupReplace(src: string, replaceMap: Map<string, string>, pattern: string) {
+    const fullPattern = pattern.replace('#group#', [...replaceMap.keys()].join('|'));
+
+    const regEx = new RegExp(fullPattern, 'g');
+
+    ''.replace;
+
+    return src.replace(regEx, (...args) => {
+        //match, ...matches, offset, string, groups
+        const [match, key] = args as string[];
+        return match.replace(key, replaceMap.get(key) || '');
+    });
+}
diff --git a/packages/SystemUI/scripts/token_alignment/index.ts b/packages/SystemUI/scripts/token_alignment/index.ts
new file mode 100644
index 0000000..1b15e48
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/index.ts
@@ -0,0 +1,240 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import DOM from './helpers/DOMFuncs';
+import { FileIO } from './helpers/FileIO';
+import { loadMIgrationList } from './helpers/migrationList';
+import { processQueriedEntries, TEvalExistingEntry } from './helpers/processXML';
+import { repoPath } from './helpers/rootPath';
+import { groupReplace } from './helpers/textFuncs';
+
+async function init() {
+    const migrationMap = await loadMIgrationList();
+    const basePath = `${repoPath}/../tm-qpr-dev/frameworks/base/core/res/res/values/`;
+
+    await processQueriedEntries(migrationMap, {
+        containerQuery: 'declare-styleable[name="Theme"]',
+        hidable: true,
+        path: `${basePath}attrs.xml`,
+        step: 0,
+        tagName: 'attr',
+        evalExistingEntry: (_attrValue, migItem, qItem) => {
+            const { hidden, textContent: currentComment } = DOM.getElementComment(qItem);
+
+            if (hidden) migItem.isHidden = hidden;
+
+            const { newComment } = migItem;
+            return [
+                hidden ? 'update' : 'duplicate',
+                {
+                    attrs: { name: migItem.replaceToken },
+                    ...(newComment
+                        ? { comment: `${newComment} @hide ` }
+                        : currentComment
+                        ? { comment: hidden ? currentComment : `${currentComment} @hide ` }
+                        : {}),
+                },
+            ];
+        },
+        evalMissingEntry: (_originalToken, { replaceToken, newComment }) => {
+            return {
+                tagName: 'attr',
+                attrs: {
+                    name: replaceToken,
+                    format: 'color',
+                },
+                comment: `${newComment} @hide `,
+            };
+        },
+    });
+
+    // only update all existing entries
+    await processQueriedEntries(migrationMap, {
+        tagName: 'item',
+        path: `${basePath}themes_device_defaults.xml`,
+        containerQuery: 'resources',
+        step: 2,
+        evalExistingEntry: (_attrValue, { isHidden, replaceToken, step }, _qItem) => {
+            if (step[0] != 'ignore')
+                return [
+                    isHidden ? 'update' : 'duplicate',
+                    {
+                        attrs: { name: replaceToken },
+                    },
+                ];
+        },
+    });
+
+    // add missing entries on specific container
+    await processQueriedEntries(migrationMap, {
+        tagName: 'item',
+        path: `${basePath}themes_device_defaults.xml`,
+        containerQuery: 'resources style[parent="Theme.Material"]',
+        step: 3,
+        evalMissingEntry: (originalToken, { newDefaultValue, replaceToken }) => {
+            return {
+                tagName: 'item',
+                content: newDefaultValue,
+                attrs: {
+                    name: replaceToken,
+                },
+            };
+        },
+    });
+
+    const evalExistingEntry: TEvalExistingEntry = (_attrValue, { replaceToken, step }, _qItem) => {
+        if (step[0] == 'update')
+            return [
+                'update',
+                {
+                    attrs: { name: replaceToken },
+                },
+            ];
+    };
+
+    await processQueriedEntries(migrationMap, {
+        tagName: 'item',
+        containerQuery: 'resources',
+        path: `${basePath}../values-night/themes_device_defaults.xml`,
+        step: 4,
+        evalExistingEntry,
+    });
+
+    await processQueriedEntries(migrationMap, {
+        tagName: 'java-symbol',
+        path: `${basePath}symbols.xml`,
+        containerQuery: 'resources',
+        step: 5,
+        evalExistingEntry,
+    });
+
+    // update attributes on tracked XML files
+    {
+        const searchAttrs = [
+            'android:color',
+            'android:indeterminateTint',
+            'app:tint',
+            'app:backgroundTint',
+            'android:background',
+            'android:tint',
+            'android:drawableTint',
+            'android:textColor',
+            'android:fillColor',
+            'android:startColor',
+            'android:endColor',
+            'name',
+            'ns1:color',
+        ];
+
+        const filtered = new Map(
+            [...migrationMap]
+                .filter(([_originalToken, { step }]) => step[0] == 'update')
+                .map(([originalToken, { replaceToken }]) => [originalToken, replaceToken])
+        );
+
+        const query =
+            searchAttrs.map((str) => `*[${str}]`).join(',') +
+            [...filtered.keys()].map((originalToken) => `item[name*="${originalToken}"]`).join(',');
+
+        const trackedFiles = await FileIO.loadFileList(
+            `${__dirname}/resources/whitelist/xmls1.json`
+        );
+
+        const promises = trackedFiles.map(async (locaFilePath) => {
+            const filePath = `${repoPath}/${locaFilePath}`;
+
+            const doc = await FileIO.loadXML(filePath);
+            const docUpdated = DOM.replaceStringInAttributeValueOnQueried(
+                doc.documentElement,
+                query,
+                searchAttrs,
+                filtered
+            );
+            if (docUpdated) {
+                await FileIO.saveFile(DOM.XMLDocToString(doc), filePath);
+            } else {
+                console.warn(`Failed to update tracked file: '${locaFilePath}'`);
+            }
+        });
+        await Promise.all(promises);
+    }
+
+    // updates tag content on tracked files
+    {
+        const searchPrefixes = ['?android:attr/', '?androidprv:attr/'];
+        const filtered = searchPrefixes
+            .reduce<Array<[string, string]>>((acc, prefix) => {
+                return [
+                    ...acc,
+                    ...[...migrationMap.entries()]
+                        .filter(([_originalToken, { step }]) => step[0] == 'update')
+                        .map(
+                            ([originalToken, { replaceToken }]) =>
+                                [`${prefix}${originalToken}`, `${prefix}${replaceToken}`] as [
+                                    string,
+                                    string
+                                ]
+                        ),
+                ];
+            }, [])
+            .sort((a, b) => b[0].length - a[0].length);
+
+        const trackedFiles = await FileIO.loadFileList(
+            `${__dirname}/resources/whitelist/xmls2.json`
+        );
+
+        const promises = trackedFiles.map(async (locaFilePath) => {
+            const filePath = `${repoPath}/${locaFilePath}`;
+            const doc = await FileIO.loadXML(filePath);
+            const docUpdated = DOM.replaceContentTextOnQueried(
+                doc.documentElement,
+                'item, color',
+                filtered
+            );
+            if (docUpdated) {
+                await FileIO.saveFile(DOM.XMLDocToString(doc), filePath);
+            } else {
+                console.warn(`Failed to update tracked file: '${locaFilePath}'`);
+            }
+        });
+        await Promise.all(promises);
+    }
+
+    // replace imports on Java / Kotlin
+    {
+        const replaceMap = new Map(
+            [...migrationMap.entries()]
+                .filter(([_originalToken, { step }]) => step[0] == 'update')
+                .map(
+                    ([originalToken, { replaceToken }]) =>
+                        [originalToken, replaceToken] as [string, string]
+                )
+                .sort((a, b) => b[0].length - a[0].length)
+        );
+
+        const trackedFiles = await FileIO.loadFileList(
+            `${__dirname}/resources/whitelist/java.json`
+        );
+
+        const promises = trackedFiles.map(async (locaFilePath) => {
+            const filePath = `${repoPath}/${locaFilePath}`;
+            const fileContent = await FileIO.loadFileAsText(filePath);
+            const str = groupReplace(fileContent, replaceMap, 'R.attr.(#group#)(?![a-zA-Z])');
+            await FileIO.saveFile(str, filePath);
+        });
+        await Promise.all(promises);
+    }
+}
+
+init();
diff --git a/packages/SystemUI/scripts/token_alignment/package-lock.json b/packages/SystemUI/scripts/token_alignment/package-lock.json
new file mode 100644
index 0000000..da9edb3
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/package-lock.json
@@ -0,0 +1,3356 @@
+{
+    "name": "token_alignment",
+    "lockfileVersion": 3,
+    "requires": true,
+    "packages": {
+        "": {
+            "dependencies": {
+                "csv-parse": "^5.3.3",
+                "high5": "^1.0.0",
+                "jsdom": "^20.0.3"
+            },
+            "devDependencies": {
+                "@types/jsdom": "^20.0.1",
+                "@types/node": "^18.11.18",
+                "@typescript-eslint/eslint-plugin": "^5.48.0",
+                "eslint-config-prettier": "^8.6.0",
+                "eslint-plugin-import": "^2.26.0",
+                "eslint-plugin-prettier": "^4.2.1",
+                "eslint-plugin-simple-import-sort": "^8.0.0",
+                "ts-node": "^10.9.1",
+                "typescript": "^4.9.4"
+            }
+        },
+        "node_modules/@cspotcode/source-map-support": {
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/trace-mapping": "0.3.9"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@eslint/eslintrc": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
+            "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "ajv": "^6.12.4",
+                "debug": "^4.3.2",
+                "espree": "^9.4.0",
+                "globals": "^13.19.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.2.1",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
+                "strip-json-comments": "^3.1.1"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/@humanwhocodes/config-array": {
+            "version": "0.11.8",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+            "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "@humanwhocodes/object-schema": "^1.2.1",
+                "debug": "^4.1.1",
+                "minimatch": "^3.0.5"
+            },
+            "engines": {
+                "node": ">=10.10.0"
+            }
+        },
+        "node_modules/@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=12.22"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/nzakas"
+            }
+        },
+        "node_modules/@humanwhocodes/object-schema": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+            "dev": true,
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+            "dev": true
+        },
+        "node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
+            }
+        },
+        "node_modules/@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "dev": true,
+            "dependencies": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+            "dev": true,
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+            "dev": true,
+            "dependencies": {
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@tootallnate/once": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+            "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@tsconfig/node10": {
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node12": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node14": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node16": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
+            "dev": true
+        },
+        "node_modules/@types/jsdom": {
+            "version": "20.0.1",
+            "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
+            "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+            "dev": true,
+            "dependencies": {
+                "@types/node": "*",
+                "@types/tough-cookie": "*",
+                "parse5": "^7.0.0"
+            }
+        },
+        "node_modules/@types/json-schema": {
+            "version": "7.0.11",
+            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+            "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+            "dev": true
+        },
+        "node_modules/@types/json5": {
+            "version": "0.0.29",
+            "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+            "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+            "dev": true
+        },
+        "node_modules/@types/node": {
+            "version": "18.11.18",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
+            "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
+            "dev": true
+        },
+        "node_modules/@types/semver": {
+            "version": "7.3.13",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
+            "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
+            "dev": true
+        },
+        "node_modules/@types/tough-cookie": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
+            "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
+            "dev": true
+        },
+        "node_modules/@typescript-eslint/eslint-plugin": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz",
+            "integrity": "sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "5.48.0",
+                "@typescript-eslint/type-utils": "5.48.0",
+                "@typescript-eslint/utils": "5.48.0",
+                "debug": "^4.3.4",
+                "ignore": "^5.2.0",
+                "natural-compare-lite": "^1.4.0",
+                "regexpp": "^3.2.0",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "@typescript-eslint/parser": "^5.0.0",
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/parser": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.0.tgz",
+            "integrity": "sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "5.48.0",
+                "@typescript-eslint/types": "5.48.0",
+                "@typescript-eslint/typescript-estree": "5.48.0",
+                "debug": "^4.3.4"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/scope-manager": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.0.tgz",
+            "integrity": "sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/types": "5.48.0",
+                "@typescript-eslint/visitor-keys": "5.48.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/@typescript-eslint/type-utils": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.0.tgz",
+            "integrity": "sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/typescript-estree": "5.48.0",
+                "@typescript-eslint/utils": "5.48.0",
+                "debug": "^4.3.4",
+                "tsutils": "^3.21.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "*"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/types": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.0.tgz",
+            "integrity": "sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw==",
+            "dev": true,
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.0.tgz",
+            "integrity": "sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/types": "5.48.0",
+                "@typescript-eslint/visitor-keys": "5.48.0",
+                "debug": "^4.3.4",
+                "globby": "^11.1.0",
+                "is-glob": "^4.0.3",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/utils": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.0.tgz",
+            "integrity": "sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ==",
+            "dev": true,
+            "dependencies": {
+                "@types/json-schema": "^7.0.9",
+                "@types/semver": "^7.3.12",
+                "@typescript-eslint/scope-manager": "5.48.0",
+                "@typescript-eslint/types": "5.48.0",
+                "@typescript-eslint/typescript-estree": "5.48.0",
+                "eslint-scope": "^5.1.1",
+                "eslint-utils": "^3.0.0",
+                "semver": "^7.3.7"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            }
+        },
+        "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+            "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+            "dev": true,
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^4.1.1"
+            },
+            "engines": {
+                "node": ">=8.0.0"
+            }
+        },
+        "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+            "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/@typescript-eslint/visitor-keys": {
+            "version": "5.48.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.0.tgz",
+            "integrity": "sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q==",
+            "dev": true,
+            "dependencies": {
+                "@typescript-eslint/types": "5.48.0",
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/abab": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+            "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="
+        },
+        "node_modules/acorn": {
+            "version": "8.8.1",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
+            "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
+            "bin": {
+                "acorn": "bin/acorn"
+            },
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/acorn-globals": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+            "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
+            "dependencies": {
+                "acorn": "^8.1.0",
+                "acorn-walk": "^8.0.2"
+            }
+        },
+        "node_modules/acorn-jsx": {
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+            "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+            "dev": true,
+            "peer": true,
+            "peerDependencies": {
+                "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            }
+        },
+        "node_modules/acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "dependencies": {
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
+        },
+        "node_modules/ajv": {
+            "version": "6.12.6",
+            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/epoberezkin"
+            }
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+            "dev": true
+        },
+        "node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/array-includes": {
+            "version": "3.1.6",
+            "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+            "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4",
+                "get-intrinsic": "^1.1.3",
+                "is-string": "^1.0.7"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/array-union": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+            "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/array.prototype.flat": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+            "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4",
+                "es-shim-unscopables": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/asynckit": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+            "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+        },
+        "node_modules/available-typed-arrays": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+            "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "dev": true
+        },
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dev": true,
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "node_modules/braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "dependencies": {
+                "fill-range": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/call-bind": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+            "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+            "dev": true,
+            "dependencies": {
+                "function-bind": "^1.1.1",
+                "get-intrinsic": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/callsites": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/combined-stream": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+            "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+            "dependencies": {
+                "delayed-stream": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+            "dev": true
+        },
+        "node_modules/create-require": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+            "dev": true
+        },
+        "node_modules/cross-spawn": {
+            "version": "7.0.3",
+            "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+            "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "path-key": "^3.1.0",
+                "shebang-command": "^2.0.0",
+                "which": "^2.0.1"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/cssom": {
+            "version": "0.5.0",
+            "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+            "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="
+        },
+        "node_modules/cssstyle": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+            "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+            "dependencies": {
+                "cssom": "~0.3.6"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cssstyle/node_modules/cssom": {
+            "version": "0.3.8",
+            "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+            "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
+        },
+        "node_modules/csv-parse": {
+            "version": "5.3.3",
+            "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.3.tgz",
+            "integrity": "sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw=="
+        },
+        "node_modules/data-urls": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+            "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+            "dependencies": {
+                "abab": "^2.0.6",
+                "whatwg-mimetype": "^3.0.0",
+                "whatwg-url": "^11.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/debug": {
+            "version": "4.3.4",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+            "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+            "dependencies": {
+                "ms": "2.1.2"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/decimal.js": {
+            "version": "10.4.3",
+            "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+            "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+        },
+        "node_modules/deep-is": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+        },
+        "node_modules/define-properties": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+            "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+            "dev": true,
+            "dependencies": {
+                "has-property-descriptors": "^1.0.0",
+                "object-keys": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/delayed-stream": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+            "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/diff": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.3.1"
+            }
+        },
+        "node_modules/dir-glob": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+            "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+            "dev": true,
+            "dependencies": {
+                "path-type": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/doctrine": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+            "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "esutils": "^2.0.2"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/domexception": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+            "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+            "dependencies": {
+                "webidl-conversions": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/entities": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
+            "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
+            "engines": {
+                "node": ">=0.12"
+            },
+            "funding": {
+                "url": "https://github.com/fb55/entities?sponsor=1"
+            }
+        },
+        "node_modules/es-abstract": {
+            "version": "1.21.0",
+            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.0.tgz",
+            "integrity": "sha512-GUGtW7eXQay0c+PRq0sGIKSdaBorfVqsCMhGHo4elP7YVqZu9nCZS4UkK4gv71gOWNMra/PaSKD3ao1oWExO0g==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "es-set-tostringtag": "^2.0.0",
+                "es-to-primitive": "^1.2.1",
+                "function-bind": "^1.1.1",
+                "function.prototype.name": "^1.1.5",
+                "get-intrinsic": "^1.1.3",
+                "get-symbol-description": "^1.0.0",
+                "globalthis": "^1.0.3",
+                "gopd": "^1.0.1",
+                "has": "^1.0.3",
+                "has-property-descriptors": "^1.0.0",
+                "has-proto": "^1.0.1",
+                "has-symbols": "^1.0.3",
+                "internal-slot": "^1.0.4",
+                "is-array-buffer": "^3.0.0",
+                "is-callable": "^1.2.7",
+                "is-negative-zero": "^2.0.2",
+                "is-regex": "^1.1.4",
+                "is-shared-array-buffer": "^1.0.2",
+                "is-string": "^1.0.7",
+                "is-typed-array": "^1.1.10",
+                "is-weakref": "^1.0.2",
+                "object-inspect": "^1.12.2",
+                "object-keys": "^1.1.1",
+                "object.assign": "^4.1.4",
+                "regexp.prototype.flags": "^1.4.3",
+                "safe-regex-test": "^1.0.0",
+                "string.prototype.trimend": "^1.0.6",
+                "string.prototype.trimstart": "^1.0.6",
+                "typed-array-length": "^1.0.4",
+                "unbox-primitive": "^1.0.2",
+                "which-typed-array": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/es-set-tostringtag": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+            "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+            "dev": true,
+            "dependencies": {
+                "get-intrinsic": "^1.1.3",
+                "has": "^1.0.3",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-shim-unscopables": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+            "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+            "dev": true,
+            "dependencies": {
+                "has": "^1.0.3"
+            }
+        },
+        "node_modules/es-to-primitive": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+            "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+            "dev": true,
+            "dependencies": {
+                "is-callable": "^1.1.4",
+                "is-date-object": "^1.0.1",
+                "is-symbol": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/escape-string-regexp": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+            "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/escodegen": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+            "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
+            "dependencies": {
+                "esprima": "^4.0.1",
+                "estraverse": "^5.2.0",
+                "esutils": "^2.0.2",
+                "optionator": "^0.8.1"
+            },
+            "bin": {
+                "escodegen": "bin/escodegen.js",
+                "esgenerate": "bin/esgenerate.js"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "optionalDependencies": {
+                "source-map": "~0.6.1"
+            }
+        },
+        "node_modules/eslint": {
+            "version": "8.31.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz",
+            "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "@eslint/eslintrc": "^1.4.1",
+                "@humanwhocodes/config-array": "^0.11.8",
+                "@humanwhocodes/module-importer": "^1.0.1",
+                "@nodelib/fs.walk": "^1.2.8",
+                "ajv": "^6.10.0",
+                "chalk": "^4.0.0",
+                "cross-spawn": "^7.0.2",
+                "debug": "^4.3.2",
+                "doctrine": "^3.0.0",
+                "escape-string-regexp": "^4.0.0",
+                "eslint-scope": "^7.1.1",
+                "eslint-utils": "^3.0.0",
+                "eslint-visitor-keys": "^3.3.0",
+                "espree": "^9.4.0",
+                "esquery": "^1.4.0",
+                "esutils": "^2.0.2",
+                "fast-deep-equal": "^3.1.3",
+                "file-entry-cache": "^6.0.1",
+                "find-up": "^5.0.0",
+                "glob-parent": "^6.0.2",
+                "globals": "^13.19.0",
+                "grapheme-splitter": "^1.0.4",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.0.0",
+                "imurmurhash": "^0.1.4",
+                "is-glob": "^4.0.0",
+                "is-path-inside": "^3.0.3",
+                "js-sdsl": "^4.1.4",
+                "js-yaml": "^4.1.0",
+                "json-stable-stringify-without-jsonify": "^1.0.1",
+                "levn": "^0.4.1",
+                "lodash.merge": "^4.6.2",
+                "minimatch": "^3.1.2",
+                "natural-compare": "^1.4.0",
+                "optionator": "^0.9.1",
+                "regexpp": "^3.2.0",
+                "strip-ansi": "^6.0.1",
+                "strip-json-comments": "^3.1.0",
+                "text-table": "^0.2.0"
+            },
+            "bin": {
+                "eslint": "bin/eslint.js"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/eslint-config-prettier": {
+            "version": "8.6.0",
+            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz",
+            "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==",
+            "dev": true,
+            "bin": {
+                "eslint-config-prettier": "bin/cli.js"
+            },
+            "peerDependencies": {
+                "eslint": ">=7.0.0"
+            }
+        },
+        "node_modules/eslint-import-resolver-node": {
+            "version": "0.3.6",
+            "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
+            "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+            "dev": true,
+            "dependencies": {
+                "debug": "^3.2.7",
+                "resolve": "^1.20.0"
+            }
+        },
+        "node_modules/eslint-import-resolver-node/node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dev": true,
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/eslint-module-utils": {
+            "version": "2.7.4",
+            "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz",
+            "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==",
+            "dev": true,
+            "dependencies": {
+                "debug": "^3.2.7"
+            },
+            "engines": {
+                "node": ">=4"
+            },
+            "peerDependenciesMeta": {
+                "eslint": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/eslint-module-utils/node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dev": true,
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/eslint-plugin-import": {
+            "version": "2.26.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
+            "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+            "dev": true,
+            "dependencies": {
+                "array-includes": "^3.1.4",
+                "array.prototype.flat": "^1.2.5",
+                "debug": "^2.6.9",
+                "doctrine": "^2.1.0",
+                "eslint-import-resolver-node": "^0.3.6",
+                "eslint-module-utils": "^2.7.3",
+                "has": "^1.0.3",
+                "is-core-module": "^2.8.1",
+                "is-glob": "^4.0.3",
+                "minimatch": "^3.1.2",
+                "object.values": "^1.1.5",
+                "resolve": "^1.22.0",
+                "tsconfig-paths": "^3.14.1"
+            },
+            "engines": {
+                "node": ">=4"
+            },
+            "peerDependencies": {
+                "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+            }
+        },
+        "node_modules/eslint-plugin-import/node_modules/debug": {
+            "version": "2.6.9",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+            "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+            "dev": true,
+            "dependencies": {
+                "ms": "2.0.0"
+            }
+        },
+        "node_modules/eslint-plugin-import/node_modules/doctrine": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+            "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+            "dev": true,
+            "dependencies": {
+                "esutils": "^2.0.2"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/eslint-plugin-import/node_modules/ms": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+            "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+            "dev": true
+        },
+        "node_modules/eslint-plugin-prettier": {
+            "version": "4.2.1",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
+            "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+            "dev": true,
+            "dependencies": {
+                "prettier-linter-helpers": "^1.0.0"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            },
+            "peerDependencies": {
+                "eslint": ">=7.28.0",
+                "prettier": ">=2.0.0"
+            },
+            "peerDependenciesMeta": {
+                "eslint-config-prettier": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/eslint-plugin-simple-import-sort": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz",
+            "integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==",
+            "dev": true,
+            "peerDependencies": {
+                "eslint": ">=5.0.0"
+            }
+        },
+        "node_modules/eslint-scope": {
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+            "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
+        "node_modules/eslint-utils": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+            "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+            "dev": true,
+            "dependencies": {
+                "eslint-visitor-keys": "^2.0.0"
+            },
+            "engines": {
+                "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/mysticatea"
+            },
+            "peerDependencies": {
+                "eslint": ">=5"
+            }
+        },
+        "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+            "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/eslint-visitor-keys": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+            "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+            "dev": true,
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
+        "node_modules/eslint/node_modules/levn": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+            "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "prelude-ls": "^1.2.1",
+                "type-check": "~0.4.0"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/eslint/node_modules/optionator": {
+            "version": "0.9.1",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+            "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "deep-is": "^0.1.3",
+                "fast-levenshtein": "^2.0.6",
+                "levn": "^0.4.1",
+                "prelude-ls": "^1.2.1",
+                "type-check": "^0.4.0",
+                "word-wrap": "^1.2.3"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/eslint/node_modules/prelude-ls": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/eslint/node_modules/type-check": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "prelude-ls": "^1.2.1"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/espree": {
+            "version": "9.4.1",
+            "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
+            "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "acorn": "^8.8.0",
+                "acorn-jsx": "^5.3.2",
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/esprima": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+            "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+            "bin": {
+                "esparse": "bin/esparse.js",
+                "esvalidate": "bin/esvalidate.js"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/esquery": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+            "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "estraverse": "^5.1.0"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/esrecurse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+            "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+            "dev": true,
+            "dependencies": {
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/esutils": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/fast-diff": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+            "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+            "dev": true
+        },
+        "node_modules/fast-glob": {
+            "version": "3.2.12",
+            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+            "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+            "dev": true,
+            "dependencies": {
+                "@nodelib/fs.stat": "^2.0.2",
+                "@nodelib/fs.walk": "^1.2.3",
+                "glob-parent": "^5.1.2",
+                "merge2": "^1.3.0",
+                "micromatch": "^4.0.4"
+            },
+            "engines": {
+                "node": ">=8.6.0"
+            }
+        },
+        "node_modules/fast-glob/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/fast-json-stable-stringify": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/fast-levenshtein": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+        },
+        "node_modules/fastq": {
+            "version": "1.15.0",
+            "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+            "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+            "dev": true,
+            "dependencies": {
+                "reusify": "^1.0.4"
+            }
+        },
+        "node_modules/file-entry-cache": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+            "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "flat-cache": "^3.0.4"
+            },
+            "engines": {
+                "node": "^10.12.0 || >=12.0.0"
+            }
+        },
+        "node_modules/fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/find-up": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "locate-path": "^6.0.0",
+                "path-exists": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/flat-cache": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+            "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "flatted": "^3.1.0",
+                "rimraf": "^3.0.2"
+            },
+            "engines": {
+                "node": "^10.12.0 || >=12.0.0"
+            }
+        },
+        "node_modules/flatted": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+            "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/for-each": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+            "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+            "dev": true,
+            "dependencies": {
+                "is-callable": "^1.1.3"
+            }
+        },
+        "node_modules/form-data": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+            "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+            "dependencies": {
+                "asynckit": "^0.4.0",
+                "combined-stream": "^1.0.8",
+                "mime-types": "^2.1.12"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/function-bind": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+            "dev": true
+        },
+        "node_modules/function.prototype.name": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+            "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.3",
+                "es-abstract": "^1.19.0",
+                "functions-have-names": "^1.2.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/functions-have-names": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+            "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+            "dev": true,
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-intrinsic": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+            "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+            "dev": true,
+            "dependencies": {
+                "function-bind": "^1.1.1",
+                "has": "^1.0.3",
+                "has-symbols": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-symbol-description": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+            "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "get-intrinsic": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/glob": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.1.1",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/glob-parent": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "is-glob": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            }
+        },
+        "node_modules/globals": {
+            "version": "13.19.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
+            "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "type-fest": "^0.20.2"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/globalthis": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+            "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+            "dev": true,
+            "dependencies": {
+                "define-properties": "^1.1.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/globby": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+            "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+            "dev": true,
+            "dependencies": {
+                "array-union": "^2.1.0",
+                "dir-glob": "^3.0.1",
+                "fast-glob": "^3.2.9",
+                "ignore": "^5.2.0",
+                "merge2": "^1.4.1",
+                "slash": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/gopd": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+            "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+            "dev": true,
+            "dependencies": {
+                "get-intrinsic": "^1.1.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/grapheme-splitter": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/has": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+            "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+            "dev": true,
+            "dependencies": {
+                "function-bind": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4.0"
+            }
+        },
+        "node_modules/has-bigints": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+            "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+            "dev": true,
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/has-property-descriptors": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+            "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+            "dev": true,
+            "dependencies": {
+                "get-intrinsic": "^1.1.1"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+            "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-symbols": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+            "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-tostringtag": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+            "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+            "dev": true,
+            "dependencies": {
+                "has-symbols": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/high5": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/high5/-/high5-1.0.0.tgz",
+            "integrity": "sha512-xucW/5M1hd+p6bj530wtRSKwqUQrgiIgOWepi4Di9abkonZaxhTDf0zrqqraxfZSXBcFSuc1/WVGBIlqSe1Hdw==",
+            "dependencies": {
+                "entities": "1.0"
+            }
+        },
+        "node_modules/high5/node_modules/entities": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
+            "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ=="
+        },
+        "node_modules/html-encoding-sniffer": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+            "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+            "dependencies": {
+                "whatwg-encoding": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/http-proxy-agent": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+            "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+            "dependencies": {
+                "@tootallnate/once": "2",
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/https-proxy-agent": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "dependencies": {
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/iconv-lite": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+            "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+            "dependencies": {
+                "safer-buffer": ">= 2.1.2 < 3.0.0"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/ignore": {
+            "version": "5.2.4",
+            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+            "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+            "dev": true,
+            "engines": {
+                "node": ">= 4"
+            }
+        },
+        "node_modules/import-fresh": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+            "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "parent-module": "^1.0.0",
+                "resolve-from": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/imurmurhash": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+            "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=0.8.19"
+            }
+        },
+        "node_modules/inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "node_modules/inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/internal-slot": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz",
+            "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==",
+            "dev": true,
+            "dependencies": {
+                "get-intrinsic": "^1.1.3",
+                "has": "^1.0.3",
+                "side-channel": "^1.0.4"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/is-array-buffer": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz",
+            "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "get-intrinsic": "^1.1.3",
+                "is-typed-array": "^1.1.10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-bigint": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+            "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+            "dev": true,
+            "dependencies": {
+                "has-bigints": "^1.0.1"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-boolean-object": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+            "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-callable": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+            "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-core-module": {
+            "version": "2.11.0",
+            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+            "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+            "dev": true,
+            "dependencies": {
+                "has": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-date-object": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+            "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+            "dev": true,
+            "dependencies": {
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dev": true,
+            "dependencies": {
+                "is-extglob": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-negative-zero": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+            "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
+        "node_modules/is-number-object": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+            "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+            "dev": true,
+            "dependencies": {
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-path-inside": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-potential-custom-element-name": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+            "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
+        },
+        "node_modules/is-regex": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+            "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-shared-array-buffer": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+            "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-string": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+            "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+            "dev": true,
+            "dependencies": {
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-symbol": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+            "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+            "dev": true,
+            "dependencies": {
+                "has-symbols": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-typed-array": {
+            "version": "1.1.10",
+            "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+            "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+            "dev": true,
+            "dependencies": {
+                "available-typed-arrays": "^1.0.5",
+                "call-bind": "^1.0.2",
+                "for-each": "^0.3.3",
+                "gopd": "^1.0.1",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-weakref": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+            "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/isexe": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+            "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/js-sdsl": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz",
+            "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==",
+            "dev": true,
+            "peer": true,
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/js-sdsl"
+            }
+        },
+        "node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/jsdom": {
+            "version": "20.0.3",
+            "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
+            "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+            "dependencies": {
+                "abab": "^2.0.6",
+                "acorn": "^8.8.1",
+                "acorn-globals": "^7.0.0",
+                "cssom": "^0.5.0",
+                "cssstyle": "^2.3.0",
+                "data-urls": "^3.0.2",
+                "decimal.js": "^10.4.2",
+                "domexception": "^4.0.0",
+                "escodegen": "^2.0.0",
+                "form-data": "^4.0.0",
+                "html-encoding-sniffer": "^3.0.0",
+                "http-proxy-agent": "^5.0.0",
+                "https-proxy-agent": "^5.0.1",
+                "is-potential-custom-element-name": "^1.0.1",
+                "nwsapi": "^2.2.2",
+                "parse5": "^7.1.1",
+                "saxes": "^6.0.0",
+                "symbol-tree": "^3.2.4",
+                "tough-cookie": "^4.1.2",
+                "w3c-xmlserializer": "^4.0.0",
+                "webidl-conversions": "^7.0.0",
+                "whatwg-encoding": "^2.0.0",
+                "whatwg-mimetype": "^3.0.0",
+                "whatwg-url": "^11.0.0",
+                "ws": "^8.11.0",
+                "xml-name-validator": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "peerDependencies": {
+                "canvas": "^2.5.0"
+            },
+            "peerDependenciesMeta": {
+                "canvas": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/json-schema-traverse": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/json-stable-stringify-without-jsonify": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/json5": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+            "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+            "dev": true,
+            "dependencies": {
+                "minimist": "^1.2.0"
+            },
+            "bin": {
+                "json5": "lib/cli.js"
+            }
+        },
+        "node_modules/levn": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+            "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+            "dependencies": {
+                "prelude-ls": "~1.1.2",
+                "type-check": "~0.3.2"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/locate-path": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "p-locate": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/lodash.merge": {
+            "version": "4.6.2",
+            "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+            "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dev": true,
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+            "dev": true
+        },
+        "node_modules/merge2": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+            "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/micromatch": {
+            "version": "4.0.5",
+            "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+            "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+            "dev": true,
+            "dependencies": {
+                "braces": "^3.0.2",
+                "picomatch": "^2.3.1"
+            },
+            "engines": {
+                "node": ">=8.6"
+            }
+        },
+        "node_modules/mime-db": {
+            "version": "1.52.0",
+            "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+            "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime-types": {
+            "version": "2.1.35",
+            "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+            "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+            "dependencies": {
+                "mime-db": "1.52.0"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minimist": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+            "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+            "dev": true,
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "node_modules/natural-compare": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/natural-compare-lite": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+            "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+            "dev": true
+        },
+        "node_modules/nwsapi": {
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
+            "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw=="
+        },
+        "node_modules/object-inspect": {
+            "version": "1.12.2",
+            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+            "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+            "dev": true,
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/object-keys": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+            "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/object.assign": {
+            "version": "4.1.4",
+            "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+            "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "has-symbols": "^1.0.3",
+                "object-keys": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/object.values": {
+            "version": "1.1.6",
+            "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+            "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/optionator": {
+            "version": "0.8.3",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+            "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+            "dependencies": {
+                "deep-is": "~0.1.3",
+                "fast-levenshtein": "~2.0.6",
+                "levn": "~0.3.0",
+                "prelude-ls": "~1.1.2",
+                "type-check": "~0.3.2",
+                "word-wrap": "~1.2.3"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/p-limit": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "yocto-queue": "^0.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/p-locate": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "p-limit": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/parent-module": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "callsites": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/parse5": {
+            "version": "7.1.2",
+            "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+            "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+            "dependencies": {
+                "entities": "^4.4.0"
+            },
+            "funding": {
+                "url": "https://github.com/inikulin/parse5?sponsor=1"
+            }
+        },
+        "node_modules/path-exists": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/path-key": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/path-parse": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+            "dev": true
+        },
+        "node_modules/path-type": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+            "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/prelude-ls": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+            "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/prettier": {
+            "version": "2.8.2",
+            "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz",
+            "integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==",
+            "dev": true,
+            "peer": true,
+            "bin": {
+                "prettier": "bin-prettier.js"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            },
+            "funding": {
+                "url": "https://github.com/prettier/prettier?sponsor=1"
+            }
+        },
+        "node_modules/prettier-linter-helpers": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+            "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+            "dev": true,
+            "dependencies": {
+                "fast-diff": "^1.1.2"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/psl": {
+            "version": "1.9.0",
+            "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+            "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
+        },
+        "node_modules/punycode": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/querystringify": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+            "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+        },
+        "node_modules/queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/regexp.prototype.flags": {
+            "version": "1.4.3",
+            "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+            "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.3",
+                "functions-have-names": "^1.2.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/regexpp": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+            "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/mysticatea"
+            }
+        },
+        "node_modules/requires-port": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+            "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+        },
+        "node_modules/resolve": {
+            "version": "1.22.1",
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+            "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+            "dev": true,
+            "dependencies": {
+                "is-core-module": "^2.9.0",
+                "path-parse": "^1.0.7",
+                "supports-preserve-symlinks-flag": "^1.0.0"
+            },
+            "bin": {
+                "resolve": "bin/resolve"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/resolve-from": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/reusify": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+            "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+            "dev": true,
+            "engines": {
+                "iojs": ">=1.0.0",
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/run-parallel": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "dependencies": {
+                "queue-microtask": "^1.2.2"
+            }
+        },
+        "node_modules/safe-regex-test": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+            "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "get-intrinsic": "^1.1.3",
+                "is-regex": "^1.1.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/safer-buffer": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+            "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+        },
+        "node_modules/saxes": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+            "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+            "dependencies": {
+                "xmlchars": "^2.2.0"
+            },
+            "engines": {
+                "node": ">=v12.22.7"
+            }
+        },
+        "node_modules/semver": {
+            "version": "7.3.8",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+            "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+            "dev": true,
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/shebang-command": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "shebang-regex": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/shebang-regex": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/side-channel": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+            "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.0",
+                "get-intrinsic": "^1.0.2",
+                "object-inspect": "^1.9.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/slash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+            "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/source-map": {
+            "version": "0.6.1",
+            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/string.prototype.trimend": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+            "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/string.prototype.trimstart": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+            "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-bom": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+            "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/strip-json-comments": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/supports-preserve-symlinks-flag": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+            "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/symbol-tree": {
+            "version": "3.2.4",
+            "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+            "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
+        },
+        "node_modules/text-table": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
+        "node_modules/tough-cookie": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
+            "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
+            "dependencies": {
+                "psl": "^1.1.33",
+                "punycode": "^2.1.1",
+                "universalify": "^0.2.0",
+                "url-parse": "^1.5.3"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/tr46": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+            "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+            "dependencies": {
+                "punycode": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/ts-node": {
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+            "dev": true,
+            "dependencies": {
+                "@cspotcode/source-map-support": "^0.8.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.1",
+                "yn": "3.1.1"
+            },
+            "bin": {
+                "ts-node": "dist/bin.js",
+                "ts-node-cwd": "dist/bin-cwd.js",
+                "ts-node-esm": "dist/bin-esm.js",
+                "ts-node-script": "dist/bin-script.js",
+                "ts-node-transpile-only": "dist/bin-transpile.js",
+                "ts-script": "dist/bin-script-deprecated.js"
+            },
+            "peerDependencies": {
+                "@swc/core": ">=1.2.50",
+                "@swc/wasm": ">=1.2.50",
+                "@types/node": "*",
+                "typescript": ">=2.7"
+            },
+            "peerDependenciesMeta": {
+                "@swc/core": {
+                    "optional": true
+                },
+                "@swc/wasm": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/tsconfig-paths": {
+            "version": "3.14.1",
+            "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
+            "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
+            "dev": true,
+            "dependencies": {
+                "@types/json5": "^0.0.29",
+                "json5": "^1.0.1",
+                "minimist": "^1.2.6",
+                "strip-bom": "^3.0.0"
+            }
+        },
+        "node_modules/tslib": {
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+            "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+            "dev": true
+        },
+        "node_modules/tsutils": {
+            "version": "3.21.0",
+            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+            "dev": true,
+            "dependencies": {
+                "tslib": "^1.8.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            },
+            "peerDependencies": {
+                "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+            }
+        },
+        "node_modules/type-check": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+            "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+            "dependencies": {
+                "prelude-ls": "~1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/typed-array-length": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+            "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "for-each": "^0.3.3",
+                "is-typed-array": "^1.1.9"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "4.9.4",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
+            "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
+            "dev": true,
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=4.2.0"
+            }
+        },
+        "node_modules/unbox-primitive": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+            "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+            "dev": true,
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "has-bigints": "^1.0.2",
+                "has-symbols": "^1.0.3",
+                "which-boxed-primitive": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/universalify": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+            "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+            "engines": {
+                "node": ">= 4.0.0"
+            }
+        },
+        "node_modules/uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "punycode": "^2.1.0"
+            }
+        },
+        "node_modules/url-parse": {
+            "version": "1.5.10",
+            "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+            "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+            "dependencies": {
+                "querystringify": "^2.1.1",
+                "requires-port": "^1.0.0"
+            }
+        },
+        "node_modules/v8-compile-cache-lib": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+            "dev": true
+        },
+        "node_modules/w3c-xmlserializer": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+            "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+            "dependencies": {
+                "xml-name-validator": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/webidl-conversions": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+            "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/whatwg-encoding": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+            "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+            "dependencies": {
+                "iconv-lite": "0.6.3"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/whatwg-mimetype": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+            "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/whatwg-url": {
+            "version": "11.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+            "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+            "dependencies": {
+                "tr46": "^3.0.0",
+                "webidl-conversions": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/which": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+            "dev": true,
+            "peer": true,
+            "dependencies": {
+                "isexe": "^2.0.0"
+            },
+            "bin": {
+                "node-which": "bin/node-which"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/which-boxed-primitive": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+            "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+            "dev": true,
+            "dependencies": {
+                "is-bigint": "^1.0.1",
+                "is-boolean-object": "^1.1.0",
+                "is-number-object": "^1.0.4",
+                "is-string": "^1.0.5",
+                "is-symbol": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/which-typed-array": {
+            "version": "1.1.9",
+            "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+            "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+            "dev": true,
+            "dependencies": {
+                "available-typed-arrays": "^1.0.5",
+                "call-bind": "^1.0.2",
+                "for-each": "^0.3.3",
+                "gopd": "^1.0.1",
+                "has-tostringtag": "^1.0.0",
+                "is-typed-array": "^1.1.10"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/word-wrap": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+            "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/ws": {
+            "version": "8.11.0",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+            "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "peerDependencies": {
+                "bufferutil": "^4.0.1",
+                "utf-8-validate": "^5.0.2"
+            },
+            "peerDependenciesMeta": {
+                "bufferutil": {
+                    "optional": true
+                },
+                "utf-8-validate": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/xml-name-validator": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+            "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/xmlchars": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+            "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
+        },
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+            "dev": true
+        },
+        "node_modules/yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/yocto-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+            "dev": true,
+            "peer": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/package.json b/packages/SystemUI/scripts/token_alignment/package.json
new file mode 100644
index 0000000..2e63668
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/package.json
@@ -0,0 +1,22 @@
+{
+    "dependencies": {
+        "csv-parse": "^5.3.3",
+        "high5": "^1.0.0",
+        "jsdom": "^20.0.3"
+    },
+    "devDependencies": {
+        "@types/jsdom": "^20.0.1",
+        "@types/node": "^18.11.18",
+        "@typescript-eslint/eslint-plugin": "^5.48.0",
+        "eslint-config-prettier": "^8.6.0",
+        "eslint-plugin-import": "^2.26.0",
+        "eslint-plugin-prettier": "^4.2.1",
+        "eslint-plugin-simple-import-sort": "^8.0.0",
+        "ts-node": "^10.9.1",
+        "typescript": "^4.9.4"
+    },
+    "scripts": {
+        "main": "ts-node ./index.ts",
+        "resetRepo": "repo forall -j100  -c 'git restore .'"
+    }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/resources/migrationList.csv b/packages/SystemUI/scripts/token_alignment/resources/migrationList.csv
new file mode 100644
index 0000000..4111bb3
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/migrationList.csv
@@ -0,0 +1,16 @@
+android	material	newDefaultValue	newComment	migrationToken
+colorAccentPrimaryVariant	colorPrimaryContainer			MigTok02
+colorAccentSecondaryVariant	colorSecondaryContainer			MigTok04
+colorAccentTertiary	colorTertiary			MigTok05
+colorAccentTertiaryVariant	colorTertiaryContainer			MigTok06
+colorBackground	colorSurfaceContainer			MigTok07
+colorSurface	colorSurfaceContainer			MigTok08
+colorSurfaceHeader	colorSurfaceContainerHighest			MigTok09
+colorSurfaceHighlight	colorSurfaceBright			MigTok10
+colorSurfaceVariant	colorSurfaceContainerHigh			MigTok11
+textColorOnAccent	colorOnPrimary			MigTok12
+textColorPrimary	colorOnSurface			MigTok13
+textColorPrimaryInverse	colorOnShadeInactive			MigTok14
+textColorSecondary	colorOnSurfaceVariant			MigTok15
+textColorSecondaryInverse	colorOnShadeInactiveVariant			MigTok16
+textColorTertiary	colorOutline			MigTok17
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/resources/whitelist/java.json b/packages/SystemUI/scripts/token_alignment/resources/whitelist/java.json
new file mode 100644
index 0000000..7f55b2d
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/whitelist/java.json
@@ -0,0 +1,30 @@
+[
+    "frameworks/base/core/java/android/app/Notification.java",
+    "packages/apps/Settings/src/com/android/settings/dashboard/profileselector/UserAdapter.java",
+    "packages/apps/Settings/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java",
+    "frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt",
+    "frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt",
+    "packages/apps/WallpaperPicker2/src/com/android/wallpaper/picker/PreviewFragment.java",
+    "packages/apps/WallpaperPicker2/src/com/android/wallpaper/picker/CategorySelectorFragment.java",
+    "packages/apps/Launcher3/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt",
+    "frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java",
+    "vendor/unbundled_google/packages/NexusLauncher/src/com/google/android/apps/nexuslauncher/customize/WallpaperCarouselView.java",
+    "vendor/unbundled_google/packages/NexusLauncher/src/com/google/android/apps/nexuslauncher/quickstep/TaskOverlayFactoryImpl.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserViewBinder.kt",
+    "frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java",
+    "frameworks/base/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipView.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java",
+    "frameworks/base/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java"
+]
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls1.json b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls1.json
new file mode 100644
index 0000000..1e59773
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls1.json
@@ -0,0 +1,184 @@
+[
+    "frameworks/base/core/res/res/color/resolver_profile_tab_selected_bg.xml",
+    "frameworks/base/core/res/res/color-night/resolver_profile_tab_selected_bg.xml",
+    "frameworks/base/core/res/res/drawable/autofill_bottomsheet_background.xml",
+    "frameworks/base/core/res/res/drawable/btn_outlined.xml",
+    "frameworks/base/core/res/res/drawable/btn_tonal.xml",
+    "frameworks/base/core/res/res/drawable/chooser_action_button_bg.xml",
+    "frameworks/base/core/res/res/drawable/chooser_row_layer_list.xml",
+    "frameworks/base/core/res/res/drawable/resolver_outlined_button_bg.xml",
+    "frameworks/base/core/res/res/drawable/resolver_profile_tab_bg.xml",
+    "frameworks/base/core/res/res/drawable/toast_frame.xml",
+    "frameworks/base/core/res/res/drawable/work_widget_mask_view_background.xml",
+    "frameworks/base/core/res/res/layout/app_language_picker_current_locale_item.xml",
+    "frameworks/base/core/res/res/layout/app_language_picker_system_current.xml",
+    "frameworks/base/core/res/res/layout/autofill_save.xml",
+    "frameworks/base/core/res/res/layout/chooser_grid.xml",
+    "frameworks/base/core/res/res/layout/user_switching_dialog.xml",
+    "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/half_rounded_left_bk.xml",
+    "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/half_rounded_right_bk.xml",
+    "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/rounded_bk.xml",
+    "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/square_bk.xml",
+    "packages/modules/IntentResolver/java/res/drawable/chooser_action_button_bg.xml",
+    "packages/modules/IntentResolver/java/res/drawable/chooser_row_layer_list.xml",
+    "packages/modules/IntentResolver/java/res/drawable/resolver_outlined_button_bg.xml",
+    "packages/modules/IntentResolver/java/res/drawable/resolver_profile_tab_bg.xml",
+    "packages/modules/IntentResolver/java/res/layout/chooser_grid.xml",
+    "frameworks/base/libs/WindowManager/Shell/res/color/one_handed_tutorial_background_color.xml",
+    "frameworks/base/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml",
+    "frameworks/base/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg.xml",
+    "frameworks/base/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg_rtl.xml",
+    "vendor/unbundled_google/packages/SystemUIGoogle/bcsmartspace/res/drawable/bg_smartspace_combination_sub_card.xml",
+    "vendor/unbundled_google/packages/SystemUIGoogle/bcsmartspace/res/layout/smartspace_combination_sub_card.xml",
+    "packages/apps/Launcher3/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml",
+    "packages/apps/Launcher3/res/drawable/rounded_action_button.xml",
+    "packages/apps/Launcher3/res/drawable/work_card.xml",
+    "packages/apps/Nfc/res/color/nfc_icon.xml",
+    "packages/apps/Nfc/res/color-night/nfc_icon.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/bg_overview_clear_all_button.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/bg_sandbox_feedback.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/bg_wellbeing_toast.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/button_taskbar_edu_bordered.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/button_taskbar_edu_colored.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/split_instructions_background.xml",
+    "packages/apps/Launcher3/quickstep/res/drawable/task_menu_item_bg.xml",
+    "packages/apps/Launcher3/quickstep/res/layout/digital_wellbeing_toast.xml",
+    "packages/apps/Launcher3/quickstep/res/layout/split_instructions_view.xml",
+    "packages/apps/Launcher3/quickstep/res/layout/taskbar_edu.xml",
+    "frameworks/base/packages/SettingsLib/res/drawable/broadcast_dialog_btn_bg.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color/share_target_text.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/all_apps_tab_background_selected.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/all_apps_tabs_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/arrow_tip_view_bg.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/button_bg.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/shortcut_halo.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/surface.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-night-v31/all_apps_tab_background_selected.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/color-night-v31/surface.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/bg_pin_keyboard_snackbar_accept_button.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/bg_search_edu_preferences_button.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/circle_accentprimary_32dp.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/ic_search.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/ic_suggest_icon_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/share_target_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/sticky_snackbar_accept_btn_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/sticky_snackbar_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/sticky_snackbar_dismiss_btn_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/drawable/tall_card_btn_background.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/layout/section_header.xml",
+    "packages/apps/Settings/res/color/dream_card_color_state_list.xml",
+    "packages/apps/Settings/res/color/dream_card_icon_color_state_list.xml",
+    "packages/apps/Settings/res/color/dream_card_text_color_state_list.xml",
+    "packages/apps/Settings/res/drawable/accessibility_text_reading_preview.xml",
+    "packages/apps/Settings/res/drawable/broadcast_button_outline.xml",
+    "packages/apps/Settings/res/drawable/button_border_selected.xml",
+    "packages/apps/Settings/res/drawable/dream_preview_rounded_bg.xml",
+    "packages/apps/Settings/res/drawable/rounded_bg.xml",
+    "packages/apps/Settings/res/drawable/sim_confirm_dialog_btn_outline.xml",
+    "packages/apps/Settings/res/drawable/user_select_background.xml",
+    "packages/apps/Settings/res/drawable/volume_dialog_button_background_outline.xml",
+    "packages/apps/Settings/res/drawable/volume_dialog_button_background_solid.xml",
+    "packages/apps/Settings/res/layout/dream_preview_button.xml",
+    "packages/apps/Settings/res/layout/qrcode_scanner_fragment.xml",
+    "frameworks/base/packages/SystemUI/res/values-television/styles.xml",
+    "frameworks/base/packages/SystemUI/res/color/media_player_album_bg.xml",
+    "frameworks/base/packages/SystemUI/res/color/media_player_outline_button_bg.xml",
+    "frameworks/base/packages/SystemUI/res/color/media_player_solid_button_bg.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml",
+    "frameworks/base/packages/SystemUI/res/color/settingslib_state_on.xml",
+    "frameworks/base/packages/SystemUI/res/color/settingslib_track_off.xml",
+    "frameworks/base/packages/SystemUI/res/color/settingslib_track_on.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/accessibility_floating_tooltip_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/action_chip_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/action_chip_container_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/availability_dot_10dp.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_item_selected_bg.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_popup_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/broadcast_dialog_btn_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/fgs_dot.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/fingerprint_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/ic_avatar_with_badge.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/drawable/kg_bouncer_secondary_button.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/drawable/kg_emergency_button_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/logout_button_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/media_ttt_chip_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/media_ttt_undo_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/notif_footer_btn_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/notification_guts_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/notification_material_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/overlay_badge_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/overlay_border.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/overlay_button_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/overlay_cancel.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/people_space_messages_count_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/people_tile_status_scrim.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/people_tile_suppressed_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/qs_media_outline_button.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/qs_media_solid_button.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/rounded_bg_full.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/screenrecord_button_background_solid.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/screenrecord_options_spinner_popup_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/screenrecord_spinner_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/screenshot_edit_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/user_switcher_fullscreen_button_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/volume_background_bottom.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/volume_background_top.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/volume_background_top_rounded.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/volume_row_rounded_background.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/volume_row_seekbar.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/wallet_action_button_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/wallet_app_button_bg.xml",
+    "frameworks/base/packages/SystemUI/res/drawable/wallet_empty_state_bg.xml",
+    "frameworks/base/packages/SystemUI/res/layout/alert_dialog_title_systemui.xml",
+    "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+    "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+    "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay.xml",
+    "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay.xml",
+    "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml",
+    "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml",
+    "frameworks/base/packages/SystemUI/res/layout/internet_connectivity_dialog.xml",
+    "frameworks/base/packages/SystemUI/res/layout/notification_snooze.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_space_tile_view.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_large_with_content.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_medium_with_content.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml",
+    "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+    "frameworks/base/packages/SystemUI/res/layout/notification_snooze.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_small.xml",
+    "frameworks/base/packages/SystemUI/res/layout/people_tile_small_horizontal.xml",
+    "frameworks/base/packages/SystemUI/res/layout/screen_share_dialog.xml",
+    "frameworks/base/packages/SystemUI/res/layout/screen_share_dialog_spinner_item_text.xml",
+    "frameworks/base/packages/SystemUI/res/layout/user_switcher_fullscreen.xml",
+    "frameworks/base/packages/SystemUI/res/layout/user_switcher_fullscreen.xml",
+    "frameworks/base/packages/SystemUI/res/layout/wallet_empty_state.xml",
+    "frameworks/base/packages/SystemUI/res/layout/wallet_fullscreen.xml",
+    "frameworks/base/packages/SystemUI/res/layout/wallet_fullscreen.xml",
+    "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+    "frameworks/base/packages/SystemUI/res/layout/notification_snooze.xml",
+    "vendor/unbundled_google/packages/SystemUIGoogle/res/drawable/columbus_chip_background_raw.xml",
+    "vendor/unbundled_google/packages/SystemUIGoogle/res/drawable/columbus_chip_background_raw.xml",
+    "vendor/unbundled_google/packages/SystemUIGoogle/res/drawable/columbus_dialog_background.xml",
+    "vendor/unbundled_google/packages/SystemUIGoogle/res/layout/columbus_target_request_dialog.xml",
+    "vendor/unbundled_google/packages/SettingsGoogle/res/color/dream_card_suw_color_state_list.xml",
+    "vendor/unbundled_google/packages/SettingsGoogle/res/drawable/dream_item_suw_rounded_bg.xml"
+]
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls2.json b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls2.json
new file mode 100644
index 0000000..20a7c76
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls2.json
@@ -0,0 +1,13 @@
+[
+    "vendor/google/nexus_overlay/PixelDocumentsUIGoogleOverlay/res/values-v31/themes.xml",
+    "vendor/google/nexus_overlay/PixelDocumentsUIGoogleOverlay/res/values-night-v31/themes.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/values/colors.xml",
+    "vendor/unbundled_google/packages/NexusLauncher/res/values/styles.xml",
+    "packages/apps/Settings/res/values-night/colors.xml",
+    "packages/apps/Settings/res/values/colors.xml",
+    "packages/apps/Settings/res/values/styles.xml",
+    "frameworks/base/packages/SystemUI/res-keyguard/values/styles.xml",
+    "frameworks/base/packages/SystemUI/res/values/styles.xml",
+    "vendor/unbundled_google/packages/SettingsGoogle/res/values/styles.xml",
+    "vendor/unbundled_google/packages/SettingsGoogle/res/values/styles.xml"
+]
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/tsconfig.json b/packages/SystemUI/scripts/token_alignment/tsconfig.json
new file mode 100644
index 0000000..20c7321
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/tsconfig.json
@@ -0,0 +1,103 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig to read more about this file */
+
+    /* Projects */
+    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
+    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
+    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+    /* Language and Environment */
+    "target": "ESNext",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
+    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
+    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+    // "jsxFragmentFactory": "",                         /* Speciffy the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
+
+    /* Modules */
+    "module": "commonjs",                                /* Specify what module code is generated. */
+    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
+    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
+    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+    "typeRoots": ["../node_modules/@types"],                                  /* Specify multiple folders that act like './node_modules/@types'. */
+    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
+    "resolveJsonModule": true,                        /* Enable importing .json files. */
+    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+    /* JavaScript Support */
+    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+    /* Emit */
+    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+    "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
+    // "removeComments": true,                           /* Disable emitting comments. */
+    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
+    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
+    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
+    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
+    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
+    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+    /* Interop Constraints */
+    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+    /* Type Checking */
+    "strict": true,                                      /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
+    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
+    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
+    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
+    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
+    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
+    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
+    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+    /* Completeness */
+    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+  }
+}
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8a0fca0..28e786b 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -91,6 +91,9 @@
     static_libs: [
         "SystemUI-flag-types",
     ],
+    optimize: {
+        proguard_flags_files: ["proguard_flags.flags"],
+    },
     java_version: "1.8",
     min_sdk_version: "current",
 }
diff --git a/packages/SystemUI/shared/proguard_flags.flags b/packages/SystemUI/shared/proguard_flags.flags
new file mode 100644
index 0000000..08859cd
--- /dev/null
+++ b/packages/SystemUI/shared/proguard_flags.flags
@@ -0,0 +1 @@
+-keep class * implements com.android.systemui.flags.ParcelableFlag
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
index 196f7f0..b6aae69 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
@@ -47,10 +47,6 @@
     val resourceId: Int
 }
 
-interface DeviceConfigFlag<T> : Flag<T> {
-    val default: T
-}
-
 interface SysPropFlag<T> : Flag<T> {
     val default: T
 }
@@ -80,8 +76,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
-        name = parcel.readString(),
-        namespace = parcel.readString(),
+        name = parcel.readString() ?: "",
+        namespace = parcel.readString() ?: "",
         default = parcel.readBoolean(),
         teamfood = parcel.readBoolean(),
         overridden = parcel.readBoolean()
@@ -137,21 +133,6 @@
 ) : ResourceFlag<Boolean>
 
 /**
- * A Flag that can reads its overrides from DeviceConfig.
- *
- * This is generally useful for flags that come from or are used _outside_ of SystemUI.
- *
- * Prefer [UnreleasedFlag] and [ReleasedFlag].
- */
-data class DeviceConfigBooleanFlag constructor(
-    override val id: Int,
-    override val name: String,
-    override val namespace: String,
-    override val default: Boolean = false,
-    override val teamfood: Boolean = false
-) : DeviceConfigFlag<Boolean>
-
-/**
  * A Flag that can reads its overrides from System Properties.
  *
  * This is generally useful for flags that come from or are used _outside_ of SystemUI.
@@ -164,7 +145,7 @@
     override val namespace: String,
     override val default: Boolean = false,
 ) : SysPropFlag<Boolean> {
-    // TODO(b/223379190): Teamfood not supported for sysprop flags yet.
+    // TODO(b/268520433): Teamfood not supported for sysprop flags yet.
     override val teamfood: Boolean = false
 }
 
@@ -186,8 +167,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
-        name = parcel.readString(),
-        namespace = parcel.readString(),
+        name = parcel.readString() ?: "",
+        namespace = parcel.readString() ?: "",
         default = parcel.readString() ?: ""
     )
 
@@ -226,8 +207,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
-        name = parcel.readString(),
-        namespace = parcel.readString(),
+        name = parcel.readString() ?: "",
+        namespace = parcel.readString() ?: "",
         default = parcel.readInt()
     )
 
@@ -266,8 +247,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
-        name = parcel.readString(),
-        namespace = parcel.readString(),
+        name = parcel.readString() ?: "",
+        namespace = parcel.readString() ?: "",
         default = parcel.readLong()
     )
 
@@ -298,8 +279,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
-        name = parcel.readString(),
-        namespace = parcel.readString(),
+        name = parcel.readString() ?: "",
+        namespace = parcel.readString() ?: "",
         default = parcel.readFloat()
     )
 
@@ -338,8 +319,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
-        name = parcel.readString(),
-        namespace = parcel.readString(),
+        name = parcel.readString() ?: "",
+        namespace = parcel.readString() ?: "",
         default = parcel.readDouble()
     )
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
index 195ba465..72a4fab 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
@@ -34,7 +34,7 @@
     /** An event representing the change */
     interface FlagEvent {
         /** the id of the flag which changed */
-        val flagId: Int
+        val flagName: String
         /** if all listeners alerted invoke this method, the restart will be skipped */
         fun requestNoRestart()
     }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index d85292a..da1641c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -39,7 +39,7 @@
         const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
         const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
         const val ACTION_SYSUI_STARTED = "com.android.systemui.STARTED"
-        const val EXTRA_ID = "id"
+        const val EXTRA_NAME = "name"
         const val EXTRA_VALUE = "value"
         const val EXTRA_FLAGS = "flags"
         private const val SETTINGS_PREFIX = "systemui/flags"
@@ -56,7 +56,7 @@
      * that the restart be suppressed
      */
     var onSettingsChangedAction: Consumer<Boolean>? = null
-    var clearCacheAction: Consumer<Int>? = null
+    var clearCacheAction: Consumer<String>? = null
     private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
     private val settingsObserver: ContentObserver = SettingsObserver()
 
@@ -96,35 +96,42 @@
      * Returns the stored value or null if not set.
      * This API is used by TheFlippinApp.
      */
-    fun isEnabled(id: Int): Boolean? = readFlagValue(id, BooleanFlagSerializer)
+    fun isEnabled(name: String): Boolean? = readFlagValue(name, BooleanFlagSerializer)
 
     /**
      * Sets the value of a boolean flag.
      * This API is used by TheFlippinApp.
      */
-    fun setFlagValue(id: Int, enabled: Boolean) {
-        val intent = createIntent(id)
+    fun setFlagValue(name: String, enabled: Boolean) {
+        val intent = createIntent(name)
         intent.putExtra(EXTRA_VALUE, enabled)
 
         context.sendBroadcast(intent)
     }
 
-    fun eraseFlag(id: Int) {
-        val intent = createIntent(id)
+    fun eraseFlag(name: String) {
+        val intent = createIntent(name)
 
         context.sendBroadcast(intent)
     }
 
     /** Returns the stored value or null if not set.  */
+    // TODO(b/265188950): Remove method this once ids are fully deprecated.
     fun <T> readFlagValue(id: Int, serializer: FlagSerializer<T>): T? {
-        val data = settings.getString(idToSettingsKey(id))
+        val data = settings.getStringFromSecure(idToSettingsKey(id))
+        return serializer.fromSettingsData(data)
+    }
+
+    /** Returns the stored value or null if not set.  */
+    fun <T> readFlagValue(name: String, serializer: FlagSerializer<T>): T? {
+        val data = settings.getString(nameToSettingsKey(name))
         return serializer.fromSettingsData(data)
     }
 
     override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
         synchronized(listeners) {
             val registerNeeded = listeners.isEmpty()
-            listeners.add(PerFlagListener(flag.id, listener))
+            listeners.add(PerFlagListener(flag.name, listener))
             if (registerNeeded) {
                 settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver)
             }
@@ -143,38 +150,38 @@
         }
     }
 
-    private fun createIntent(id: Int): Intent {
+    private fun createIntent(name: String): Intent {
         val intent = Intent(ACTION_SET_FLAG)
         intent.setPackage(RECEIVING_PACKAGE)
-        intent.putExtra(EXTRA_ID, id)
+        intent.putExtra(EXTRA_NAME, name)
 
         return intent
     }
 
+    // TODO(b/265188950): Remove method this once ids are fully deprecated.
     fun idToSettingsKey(id: Int): String {
         return "$SETTINGS_PREFIX/$id"
     }
 
+    fun nameToSettingsKey(name: String): String {
+        return "$SETTINGS_PREFIX/$name"
+    }
+
     inner class SettingsObserver : ContentObserver(handler) {
         override fun onChange(selfChange: Boolean, uri: Uri?) {
             if (uri == null) {
                 return
             }
             val parts = uri.pathSegments
-            val idStr = parts[parts.size - 1]
-            val id = try {
-                idStr.toInt()
-            } catch (e: NumberFormatException) {
-                return
-            }
-            clearCacheAction?.accept(id)
-            dispatchListenersAndMaybeRestart(id, onSettingsChangedAction)
+            val name = parts[parts.size - 1]
+            clearCacheAction?.accept(name)
+            dispatchListenersAndMaybeRestart(name, onSettingsChangedAction)
         }
     }
 
-    fun dispatchListenersAndMaybeRestart(id: Int, restartAction: Consumer<Boolean>?) {
+    fun dispatchListenersAndMaybeRestart(name: String, restartAction: Consumer<Boolean>?) {
         val filteredListeners: List<FlagListenable.Listener> = synchronized(listeners) {
-            listeners.mapNotNull { if (it.id == id) it.listener else null }
+            listeners.mapNotNull { if (it.name == name) it.listener else null }
         }
         // If there are no listeners, there's nothing to dispatch to, and nothing to suppress it.
         if (filteredListeners.isEmpty()) {
@@ -185,7 +192,7 @@
         val suppressRestartList: List<Boolean> = filteredListeners.map { listener ->
             var didRequestNoRestart = false
             val event = object : FlagListenable.FlagEvent {
-                override val flagId = id
+                override val flagName = name
                 override fun requestNoRestart() {
                     didRequestNoRestart = true
                 }
@@ -198,7 +205,7 @@
         restartAction?.accept(suppressRestart)
     }
 
-    private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
+    private data class PerFlagListener(val name: String, val listener: FlagListenable.Listener)
 }
 
 class NoFlagResultsException : Exception(
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt
index 742bb0b..6beb851 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt
@@ -22,7 +22,10 @@
 
 class FlagSettingsHelper(private val contentResolver: ContentResolver) {
 
-    fun getString(key: String): String? = Settings.Secure.getString(contentResolver, key)
+    // TODO(b/265188950): Remove method this once ids are fully deprecated.
+    fun getStringFromSecure(key: String): String? = Settings.Secure.getString(contentResolver, key)
+
+    fun getString(key: String): String? = Settings.Global.getString(contentResolver, key)
 
     fun registerContentObserver(
         name: String,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
index 12e0b9a..c5979cc 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
@@ -65,35 +65,40 @@
             } else {
                 1
             }
-        viewsToTranslate.forEach { (view, direction, shouldBeAnimated) ->
-            if (shouldBeAnimated()) {
-                view.get()?.translationX = xTrans * direction.multiplier * rtlMultiplier
-            }
+        viewsToTranslate.forEach { (view, direction) ->
+            view.get()?.translationX = xTrans * direction.multiplier * rtlMultiplier
         }
     }
 
     /** Finds in [parent] all views specified by [ids] and register them for the animation. */
     private fun registerViewsForAnimation(parent: ViewGroup, ids: Set<ViewIdToTranslate>) {
         viewsToTranslate =
-            ids.mapNotNull { (id, dir, pred) ->
-                parent.findViewById<View>(id)?.let { view ->
-                    ViewToTranslate(WeakReference(view), dir, pred)
+            ids.asSequence()
+                .filter { it.shouldBeAnimated() }
+                .mapNotNull {
+                    parent.findViewById<View>(it.viewId)?.let { view ->
+                        ViewToTranslate(WeakReference(view), it.direction)
+                    }
                 }
-            }
+                .toList()
     }
 
-    /** Represents a view to animate. [rootView] should contain a view with [viewId] inside. */
+    /**
+     * Represents a view to animate. [rootView] should contain a view with [viewId] inside.
+     * [shouldBeAnimated] is only evaluated when the viewsToTranslate is registered in
+     * [registerViewsForAnimation].
+     */
     data class ViewIdToTranslate(
         val viewId: Int,
         val direction: Direction,
         val shouldBeAnimated: () -> Boolean = { true }
     )
 
-    private data class ViewToTranslate(
-        val view: WeakReference<View>,
-        val direction: Direction,
-        val shouldBeAnimated: () -> Boolean
-    )
+    /**
+     * Represents a view whose animation process is in-progress. It should be immutable because the
+     * started animation should be completed.
+     */
+    private data class ViewToTranslate(val view: WeakReference<View>, val direction: Direction)
 
     /** Direction of the animation. */
     enum class Direction(val multiplier: Float) {
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/hardware/InputDevice.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt
new file mode 100644
index 0000000..a9a5cf9
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.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.shared.hardware
+
+import android.view.InputDevice
+
+/**
+ * Returns true if [InputDevice] is electronic components to allow a user to use an active stylus in
+ * the host device or a passive stylus is detected by the host device.
+ */
+val InputDevice.isInternalStylusSource: Boolean
+    get() = isAnyStylusSource && !isExternal
+
+/** Returns true if [InputDevice] is an active stylus. */
+val InputDevice.isExternalStylusSource: Boolean
+    get() = isAnyStylusSource && isExternal
+
+/**
+ * Returns true if [InputDevice] supports any stylus source.
+ *
+ * @see InputDevice.isInternalStylusSource
+ * @see InputDevice.isExternalStylusSource
+ */
+val InputDevice.isAnyStylusSource: Boolean
+    get() = supportsSource(InputDevice.SOURCE_STYLUS)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt
new file mode 100644
index 0000000..f020b4e
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.shared.hardware
+
+import android.hardware.input.InputManager
+import android.view.InputDevice
+
+/**
+ * Gets information about all input devices in the system and returns as a lazy [Sequence].
+ *
+ * For performance reasons, it is preferred to operate atop the returned [Sequence] to ensure each
+ * operation is executed on an element-per-element basis yet customizable.
+ *
+ * For example:
+ * ```kotlin
+ * val stylusDevices = inputManager.getInputDeviceSequence().filter {
+ *   it.supportsSource(InputDevice.SOURCE_STYLUS)
+ * }
+ *
+ * val hasInternalStylus = stylusDevices.any { it.isInternal }
+ * val hasExternalStylus = stylusDevices.any { !it.isInternal }
+ * ```
+ *
+ * @return a [Sequence] of [InputDevice].
+ */
+fun InputManager.getInputDeviceSequence(): Sequence<InputDevice> =
+    inputDeviceIds.asSequence().mapNotNull { getInputDevice(it) }
+
+/**
+ * Returns the first [InputDevice] matching the given predicate, or null if no such [InputDevice]
+ * was found.
+ */
+fun InputManager.findInputDevice(predicate: (InputDevice) -> Boolean): InputDevice? =
+    getInputDeviceSequence().find { predicate(it) }
+
+/**
+ * Returns true if [any] [InputDevice] matches with [predicate].
+ *
+ * For example:
+ * ```kotlin
+ * val hasStylusSupport = inputManager.hasInputDevice { it.isStylusSupport() }
+ * val hasStylusPen = inputManager.hasInputDevice { it.isStylusPen() }
+ * ```
+ */
+fun InputManager.hasInputDevice(predicate: (InputDevice) -> Boolean): Boolean =
+    getInputDeviceSequence().any { predicate(it) }
+
+/** Returns true if host device has any [InputDevice] where [InputDevice.isInternalStylusSource]. */
+fun InputManager.hasInternalStylusSource(): Boolean = hasInputDevice { it.isInternalStylusSource }
+
+/** Returns true if host device has any [InputDevice] where [InputDevice.isExternalStylusSource]. */
+fun InputManager.hasExternalStylusSource(): Boolean = hasInputDevice { it.isExternalStylusSource }
+
+/** Returns true if host device has any [InputDevice] where [InputDevice.isAnyStylusSource]. */
+fun InputManager.hasAnyStylusSource(): Boolean = hasInputDevice { it.isAnyStylusSource }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 1c532fe..b8bddd1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -22,6 +22,7 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.view.MotionEvent;
+import com.android.internal.util.ScreenshotRequest;
 
 import com.android.systemui.shared.recents.model.Task;
 
@@ -87,12 +88,6 @@
     void notifyPrioritizedRotation(int rotation) = 25;
 
     /**
-     * Handle the provided image as if it was a screenshot.
-     */
-    void handleImageBundleAsScreenshot(in Bundle screenImageBundle, in Rect locationInScreen,
-              in Insets visibleInsets, in Task.TaskKey task) = 28;
-
-    /**
      * Notifies to expand notification panel.
      */
     void expandNotificationPanel() = 29;
@@ -125,5 +120,10 @@
      */
     void toggleNotificationPanel() = 50;
 
-    // Next id = 51
+    /**
+     * Handle the screenshot request.
+     */
+    void takeScreenshot(in ScreenshotRequest request) = 51;
+
+    // Next id = 52
 }
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 0ee813b..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
@@ -15,39 +15,36 @@
  */
 package com.android.systemui.shared.regionsampling
 
+import android.app.WallpaperColors
+import android.app.WallpaperManager
 import android.graphics.Color
+import android.graphics.Point
 import android.graphics.Rect
+import android.graphics.RectF
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCallback
 import java.io.PrintWriter
 import java.util.concurrent.Executor
 
 /** Class for instance of RegionSamplingHelper */
-open class RegionSampler(
-    sampledView: View?,
+open class RegionSampler
+@JvmOverloads
+constructor(
+    val sampledView: View?,
     mainExecutor: Executor?,
-    bgExecutor: Executor?,
-    regionSamplingEnabled: Boolean,
-    updateFun: UpdateColorCallback
-) {
+    val bgExecutor: Executor?,
+    val regionSamplingEnabled: Boolean,
+    val updateForegroundColor: UpdateColorCallback,
+    val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView?.context)
+) : WallpaperManager.LocalWallpaperColorConsumer {
     private var regionDarkness = RegionDarkness.DEFAULT
     private var samplingBounds = Rect()
     private val tmpScreenLocation = IntArray(2)
     @VisibleForTesting var regionSampler: RegionSamplingHelper? = null
     private var lightForegroundColor = Color.WHITE
     private var darkForegroundColor = Color.BLACK
-
-    @VisibleForTesting
-    open fun createRegionSamplingHelper(
-        sampledView: View,
-        callback: SamplingCallback,
-        mainExecutor: Executor?,
-        bgExecutor: Executor?
-    ): RegionSamplingHelper {
-        return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
-    }
+    private val displaySize = Point()
 
     /**
      * Sets the colors to be used for Dark and Light Foreground.
@@ -73,7 +70,7 @@
         }
     }
 
-    private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
+    private fun getRegionDarkness(isRegionDark: Boolean): RegionDarkness {
         return if (isRegionDark) {
             RegionDarkness.DARK
         } else {
@@ -87,56 +84,119 @@
 
     /** Start region sampler */
     fun startRegionSampler() {
-        regionSampler?.start(samplingBounds)
+        if (!regionSamplingEnabled || sampledView == null) {
+            return
+        }
+
+        val sampledRegion = calculateSampledRegion(sampledView)
+        val regions = ArrayList<RectF>()
+        val sampledRegionWithOffset = convertBounds(sampledRegion)
+        regions.add(sampledRegionWithOffset)
+
+        wallpaperManager?.removeOnColorsChangedListener(this)
+        wallpaperManager?.addOnColorsChangedListener(this, regions)
+
+        // TODO(b/265969235): conditionally set FLAG_LOCK or FLAG_SYSTEM once HS smartspace
+        // implemented
+        bgExecutor?.execute(
+            Runnable {
+                val initialSampling =
+                    wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)
+                onColorsChanged(sampledRegionWithOffset, initialSampling)
+            }
+        )
     }
 
     /** Stop region sampler */
     fun stopRegionSampler() {
-        regionSampler?.stop()
+        wallpaperManager?.removeOnColorsChangedListener(this)
     }
 
     /** 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 {
+        val screenLocation = tmpScreenLocation
+        /**
+         * The method getLocationOnScreen is used to obtain the view coordinates relative to its
+         * left and top edges on the device screen. Directly accessing the X and Y coordinates of
+         * the view returns the location relative to its parent view instead.
+         */
+        sampledView.getLocationOnScreen(screenLocation)
+        val left = screenLocation[0]
+        val top = screenLocation[1]
+
+        samplingBounds.left = left
+        samplingBounds.top = top
+        samplingBounds.right = left + sampledView.width
+        samplingBounds.bottom = top + sampledView.height
+
+        return RectF(samplingBounds)
+    }
+
+    /**
+     * Convert the bounds of the region we want to sample from to fractional offsets because
+     * WallpaperManager requires the bounds to be between [0,1]. The wallpaper is treated as one
+     * continuous image, so if there are multiple screens, then each screen falls into a fractional
+     * range. For instance, 4 screens have the ranges [0, 0.25], [0,25, 0.5], [0.5, 0.75], [0.75,
+     * 1].
+     */
+    fun convertBounds(originalBounds: RectF): RectF {
+
+        // TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER
+        // TODO(b/265968912): remove hard-coded value once LS wallpaper supported
+        val wallpaperPageNum = 0
+        val numScreens = 1
+
+        val screenWidth = displaySize.x
+        // TODO: investigate small difference between this and the height reported in go/web-hv
+        val screenHeight = displaySize.y
+
+        val newBounds = RectF()
+        // horizontal
+        newBounds.left = ((originalBounds.left / screenWidth) + wallpaperPageNum) / numScreens
+        newBounds.right = ((originalBounds.right / screenWidth) + wallpaperPageNum) / numScreens
+        // vertical
+        newBounds.top = originalBounds.top / screenHeight
+        newBounds.bottom = originalBounds.bottom / screenHeight
+
+        return newBounds
     }
 
     init {
-        if (regionSamplingEnabled && sampledView != null) {
-            regionSampler =
-                createRegionSamplingHelper(
-                    sampledView,
-                    object : SamplingCallback {
-                        override fun onRegionDarknessChanged(isRegionDark: Boolean) {
-                            regionDarkness = convertToClockDarkness(isRegionDark)
-                            updateFun()
-                        }
-                        /**
-                         * The method getLocationOnScreen is used to obtain the view coordinates
-                         * relative to its left and top edges on the device screen. Directly
-                         * accessing the X and Y coordinates of the view returns the location
-                         * relative to its parent view instead.
-                         */
-                        override fun getSampledRegion(sampledView: View): Rect {
-                            val screenLocation = tmpScreenLocation
-                            sampledView.getLocationOnScreen(screenLocation)
-                            val left = screenLocation[0]
-                            val top = screenLocation[1]
-                            samplingBounds.left = left
-                            samplingBounds.top = top
-                            samplingBounds.right = left + sampledView.width
-                            samplingBounds.bottom = top + sampledView.height
-                            return samplingBounds
-                        }
+        sampledView?.context?.display?.getSize(displaySize)
+    }
 
-                        override fun isSamplingEnabled(): Boolean {
-                            return regionSamplingEnabled
-                        }
-                    },
-                    mainExecutor,
-                    bgExecutor
-                )
-        }
-        regionSampler?.setWindowVisible(true)
+    override fun onColorsChanged(area: RectF?, colors: WallpaperColors?) {
+        // update text color when wallpaper color changes
+        regionDarkness =
+            getRegionDarkness(
+                (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) !=
+                    WallpaperColors.HINT_SUPPORTS_DARK_TEXT
+            )
+        updateForegroundColor()
     }
 }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index 857cc462..5d036fb 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -35,6 +35,7 @@
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.widget.FrameLayout;
 
+import androidx.annotation.BoolRes;
 import androidx.core.view.OneShotPreDrawListener;
 
 import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
@@ -65,6 +66,8 @@
     private final int mTaskbarBottomMarginResource;
     @DimenRes
     private final int mButtonDiameterResource;
+    @BoolRes
+    private final int mFloatingRotationBtnPositionLeftResource;
 
     private AnimatedVectorDrawable mAnimatedDrawable;
     private boolean mIsShowing;
@@ -84,7 +87,7 @@
             @LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin,
             @DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin,
             @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
-            @DimenRes int rippleMaxWidth) {
+            @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
         mWindowManager = context.getSystemService(WindowManager.class);
         mKeyButtonContainer = (ViewGroup) LayoutInflater.from(context).inflate(layout, null);
         mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
@@ -100,6 +103,7 @@
         mTaskbarLeftMarginResource = taskbarLeftMargin;
         mTaskbarBottomMarginResource = taskbarBottomMargin;
         mButtonDiameterResource = buttonDiameter;
+        mFloatingRotationBtnPositionLeftResource = floatingRotationBtnPositionLeftResource;
 
         updateDimensionResources();
     }
@@ -116,8 +120,11 @@
         int taskbarMarginBottom =
                 res.getDimensionPixelSize(mTaskbarBottomMarginResource);
 
+        boolean floatingRotationButtonPositionLeft =
+                res.getBoolean(mFloatingRotationBtnPositionLeftResource);
+
         mPositionCalculator = new FloatingRotationButtonPositionCalculator(defaultMargin,
-                taskbarMarginLeft, taskbarMarginBottom);
+                taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft);
 
         final int diameter = res.getDimensionPixelSize(mButtonDiameterResource);
         mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
index ec3c073..40e43a9 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
@@ -10,7 +10,8 @@
 class FloatingRotationButtonPositionCalculator(
     private val defaultMargin: Int,
     private val taskbarMarginLeft: Int,
-    private val taskbarMarginBottom: Int
+    private val taskbarMarginBottom: Int,
+    private val floatingRotationButtonPositionLeft: Boolean
 ) {
 
     fun calculatePosition(
@@ -18,7 +19,6 @@
         taskbarVisible: Boolean,
         taskbarStashed: Boolean
     ): Position {
-
         val isTaskbarSide = currentRotation == Surface.ROTATION_0
             || currentRotation == Surface.ROTATION_90
         val useTaskbarMargin = isTaskbarSide && taskbarVisible && !taskbarStashed
@@ -55,11 +55,21 @@
     )
 
     private fun resolveGravity(rotation: Int): Int =
-        when (rotation) {
-            Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.LEFT
-            Surface.ROTATION_90 -> Gravity.BOTTOM or Gravity.RIGHT
-            Surface.ROTATION_180 -> Gravity.TOP or Gravity.RIGHT
-            Surface.ROTATION_270 -> Gravity.TOP or Gravity.LEFT
-            else -> throw IllegalArgumentException("Invalid rotation $rotation")
+        if (floatingRotationButtonPositionLeft) {
+            when (rotation) {
+                Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.LEFT
+                Surface.ROTATION_90 -> Gravity.BOTTOM or Gravity.RIGHT
+                Surface.ROTATION_180 -> Gravity.TOP or Gravity.RIGHT
+                Surface.ROTATION_270 -> Gravity.TOP or Gravity.LEFT
+                else -> throw IllegalArgumentException("Invalid rotation $rotation")
+            }
+        } else {
+            when (rotation) {
+                Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.RIGHT
+                Surface.ROTATION_90 -> Gravity.TOP or Gravity.RIGHT
+                Surface.ROTATION_180 -> Gravity.TOP or Gravity.LEFT
+                Surface.ROTATION_270 -> Gravity.BOTTOM or Gravity.LEFT
+                else -> throw IllegalArgumentException("Invalid rotation $rotation")
+            }
         }
 }
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 25d2721..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,
@@ -48,48 +48,28 @@
         val drawableInsetSize: Int
         try {
             val keyShadowBlur =
-                attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0)
+                attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f)
             val keyShadowOffsetX =
-                attributes.getDimensionPixelSize(
-                    R.styleable.DoubleShadowTextView_keyShadowOffsetX,
-                    0
-                )
+                attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f)
             val keyShadowOffsetY =
-                attributes.getDimensionPixelSize(
-                    R.styleable.DoubleShadowTextView_keyShadowOffsetY,
-                    0
-                )
+                attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f)
             val keyShadowAlpha =
                 attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f)
             mKeyShadowInfo =
-                ShadowInfo(
-                    keyShadowBlur.toFloat(),
-                    keyShadowOffsetX.toFloat(),
-                    keyShadowOffsetY.toFloat(),
-                    keyShadowAlpha
-                )
+                ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha)
             val ambientShadowBlur =
-                attributes.getDimensionPixelSize(
-                    R.styleable.DoubleShadowTextView_ambientShadowBlur,
-                    0
-                )
+                attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f)
             val ambientShadowOffsetX =
-                attributes.getDimensionPixelSize(
-                    R.styleable.DoubleShadowTextView_ambientShadowOffsetX,
-                    0
-                )
+                attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f)
             val ambientShadowOffsetY =
-                attributes.getDimensionPixelSize(
-                    R.styleable.DoubleShadowTextView_ambientShadowOffsetY,
-                    0
-                )
+                attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f)
             val ambientShadowAlpha =
                 attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f)
             mAmbientShadowInfo =
                 ShadowInfo(
-                    ambientShadowBlur.toFloat(),
-                    ambientShadowOffsetX.toFloat(),
-                    ambientShadowOffsetY.toFloat(),
+                    ambientShadowBlur,
+                    ambientShadowOffsetX,
+                    ambientShadowOffsetY,
                     ambientShadowAlpha
                 )
             drawableSize =
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index fd41cb06..6bfaf5e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -283,17 +283,6 @@
     }
 
     /**
-     * @return whether screen pinning is active.
-     */
-    public boolean isScreenPinningActive() {
-        try {
-            return getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED;
-        } catch (RemoteException e) {
-            return false;
-        }
-    }
-
-    /**
      * @return whether screen pinning is enabled.
      */
     public boolean isScreenPinningEnabled() {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 766266d..65dedc6 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -43,6 +43,7 @@
     public static final String KEY_EXTRA_SYSUI_PROXY = "extra_sysui_proxy";
     public static final String KEY_EXTRA_WINDOW_CORNER_RADIUS = "extra_window_corner_radius";
     public static final String KEY_EXTRA_SUPPORTS_WINDOW_CORNERS = "extra_supports_window_corners";
+    public static final String KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER = "extra_unfold_animation";
     // See ISysuiUnlockAnimationController.aidl
     public static final String KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER = "unlock_animation";
 
@@ -112,7 +113,8 @@
     public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
     // Freeform windows are showing in desktop mode
     public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
-
+    // Device dreaming state
+    public static final int SYSUI_STATE_DEVICE_DREAMING = 1 << 27;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -141,7 +143,8 @@
             SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
             SYSUI_STATE_IMMERSIVE_MODE,
             SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
-            SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE
+            SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
+            SYSUI_STATE_DEVICE_DREAMING
     })
     public @interface SystemUiStateFlags {}
 
@@ -179,6 +182,7 @@
         str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : "");
         str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0
                 ? "freeform_active_in_desktop_mode" : "");
+        str.add((flags & SYSUI_STATE_DEVICE_DREAMING) != 0 ? "device_dreaming" : "");
         return str.toString();
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index 8af934f..dd52cfb 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shared.system;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityTaskManager;
 import android.app.TaskStackListener;
@@ -27,6 +28,8 @@
 import android.util.Log;
 import android.window.TaskSnapshot;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.os.SomeArgs;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -43,15 +46,51 @@
 
     private final Impl mImpl;
 
+    /**
+     * Proxies calls to the given handler callback synchronously for testing purposes.
+     */
+    private static class TestSyncHandler extends Handler {
+        private Handler.Callback mCb;
+
+        public TestSyncHandler() {
+            super(Looper.getMainLooper());
+        }
+
+        public void setCallback(Handler.Callback cb) {
+            mCb = cb;
+        }
+
+        @Override
+        public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
+            return mCb.handleMessage(msg);
+        }
+    }
+
     private TaskStackChangeListeners() {
         mImpl = new Impl(Looper.getMainLooper());
     }
 
+    private TaskStackChangeListeners(Handler h) {
+        mImpl = new Impl(h);
+    }
+
     public static TaskStackChangeListeners getInstance() {
         return INSTANCE;
     }
 
     /**
+     * Returns an instance of the listeners that can be called upon synchronously for testsing
+     * purposes.
+     */
+    @VisibleForTesting
+    public static TaskStackChangeListeners getTestInstance() {
+        TestSyncHandler h = new TestSyncHandler();
+        TaskStackChangeListeners l = new TaskStackChangeListeners(h);
+        h.setCallback(l.mImpl);
+        return l;
+    }
+
+    /**
      * Registers a task stack listener with the system.
      * This should be called on the main thread.
      */
@@ -71,7 +110,15 @@
         }
     }
 
-    private static class Impl extends TaskStackListener implements Handler.Callback {
+    /**
+     * Returns an instance of the listener to call upon from tests.
+     */
+    @VisibleForTesting
+    public TaskStackListener getListenerImpl() {
+        return mImpl;
+    }
+
+    private class Impl extends TaskStackListener implements Handler.Callback {
 
         private static final int ON_TASK_STACK_CHANGED = 1;
         private static final int ON_TASK_SNAPSHOT_CHANGED = 2;
@@ -104,10 +151,14 @@
         private final Handler mHandler;
         private boolean mRegistered;
 
-        Impl(Looper looper) {
+        private Impl(Looper looper) {
             mHandler = new Handler(looper, this);
         }
 
+        private Impl(Handler handler) {
+            mHandler = handler;
+        }
+
         public void addListener(TaskStackChangeListener listener) {
             synchronized (mTaskStackListeners) {
                 mTaskStackListeners.add(listener);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt
index 7f2933e..c9e57b4 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt
@@ -15,21 +15,51 @@
 package com.android.systemui.unfold.system
 
 import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration
+import android.os.Trace
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
 import com.android.systemui.unfold.util.CurrentActivityTypeProvider
 import javax.inject.Inject
 import javax.inject.Singleton
 
 @Singleton
-class ActivityManagerActivityTypeProvider @Inject constructor(
-    private val activityManager: ActivityManager
-) : CurrentActivityTypeProvider {
+class ActivityManagerActivityTypeProvider
+@Inject
+constructor(private val activityManager: ActivityManager) : CurrentActivityTypeProvider {
 
     override val isHomeActivity: Boolean?
-        get() {
-            val activityType = activityManager.getRunningTasks(/* maxNum= */ 1)
-                    ?.getOrNull(0)?.topActivityType ?: return null
+        get() = _isHomeActivity
 
-            return activityType == WindowConfiguration.ACTIVITY_TYPE_HOME
+    private var _isHomeActivity: Boolean? = null
+
+
+    override fun init() {
+        _isHomeActivity = activityManager.isOnHomeActivity()
+        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
+    }
+
+    override fun uninit() {
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+    }
+
+    private val taskStackChangeListener =
+        object : TaskStackChangeListener {
+            override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) {
+                _isHomeActivity = taskInfo.isHomeActivity()
+            }
         }
+
+    private fun RunningTaskInfo.isHomeActivity(): Boolean =
+        topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME
+
+    private fun ActivityManager.isOnHomeActivity(): Boolean? {
+        try {
+            Trace.beginSection("isOnHomeActivity")
+            return getRunningTasks(/* maxNum= */ 1)?.firstOrNull()?.isHomeActivity()
+        } finally {
+            Trace.endSection()
+        }
+    }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt
index 24ae42a..fe607e1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
-import com.android.systemui.unfold.dagger.UnfoldBackground
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
 import com.android.systemui.unfold.dagger.UnfoldMain
 import com.android.systemui.unfold.updates.FoldProvider
 import com.android.systemui.unfold.util.CurrentActivityTypeProvider
@@ -56,6 +56,6 @@
     abstract fun mainHandler(@Main handler: Handler): Handler
 
     @Binds
-    @UnfoldBackground
+    @UnfoldSingleThreadBg
     abstract fun backgroundExecutor(@UiBackground executor: Executor): Executor
 }
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
index 05372fe..31234cf 100644
--- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
@@ -35,7 +35,7 @@
         teamfood: Boolean = false
     ): UnreleasedFlag {
         val flag = UnreleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
-        FlagsFactory.checkForDupesAndAdd(flag)
+        checkForDupesAndAdd(flag)
         return flag
     }
 
@@ -46,7 +46,7 @@
         teamfood: Boolean = false
     ): ReleasedFlag {
         val flag = ReleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
-        FlagsFactory.checkForDupesAndAdd(flag)
+        checkForDupesAndAdd(flag)
         return flag
     }
 
@@ -65,7 +65,7 @@
                 resourceId = resourceId,
                 teamfood = teamfood
             )
-        FlagsFactory.checkForDupesAndAdd(flag)
+        checkForDupesAndAdd(flag)
         return flag
     }
 
@@ -77,18 +77,13 @@
     ): SysPropBooleanFlag {
         val flag =
             SysPropBooleanFlag(id = id, name = name, namespace = "systemui", default = default)
-        FlagsFactory.checkForDupesAndAdd(flag)
+        checkForDupesAndAdd(flag)
         return flag
     }
 
     private fun checkForDupesAndAdd(flag: Flag<*>) {
         if (flagMap.containsKey(flag.name)) {
-            throw IllegalArgumentException("Name {flag.name} is already registered")
-        }
-        flagMap.forEach {
-            if (it.value.id == flag.id) {
-                throw IllegalArgumentException("Name {flag.id} is already registered")
-            }
+            throw IllegalArgumentException("Name {$flag.name} is already registered")
         }
         flagMap[flag.name] = flag
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
index 38fa354..c4f1ce8 100644
--- a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
@@ -16,12 +16,13 @@
 
 package com.android.keyguard
 
-import android.annotation.IntDef
 import android.content.ContentResolver
 import android.database.ContentObserver
 import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT
 import android.net.Uri
 import android.os.Handler
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_UNFOLD_DEVICE
 import android.os.UserHandle
 import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL
 import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO
@@ -29,6 +30,8 @@
 import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT
 import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
 import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_WAKE
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD
 import android.util.Log
 import com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser
 import com.android.systemui.Dumpable
@@ -52,23 +55,44 @@
 
     companion object {
         const val TAG = "ActiveUnlockConfig"
-
-        const val BIOMETRIC_TYPE_NONE = 0
-        const val BIOMETRIC_TYPE_ANY_FACE = 1
-        const val BIOMETRIC_TYPE_ANY_FINGERPRINT = 2
-        const val BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT = 3
     }
 
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(BIOMETRIC_TYPE_NONE, BIOMETRIC_TYPE_ANY_FACE, BIOMETRIC_TYPE_ANY_FINGERPRINT,
-            BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT)
-    annotation class BiometricType
-
     /**
      * Indicates the origin for an active unlock request.
      */
-    enum class ACTIVE_UNLOCK_REQUEST_ORIGIN {
-        WAKE, UNLOCK_INTENT, BIOMETRIC_FAIL, ASSISTANT
+    enum class ActiveUnlockRequestOrigin {
+        /**
+         * Trigger ActiveUnlock on wake ups that'd trigger FaceAuth, see [FaceWakeUpTriggersConfig]
+         */
+        WAKE,
+
+        /**
+         * Trigger ActiveUnlock on unlock intents. This includes the bouncer showing or tapping on
+         * a notification. May also include wakeups: [wakeupsConsideredUnlockIntents].
+         */
+        UNLOCK_INTENT,
+
+        /**
+         * Trigger ActiveUnlock on biometric failures. This may include soft errors depending on
+         * the other settings. See: [faceErrorsToTriggerBiometricFailOn],
+         * [faceAcquireInfoToTriggerBiometricFailOn].
+         */
+        BIOMETRIC_FAIL,
+
+        /**
+         * Trigger ActiveUnlock when the assistant is triggered.
+         */
+        ASSISTANT,
+    }
+
+    /**
+     * Biometric type options.
+     */
+    enum class BiometricType(val intValue: Int) {
+        NONE(0),
+        ANY_FACE(1),
+        ANY_FINGERPRINT(2),
+        UNDER_DISPLAY_FINGERPRINT(3),
     }
 
     var keyguardUpdateMonitor: KeyguardUpdateMonitor? = null
@@ -76,9 +100,11 @@
     private var requestActiveUnlockOnUnlockIntent = false
     private var requestActiveUnlockOnBioFail = false
 
-    private var faceErrorsToTriggerBiometricFailOn = mutableSetOf(FACE_ERROR_TIMEOUT)
+    private var faceErrorsToTriggerBiometricFailOn = mutableSetOf<Int>()
     private var faceAcquireInfoToTriggerBiometricFailOn = mutableSetOf<Int>()
-    private var onUnlockIntentWhenBiometricEnrolled = mutableSetOf<Int>(BIOMETRIC_TYPE_NONE)
+    private var onUnlockIntentWhenBiometricEnrolled = mutableSetOf<Int>()
+    private var wakeupsConsideredUnlockIntents = mutableSetOf<Int>()
+    private var wakeupsToForceDismissKeyguard = mutableSetOf<Int>()
 
     private val settingsObserver = object : ContentObserver(handler) {
         private val wakeUri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE)
@@ -89,16 +115,22 @@
                 secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO)
         private val unlockIntentWhenBiometricEnrolledUri =
                 secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED)
+        private val wakeupsConsideredUnlockIntentsUri =
+            secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS)
+        private val wakeupsToForceDismissKeyguardUri =
+            secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD)
 
         fun register() {
             registerUri(
                     listOf(
-                            wakeUri,
-                            unlockIntentUri,
-                            bioFailUri,
-                            faceErrorsUri,
-                            faceAcquireInfoUri,
-                            unlockIntentWhenBiometricEnrolledUri
+                        wakeUri,
+                        unlockIntentUri,
+                        bioFailUri,
+                        faceErrorsUri,
+                        faceAcquireInfoUri,
+                        unlockIntentWhenBiometricEnrolledUri,
+                        wakeupsConsideredUnlockIntentsUri,
+                        wakeupsToForceDismissKeyguardUri,
                     )
             )
 
@@ -153,7 +185,7 @@
                         secureSettings.getStringForUser(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
                                 getCurrentUser()),
                         faceAcquireInfoToTriggerBiometricFailOn,
-                        setOf<Int>())
+                        emptySet())
             }
 
             if (selfChange || uris.contains(unlockIntentWhenBiometricEnrolledUri)) {
@@ -162,7 +194,25 @@
                                 ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
                                 getCurrentUser()),
                         onUnlockIntentWhenBiometricEnrolled,
-                        setOf(BIOMETRIC_TYPE_NONE))
+                        setOf(BiometricType.NONE.intValue))
+            }
+
+            if (selfChange || uris.contains(wakeupsConsideredUnlockIntentsUri)) {
+                processStringArray(
+                    secureSettings.getStringForUser(
+                        ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+                        getCurrentUser()),
+                    wakeupsConsideredUnlockIntents,
+                    setOf(WAKE_REASON_UNFOLD_DEVICE))
+            }
+
+            if (selfChange || uris.contains(wakeupsToForceDismissKeyguardUri)) {
+                processStringArray(
+                    secureSettings.getStringForUser(
+                        ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD,
+                        getCurrentUser()),
+                    wakeupsToForceDismissKeyguard,
+                    setOf(WAKE_REASON_UNFOLD_DEVICE))
             }
         }
 
@@ -181,10 +231,12 @@
             out.clear()
             stringSetting?.let {
                 for (code: String in stringSetting.split("|")) {
-                    try {
-                        out.add(code.toInt())
-                    } catch (e: NumberFormatException) {
-                        Log.e(TAG, "Passed an invalid setting=$code")
+                    if (code.isNotEmpty()) {
+                        try {
+                            out.add(code.toInt())
+                        } catch (e: NumberFormatException) {
+                            Log.e(TAG, "Passed an invalid setting=$code")
+                        }
                     }
                 }
             } ?: out.addAll(default)
@@ -221,22 +273,38 @@
     }
 
     /**
+     * Whether the PowerManager wake reason is considered an unlock intent and should use origin
+     * [ActiveUnlockRequestOrigin.UNLOCK_INTENT] instead of [ActiveUnlockRequestOrigin.WAKE].
+     */
+    fun isWakeupConsideredUnlockIntent(pmWakeReason: Int): Boolean {
+        return wakeupsConsideredUnlockIntents.contains(pmWakeReason)
+    }
+
+    /**
+     * Whether the PowerManager wake reason should force dismiss the keyguard if active
+     * unlock is successful.
+     */
+    fun shouldWakeupForceDismissKeyguard(pmWakeReason: Int): Boolean {
+        return wakeupsToForceDismissKeyguard.contains(pmWakeReason)
+    }
+
+    /**
      * Whether to trigger active unlock based on where the request is coming from and
      * the current settings.
      */
-    fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ACTIVE_UNLOCK_REQUEST_ORIGIN): Boolean {
+    fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ActiveUnlockRequestOrigin): Boolean {
         return when (requestOrigin) {
-            ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE -> requestActiveUnlockOnWakeup
+            ActiveUnlockRequestOrigin.WAKE -> requestActiveUnlockOnWakeup
 
-            ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT ->
+            ActiveUnlockRequestOrigin.UNLOCK_INTENT ->
                 requestActiveUnlockOnUnlockIntent || requestActiveUnlockOnWakeup ||
                         (shouldRequestActiveUnlockOnUnlockIntentFromBiometricEnrollment())
 
-            ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL ->
+            ActiveUnlockRequestOrigin.BIOMETRIC_FAIL ->
                 requestActiveUnlockOnBioFail || requestActiveUnlockOnUnlockIntent ||
                         requestActiveUnlockOnWakeup
 
-            ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT -> isActiveUnlockEnabled()
+            ActiveUnlockRequestOrigin.ASSISTANT -> isActiveUnlockEnabled()
         }
     }
 
@@ -252,18 +320,18 @@
             val udfpsEnrolled = it.isUdfpsEnrolled
 
             if (!anyFaceEnrolled && !anyFingerprintEnrolled) {
-                return onUnlockIntentWhenBiometricEnrolled.contains(BIOMETRIC_TYPE_NONE)
+                return onUnlockIntentWhenBiometricEnrolled.contains(BiometricType.NONE.intValue)
             }
 
             if (!anyFaceEnrolled && anyFingerprintEnrolled) {
                 return onUnlockIntentWhenBiometricEnrolled.contains(
-                        BIOMETRIC_TYPE_ANY_FINGERPRINT) ||
+                        BiometricType.ANY_FINGERPRINT.intValue) ||
                         (udfpsEnrolled && onUnlockIntentWhenBiometricEnrolled.contains(
-                                BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT))
+                                BiometricType.UNDER_DISPLAY_FINGERPRINT.intValue))
             }
 
             if (!anyFingerprintEnrolled && anyFaceEnrolled) {
-                return onUnlockIntentWhenBiometricEnrolled.contains(BIOMETRIC_TYPE_ANY_FACE)
+                return onUnlockIntentWhenBiometricEnrolled.contains(BiometricType.ANY_FACE.intValue)
             }
         }
 
@@ -275,11 +343,27 @@
         pw.println("   requestActiveUnlockOnWakeup=$requestActiveUnlockOnWakeup")
         pw.println("   requestActiveUnlockOnUnlockIntent=$requestActiveUnlockOnUnlockIntent")
         pw.println("   requestActiveUnlockOnBioFail=$requestActiveUnlockOnBioFail")
+
+        val onUnlockIntentWhenBiometricEnrolledString =
+            onUnlockIntentWhenBiometricEnrolled.map {
+                for (biometricType in BiometricType.values()) {
+                    if (biometricType.intValue == it) {
+                        return@map biometricType.name
+                    }
+                }
+                return@map "UNKNOWN"
+            }
         pw.println("   requestActiveUnlockOnUnlockIntentWhenBiometricEnrolled=" +
-                "$onUnlockIntentWhenBiometricEnrolled")
+                "$onUnlockIntentWhenBiometricEnrolledString")
         pw.println("   requestActiveUnlockOnFaceError=$faceErrorsToTriggerBiometricFailOn")
         pw.println("   requestActiveUnlockOnFaceAcquireInfo=" +
                 "$faceAcquireInfoToTriggerBiometricFailOn")
+        pw.println("   activeUnlockWakeupsConsideredUnlockIntents=${
+            wakeupsConsideredUnlockIntents.map { PowerManager.wakeReasonToString(it) }
+        }")
+        pw.println("   activeUnlockFromWakeupsToAlwaysDismissKeyguard=${
+            wakeupsToForceDismissKeyguard.map { PowerManager.wakeReasonToString(it) }
+        }")
 
         pw.println("Current state:")
         keyguardUpdateMonitor?.let {
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 8f38e58..92ee373 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -24,6 +24,8 @@
 import android.text.format.DateFormat
 import android.util.TypedValue
 import android.view.View
+import android.view.ViewTreeObserver
+import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
@@ -38,14 +40,19 @@
 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.KeyguardClockLog
+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.plugins.WeatherData
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
 import com.android.systemui.statusbar.policy.ConfigurationController
-import java.io.PrintWriter
+import com.android.systemui.util.concurrency.DelayableExecutor
 import java.util.Locale
 import java.util.TimeZone
 import java.util.concurrent.Executor
@@ -53,7 +60,6 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
@@ -62,7 +68,9 @@
  * 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,
@@ -71,22 +79,31 @@
     private val configurationController: ConfigurationController,
     @Main private val resources: Resources,
     private val context: Context,
-    @Main private val mainExecutor: Executor,
+    @Main private val mainExecutor: DelayableExecutor,
     @Background private val bgExecutor: Executor,
-    @KeyguardClockLog private val logBuffer: LogBuffer?,
+    @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
+    @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
     private val featureFlags: FeatureFlags
 ) {
     var clock: ClockController? = null
         set(value) {
             field = value
             if (value != null) {
-                if (logBuffer != null) {
-                    value.setLogBuffer(logBuffer)
-                }
+                smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
+                value.smallClock.logBuffer = smallLogBuffer
+                largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
+                value.largeClock.logBuffer = largeLogBuffer
 
                 value.initialize(resources, dozeAmount, 0f)
-                updateRegionSamplers(value)
+
+                if (regionSamplingEnabled) {
+                    clock?.smallClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+                    clock?.largeClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+                } else {
+                    updateColors()
+                }
                 updateFontSizes()
+                updateTimeListeners()
             }
         }
 
@@ -100,120 +117,184 @@
     private var disposableHandle: DisposableHandle? = null
     private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
 
-    private fun updateColors() {
+    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)
 
-        if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
-            val wallpaperManager = WallpaperManager.getInstance(context)
-            if (!wallpaperManager.lockScreenWallpaperExists()) {
-                smallClockIsDark = smallRegionSampler!!.currentRegionDarkness().isDark
-                largeClockIsDark = largeRegionSampler!!.currentRegionDarkness().isDark
+            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
+                }
+
+                // 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)
+                    }
+                }
             }
-        } else {
-            val isLightTheme = TypedValue()
-            context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
-            smallClockIsDark = isLightTheme.data == 0
-            largeClockIsDark = isLightTheme.data == 0
         }
 
+    private fun updateColors() {
+        val wallpaperManager = WallpaperManager.getInstance(context)
+        if (regionSamplingEnabled && !wallpaperManager.lockScreenWallpaperExists()) {
+            if (regionSampler != null) {
+                if (regionSampler?.sampledView == clock?.smallClock?.view) {
+                    smallClockIsDark = regionSampler!!.currentRegionDarkness().isDark
+                    clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark)
+                    return
+                } else if (regionSampler?.sampledView == clock?.largeClock?.view) {
+                    largeClockIsDark = regionSampler!!.currentRegionDarkness().isDark
+                    clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark)
+                    return
+                }
+            }
+        }
+
+        val isLightTheme = TypedValue()
+        context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
+        smallClockIsDark = isLightTheme.data == 0
+        largeClockIsDark = isLightTheme.data == 0
+
         clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark)
         clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark)
     }
 
-    private fun updateRegionSamplers(currentClock: ClockController?) {
-        smallRegionSampler?.stopRegionSampler()
-        largeRegionSampler?.stopRegionSampler()
-
-        smallRegionSampler = createRegionSampler(
-                currentClock?.smallClock?.view,
-                mainExecutor,
-                bgExecutor,
-                regionSamplingEnabled,
-                ::updateColors
-        )
-
-        largeRegionSampler = createRegionSampler(
-                currentClock?.largeClock?.view,
-                mainExecutor,
-                bgExecutor,
-                regionSamplingEnabled,
-                ::updateColors
-        )
-
-        smallRegionSampler!!.startRegionSampler()
-        largeRegionSampler!!.startRegionSampler()
+    private fun updateRegionSampler(sampledRegion: View) {
+        regionSampler?.stopRegionSampler()
+        regionSampler =
+            createRegionSampler(
+                    sampledRegion,
+                    mainExecutor,
+                    bgExecutor,
+                    regionSamplingEnabled,
+                    ::updateColors
+                )
+                ?.apply { startRegionSampler() }
 
         updateColors()
     }
 
     protected open fun createRegionSampler(
-            sampledView: View?,
-            mainExecutor: Executor?,
-            bgExecutor: Executor?,
-            regionSamplingEnabled: Boolean,
-            updateColors: () -> Unit
-    ): RegionSampler {
+        sampledView: View?,
+        mainExecutor: Executor?,
+        bgExecutor: Executor?,
+        regionSamplingEnabled: Boolean,
+        updateColors: () -> Unit
+    ): RegionSampler? {
         return RegionSampler(
             sampledView,
             mainExecutor,
             bgExecutor,
             regionSamplingEnabled,
-            updateColors)
+            updateColors
+        )
     }
 
-    var smallRegionSampler: RegionSampler? = null
-    var largeRegionSampler: RegionSampler? = null
+    var regionSampler: RegionSampler? = null
+    var smallTimeListener: TimeListener? = null
+    var largeTimeListener: TimeListener? = null
+    val shouldTimeListenerRun: Boolean
+        get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
 
     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
             }
         }
 
-        override fun onTimeFormatChanged(timeFormat: String) {
-            clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+    private val localeBroadcastReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                clock?.events?.onLocaleChanged(Locale.getDefault())
+            }
         }
 
-        override fun onTimeZoneChanged(timeZone: TimeZone) {
-            clock?.events?.onTimeZoneChanged(timeZone)
-        }
+    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)
+                    }
+                }
 
-        override fun onUserSwitchComplete(userId: Int) {
-            clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                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: WeatherData) {
+                clock?.events?.onWeatherDataChanged(data)
+            }
         }
-    }
 
     fun registerListeners(parent: View) {
         if (isRegistered) {
@@ -228,19 +309,20 @@
         configurationController.addCallback(configListener)
         batteryController.addCallback(batteryCallback)
         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        smallRegionSampler?.startRegionSampler()
-        largeRegionSampler?.startRegionSampler()
-        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)
     }
 
     fun unregisterListeners() {
@@ -254,75 +336,141 @@
         configurationController.removeCallback(configListener)
         batteryController.removeCallback(batteryCallback)
         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
-        smallRegionSampler?.stopRegionSampler()
-        largeRegionSampler?.stopRegionSampler()
+        regionSampler?.stopRegionSampler()
+        smallTimeListener?.stop()
+        largeTimeListener?.stop()
+    }
+
+    private fun updateTimeListeners() {
+        smallTimeListener?.stop()
+        largeTimeListener?.stop()
+
+        smallTimeListener = null
+        largeTimeListener = null
+
+        clock?.smallClock?.let {
+            smallTimeListener = TimeListener(it, mainExecutor)
+            smallTimeListener?.update(shouldTimeListenerRun)
+        }
+        clock?.largeClock?.let {
+            largeTimeListener = TimeListener(it, mainExecutor)
+            largeTimeListener?.update(shouldTimeListenerRun)
+        }
     }
 
     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()
+            )
     }
 
-    /**
-     * Dump information for debugging
-     */
-    fun dump(pw: PrintWriter) {
-        pw.println(this)
-        clock?.dump(pw)
-        smallRegionSampler?.dump(pw)
-        largeRegionSampler?.dump(pw)
+    private fun handleDoze(doze: Float) {
+        dozeAmount = doze
+        clock?.animations?.doze(dozeAmount)
+        smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
+        largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
     }
 
     @VisibleForTesting
     internal fun listenForDozeAmount(scope: CoroutineScope): Job {
-        return scope.launch {
-            keyguardInteractor.dozeAmount.collect {
-                dozeAmount = it
-                clock?.animations?.doze(dozeAmount)
-            }
-        }
+        return scope.launch { keyguardInteractor.dozeAmount.collect { handleDoze(it) } }
     }
 
     @VisibleForTesting
     internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
         return scope.launch {
-            keyguardTransitionInteractor.dozeAmountTransition.collect {
-                dozeAmount = it.value
-                clock?.animations?.doze(dozeAmount)
-            }
+            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 {
-                dozeAmount = 1f
-                clock?.animations?.doze(dozeAmount)
-            }
+            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
+            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
             }
-            .collect { localIsDozing ->
-                isDozing = localIsDozing
+
+        val secondsRunnable =
+            object : Runnable {
+                override fun run() {
+                    if (!isRunning) {
+                        return
+                    }
+
+                    executor.executeDelayed(this, 990)
+                    clockFace.events.onTimeTick()
+                }
+            }
+
+        var isRunning: Boolean = false
+            private set
+
+        fun start() {
+            if (isRunning) {
+                return
+            }
+
+            isRunning = true
+            when (clockFace.events.tickRate) {
+                ClockTickRate.PER_MINUTE -> {
+                    /* Handled by KeyguardClockSwitchController */
+                }
+                ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
+                ClockTickRate.PER_FRAME -> {
+                    clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
+                    clockFace.view.invalidate()
+                }
             }
         }
+
+        fun stop() {
+            if (!isRunning) {
+                return
+            }
+
+            isRunning = false
+            clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
+        }
+
+        fun update(shouldRun: Boolean) = if (shouldRun) start() else stop()
+    }
+
+    companion object {
+        private val TAG = ClockEventController::class.simpleName!!
+        private val DOZE_TICKRATE_THRESHOLD = 0.99f
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java
index a25b281..c5a06b4 100644
--- a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java
+++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java
@@ -24,7 +24,6 @@
 import android.widget.Button;
 
 import com.android.internal.util.EmergencyAffordanceManager;
-import com.android.internal.widget.LockPatternUtils;
 
 /**
  * This class implements a smart emergency button that updates itself based
@@ -40,8 +39,6 @@
     private int mDownY;
     private boolean mLongPressWasDragged;
 
-    private LockPatternUtils mLockPatternUtils;
-
     private final boolean mEnableEmergencyCallWhileSimLocked;
 
     public EmergencyButton(Context context) {
@@ -58,7 +55,6 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mLockPatternUtils = new LockPatternUtils(mContext);
         if (mEmergencyAffordanceManager.needsEmergencyAffordance()) {
             setOnLongClickListener(v -> {
                 if (!mLongPressWasDragged
@@ -95,7 +91,8 @@
         return super.performLongClick();
     }
 
-    void updateEmergencyCallButton(boolean isInCall, boolean hasTelephonyRadio, boolean simLocked) {
+    void updateEmergencyCallButton(boolean isInCall, boolean hasTelephonyRadio, boolean simLocked,
+            boolean isSecure) {
         boolean visible = false;
         if (hasTelephonyRadio) {
             // Emergency calling requires a telephony radio.
@@ -107,7 +104,7 @@
                     visible = mEnableEmergencyCallWhileSimLocked;
                 } else {
                     // Only show if there is a secure screen (pin/pattern/SIM pin/SIM puk);
-                    visible = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser());
+                    visible = isSecure;
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java
index ea808eb..f7e8eb4 100644
--- a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java
+++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java
@@ -18,6 +18,7 @@
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 
+import android.annotation.SuppressLint;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.content.Intent;
@@ -31,16 +32,22 @@
 import android.util.Log;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.dagger.KeyguardBouncerScope;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.util.EmergencyDialerConstants;
 import com.android.systemui.util.ViewController;
 
+import java.util.concurrent.Executor;
+
 import javax.inject.Inject;
 
 /** View Controller for {@link com.android.keyguard.EmergencyButton}. */
@@ -57,6 +64,9 @@
     private final MetricsLogger mMetricsLogger;
 
     private EmergencyButtonCallback mEmergencyButtonCallback;
+    private LockPatternUtils mLockPatternUtils;
+    private Executor mMainExecutor;
+    private Executor mBackgroundExecutor;
 
     private final KeyguardUpdateMonitorCallback mInfoCallback =
             new KeyguardUpdateMonitorCallback() {
@@ -78,12 +88,15 @@
         }
     };
 
-    private EmergencyButtonController(@Nullable EmergencyButton view,
+    @VisibleForTesting
+    public EmergencyButtonController(@Nullable EmergencyButton view,
             ConfigurationController configurationController,
             KeyguardUpdateMonitor keyguardUpdateMonitor, TelephonyManager telephonyManager,
             PowerManager powerManager, ActivityTaskManager activityTaskManager,
             ShadeController shadeController,
-            @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger) {
+            @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger,
+            LockPatternUtils lockPatternUtils,
+            Executor mainExecutor, Executor backgroundExecutor) {
         super(view);
         mConfigurationController = configurationController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -93,6 +106,9 @@
         mShadeController = shadeController;
         mTelecomManager = telecomManager;
         mMetricsLogger = metricsLogger;
+        mLockPatternUtils = lockPatternUtils;
+        mMainExecutor = mainExecutor;
+        mBackgroundExecutor = backgroundExecutor;
     }
 
     @Override
@@ -113,13 +129,27 @@
         mConfigurationController.removeCallback(mConfigurationListener);
     }
 
-    private void updateEmergencyCallButton() {
+    /**
+     * Updates the visibility of the emergency button.
+     *
+     * This method runs binder calls in a background thread.
+     */
+    @VisibleForTesting
+    @SuppressLint("MissingPermission")
+    public void updateEmergencyCallButton() {
         if (mView != null) {
-            mView.updateEmergencyCallButton(
-                    mTelecomManager != null && mTelecomManager.isInCall(),
-                    getContext().getPackageManager().hasSystemFeature(
-                            PackageManager.FEATURE_TELEPHONY),
-                    mKeyguardUpdateMonitor.isSimPinVoiceSecure());
+            // Run in bg thread to avoid throttling the main thread with binder call.
+            mBackgroundExecutor.execute(() -> {
+                boolean isInCall = mTelecomManager != null && mTelecomManager.isInCall();
+                boolean isSecure = mLockPatternUtils
+                        .isSecure(KeyguardUpdateMonitor.getCurrentUser());
+                mMainExecutor.execute(() -> mView.updateEmergencyCallButton(
+                        /* isInCall= */ isInCall,
+                        /* hasTelephonyRadio= */ getContext().getPackageManager()
+                                .hasSystemFeature(PackageManager.FEATURE_TELEPHONY),
+                        /* simLocked= */ mKeyguardUpdateMonitor.isSimPinVoiceSecure(),
+                        /* isSecure= */ isSecure));
+            });
         }
     }
 
@@ -129,6 +159,7 @@
     /**
      * Shows the emergency dialer or returns the user to the existing call.
      */
+    @SuppressLint("MissingPermission")
     public void takeEmergencyCallAction() {
         mMetricsLogger.action(MetricsEvent.ACTION_EMERGENCY_CALL);
         if (mPowerManager != null) {
@@ -136,29 +167,35 @@
         }
         mActivityTaskManager.stopSystemLockTaskMode();
         mShadeController.collapseShade(false);
-        if (mTelecomManager != null && mTelecomManager.isInCall()) {
-            mTelecomManager.showInCallScreen(false);
-            if (mEmergencyButtonCallback != null) {
-                mEmergencyButtonCallback.onEmergencyButtonClickedWhenInCall();
-            }
-        } else {
-            mKeyguardUpdateMonitor.reportEmergencyCallAction(true /* bypassHandler */);
-            if (mTelecomManager == null) {
-                Log.wtf(LOG_TAG, "TelecomManager was null, cannot launch emergency dialer");
-                return;
-            }
-            Intent emergencyDialIntent =
-                    mTelecomManager.createLaunchEmergencyDialerIntent(null /* number*/)
-                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
-                                    | Intent.FLAG_ACTIVITY_CLEAR_TOP)
-                            .putExtra(EmergencyDialerConstants.EXTRA_ENTRY_TYPE,
-                                    EmergencyDialerConstants.ENTRY_TYPE_LOCKSCREEN_BUTTON);
+        // Run in bg thread to avoid throttling the main thread with binder call.
+        mBackgroundExecutor.execute(() -> {
+            boolean isInCall = mTelecomManager != null && mTelecomManager.isInCall();
+            mMainExecutor.execute(() -> {
+                if (isInCall) {
+                    mTelecomManager.showInCallScreen(false);
+                    if (mEmergencyButtonCallback != null) {
+                        mEmergencyButtonCallback.onEmergencyButtonClickedWhenInCall();
+                    }
+                } else {
+                    mKeyguardUpdateMonitor.reportEmergencyCallAction(true /* bypassHandler */);
+                    if (mTelecomManager == null) {
+                        Log.wtf(LOG_TAG, "TelecomManager was null, cannot launch emergency dialer");
+                        return;
+                    }
+                    Intent emergencyDialIntent =
+                            mTelecomManager.createLaunchEmergencyDialerIntent(null /* number*/)
+                                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                                            | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+                                            | Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                                    .putExtra(EmergencyDialerConstants.EXTRA_ENTRY_TYPE,
+                                            EmergencyDialerConstants.ENTRY_TYPE_LOCKSCREEN_BUTTON);
 
-            getContext().startActivityAsUser(emergencyDialIntent,
-                    ActivityOptions.makeCustomAnimation(getContext(), 0, 0).toBundle(),
-                    new UserHandle(KeyguardUpdateMonitor.getCurrentUser()));
-        }
+                    getContext().startActivityAsUser(emergencyDialIntent,
+                            ActivityOptions.makeCustomAnimation(getContext(), 0, 0).toBundle(),
+                            new UserHandle(KeyguardUpdateMonitor.getCurrentUser()));
+                }
+            });
+        });
     }
 
     /** */
@@ -178,13 +215,19 @@
         @Nullable
         private final TelecomManager mTelecomManager;
         private final MetricsLogger mMetricsLogger;
+        private final LockPatternUtils mLockPatternUtils;
+        private final Executor mMainExecutor;
+        private final Executor mBackgroundExecutor;
 
         @Inject
         public Factory(ConfigurationController configurationController,
                 KeyguardUpdateMonitor keyguardUpdateMonitor, TelephonyManager telephonyManager,
                 PowerManager powerManager, ActivityTaskManager activityTaskManager,
                 ShadeController shadeController,
-                @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger) {
+                @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger,
+                LockPatternUtils lockPatternUtils,
+                @Main Executor mainExecutor,
+                @Background Executor backgroundExecutor) {
 
             mConfigurationController = configurationController;
             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -194,14 +237,17 @@
             mShadeController = shadeController;
             mTelecomManager = telecomManager;
             mMetricsLogger = metricsLogger;
+            mLockPatternUtils = lockPatternUtils;
+            mMainExecutor = mainExecutor;
+            mBackgroundExecutor = backgroundExecutor;
         }
 
         /** Construct an {@link com.android.keyguard.EmergencyButtonController}. */
         public EmergencyButtonController create(EmergencyButton view) {
             return new EmergencyButtonController(view, mConfigurationController,
                     mKeyguardUpdateMonitor, mTelephonyManager, mPowerManager, mActivityTaskManager,
-                    mShadeController,
-                    mTelecomManager, mMetricsLogger);
+                    mShadeController, mTelecomManager, mMetricsLogger, mLockPatternUtils,
+                    mMainExecutor, mBackgroundExecutor);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index 5bb9367..e0cf7b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -50,6 +50,7 @@
 import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED
 import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED
 import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED
+import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED
 import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN
 import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN
 import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE
@@ -126,6 +127,7 @@
     const val STRONG_AUTH_ALLOWED_CHANGED = "Face auth stopped because strong auth allowed changed"
     const val NON_STRONG_BIOMETRIC_ALLOWED_CHANGED =
         "Face auth stopped because non strong biometric allowed changed"
+    const val POSTURE_CHANGED = "Face auth started/stopped due to device posture changed."
 }
 
 /**
@@ -173,6 +175,7 @@
             return PowerManager.wakeReasonToString(extraInfo)
         }
     },
+    @UiEvent(doc = POSTURE_CHANGED) FACE_AUTH_UPDATED_POSTURE_CHANGED(1265, POSTURE_CHANGED),
     @Deprecated(
         "Not a face auth trigger.",
         ReplaceWith(
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
index a0c43fb..788a66d 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
@@ -29,7 +29,7 @@
 import java.util.stream.Collectors
 import javax.inject.Inject
 
-/** Determines which device wake-ups should trigger face authentication. */
+/** Determines which device wake-ups should trigger passive authentication. */
 @SysUISingleton
 class FaceWakeUpTriggersConfig
 @Inject
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index 7da27b1..baaef19 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -103,6 +103,7 @@
 
     @Override
     public void reset() {
+        super.reset();
         // start fresh
         mDismissing = false;
         mView.resetPasswordText(false /* animate */, false /* announce */);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 62babad..0326b6d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -7,7 +7,6 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -20,11 +19,15 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.plugins.ClockController;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
+import kotlin.Unit;
+
 /**
  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
  */
@@ -87,6 +90,7 @@
     private int mClockSwitchYAmount;
     @VisibleForTesting boolean mChildrenAreLaidOut = false;
     @VisibleForTesting boolean mAnimateOnLayout = true;
+    private LogBuffer mLogBuffer = null;
 
     public KeyguardClockSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -113,6 +117,14 @@
         onDensityOrFontScaleChanged();
     }
 
+    public void setLogBuffer(LogBuffer logBuffer) {
+        mLogBuffer = logBuffer;
+    }
+
+    public LogBuffer getLogBuffer() {
+        return mLogBuffer;
+    }
+
     void setClock(ClockController clock, int statusBarState) {
         mClock = clock;
 
@@ -121,12 +133,16 @@
         mLargeClockFrame.removeAllViews();
 
         if (clock == null) {
-            Log.e(TAG, "No clock being shown");
+            if (mLogBuffer != null) {
+                mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown");
+            }
             return;
         }
 
         // Attach small and big clock views to hierarchy.
-        Log.i(TAG, "Attached new clock views to switch");
+        if (mLogBuffer != null) {
+            mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch");
+        }
         mSmallClockFrame.addView(clock.getSmallClock().getView());
         mLargeClockFrame.addView(clock.getLargeClock().getView());
         updateClockTargetRegions();
@@ -152,8 +168,18 @@
     }
 
     private void updateClockViews(boolean useLargeClock, boolean animate) {
-        Log.i(TAG, "updateClockViews; useLargeClock=" + useLargeClock + "; animate=" + animate
-                + "; mChildrenAreLaidOut=" + mChildrenAreLaidOut);
+        if (mLogBuffer != null) {
+            mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> {
+                msg.setBool1(useLargeClock);
+                msg.setBool2(animate);
+                msg.setBool3(mChildrenAreLaidOut);
+                return Unit.INSTANCE;
+            }, (msg) -> "updateClockViews"
+                    + "; useLargeClock=" + msg.getBool1()
+                    + "; animate=" + msg.getBool2()
+                    + "; mChildrenAreLaidOut=" + msg.getBool3());
+        }
+
         if (mClockInAnim != null) mClockInAnim.cancel();
         if (mClockOutAnim != null) mClockOutAnim.cancel();
         if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
@@ -269,7 +295,9 @@
     public void dump(PrintWriter pw, String[] args) {
         pw.println("KeyguardClockSwitch:");
         pw.println("  mSmallClockFrame: " + mSmallClockFrame);
+        pw.println("  mSmallClockFrame.alpha: " + mSmallClockFrame.getAlpha());
         pw.println("  mLargeClockFrame: " + mLargeClockFrame);
+        pw.println("  mLargeClockFrame.alpha: " + mLargeClockFrame.getAlpha());
         pw.println("  mStatusArea: " + mStatusArea);
         pw.println("  mDisplayedClockSize: " + mDisplayedClockSize);
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 788f120..b85b2b8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -38,10 +38,14 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.log.dagger.KeyguardClockLog;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.ClockRegistry;
+import com.android.systemui.shared.regionsampling.RegionSampler;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -62,6 +66,8 @@
  */
 public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch>
         implements Dumpable {
+    private static final String TAG = "KeyguardClockSwitchController";
+
     private final StatusBarStateController mStatusBarStateController;
     private final ClockRegistry mClockRegistry;
     private final KeyguardSliceViewController mKeyguardSliceViewController;
@@ -70,6 +76,7 @@
     private final SecureSettings mSecureSettings;
     private final DumpManager mDumpManager;
     private final ClockEventController mClockEventController;
+    private final LogBuffer mLogBuffer;
 
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
@@ -82,7 +89,10 @@
     private final ClockRegistry.ClockChangeListener mClockChangedListener;
 
     private ViewGroup mStatusArea;
-    // If set will replace keyguard_slice_view
+
+    // If the SMARTSPACE flag is set, keyguard_slice_view is replaced by the following views.
+    private ViewGroup mDateWeatherView;
+    private View mWeatherView;
     private View mSmartspaceView;
 
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
@@ -96,6 +106,12 @@
             updateDoubleLineClock();
         }
     };
+    private final ContentObserver mShowWeatherObserver = new ContentObserver(null) {
+        @Override
+        public void onChange(boolean change) {
+            setWeatherVisibility();
+        }
+    };
 
     private final KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener
             mKeyguardUnlockAnimationListener =
@@ -119,7 +135,8 @@
             SecureSettings secureSettings,
             @Main Executor uiExecutor,
             DumpManager dumpManager,
-            ClockEventController clockEventController) {
+            ClockEventController clockEventController,
+            @KeyguardClockLog LogBuffer logBuffer) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mClockRegistry = clockRegistry;
@@ -131,9 +148,16 @@
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
+        mLogBuffer = logBuffer;
+        mView.setLogBuffer(mLogBuffer);
 
-        mClockChangedListener = () -> {
-            setClock(mClockRegistry.createCurrentClock());
+        mClockChangedListener = new ClockRegistry.ClockChangeListener() {
+            @Override
+            public void onCurrentClockChanged() {
+                setClock(mClockRegistry.createCurrentClock());
+            }
+            @Override
+            public void onAvailableClocksChanged() { }
         };
     }
 
@@ -183,20 +207,35 @@
 
         if (mSmartspaceController.isEnabled()) {
             View ksv = mView.findViewById(R.id.keyguard_slice_view);
-            int ksvIndex = mStatusArea.indexOfChild(ksv);
+            int viewIndex = mStatusArea.indexOfChild(ksv);
             ksv.setVisibility(View.GONE);
 
-            addSmartspaceView(ksvIndex);
+            // TODO(b/261757708): add content observer for the Settings toggle and add/remove
+            //  weather according to the Settings.
+            if (mSmartspaceController.isDateWeatherDecoupled()) {
+                addDateWeatherView(viewIndex);
+                viewIndex += 1;
+            }
+
+            addSmartspaceView(viewIndex);
         }
 
         mSecureSettings.registerContentObserverForUser(
-                Settings.Secure.getUriFor(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+                Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
                 false, /* notifyForDescendants */
                 mDoubleLineClockObserver,
                 UserHandle.USER_ALL
         );
 
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED,
+                false, /* notifyForDescendants */
+                mShowWeatherObserver,
+                UserHandle.USER_ALL
+        );
+
         updateDoubleLineClock();
+        setWeatherVisibility();
 
         mKeyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
                 mKeyguardUnlockAnimationListener);
@@ -220,6 +259,14 @@
 
     void onLocaleListChanged() {
         if (mSmartspaceController.isEnabled()) {
+            if (mSmartspaceController.isDateWeatherDecoupled()) {
+                mDateWeatherView.removeView(mWeatherView);
+                int index = mStatusArea.indexOfChild(mDateWeatherView);
+                if (index >= 0) {
+                    mStatusArea.removeView(mDateWeatherView);
+                    addDateWeatherView(index);
+                }
+            }
             int index = mStatusArea.indexOfChild(mSmartspaceView);
             if (index >= 0) {
                 mStatusArea.removeView(mSmartspaceView);
@@ -228,6 +275,30 @@
         }
     }
 
+    private void addDateWeatherView(int index) {
+        mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                MATCH_PARENT, WRAP_CONTENT);
+        mStatusArea.addView(mDateWeatherView, index, lp);
+        int startPadding = getContext().getResources().getDimensionPixelSize(
+                R.dimen.below_clock_padding_start);
+        int endPadding = getContext().getResources().getDimensionPixelSize(
+                R.dimen.below_clock_padding_end);
+        mDateWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+        addWeatherView();
+    }
+
+    private void addWeatherView() {
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                WRAP_CONTENT, WRAP_CONTENT);
+        mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+        // Place weather right after the date, before the extras
+        final int index = mDateWeatherView.getChildCount() == 0 ? 0 : 1;
+        mDateWeatherView.addView(mWeatherView, index, lp);
+        mWeatherView.setPaddingRelative(0, 0, 4, 0);
+    }
+
     private void addSmartspaceView(int index) {
         mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
         LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
@@ -292,7 +363,8 @@
         }
         ClockController clock = getClock();
         if (clock != null) {
-            clock.getEvents().onTimeTick();
+            clock.getSmallClock().getEvents().onTimeTick();
+            clock.getLargeClock().getEvents().onTimeTick();
         }
     }
 
@@ -378,6 +450,10 @@
     }
 
     private void setClock(ClockController clock) {
+        if (clock != null && mLogBuffer != null) {
+            mLogBuffer.log(TAG, LogLevel.INFO, "New Clock");
+        }
+
         mClockEventController.setClock(clock);
         mView.setClock(clock, mStatusBarStateController.getState());
     }
@@ -400,6 +476,14 @@
         }
     }
 
+    private void setWeatherVisibility() {
+        if (mWeatherView != null) {
+            mUiExecutor.execute(
+                    () -> mWeatherView.setVisibility(
+                        mSmartspaceController.isWeatherEnabled() ? View.VISIBLE : View.GONE));
+        }
+    }
+
     /**
      * Sets the clipChildren property on relevant views, to allow the smartspace to draw out of
      * bounds during the unlock transition.
@@ -419,6 +503,10 @@
         if (clock != null) {
             clock.dump(pw);
         }
+        final RegionSampler regionSampler = mClockEventController.getRegionSampler();
+        if (regionSampler != null) {
+            regionSampler.dump(pw);
+        }
     }
 
     /** Gets the animations for the current clock. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index 02776a2..ec8fa92 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -15,8 +15,6 @@
  */
 package com.android.keyguard;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import android.app.Presentation;
 import android.content.Context;
 import android.graphics.Color;
@@ -37,9 +35,11 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationBarView;
+import com.android.systemui.settings.DisplayTracker;
 
 import java.util.concurrent.Executor;
 
@@ -53,6 +53,7 @@
 
     private MediaRouter mMediaRouter = null;
     private final DisplayManager mDisplayService;
+    private final DisplayTracker mDisplayTracker;
     private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
     private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
     private final Context mContext;
@@ -62,46 +63,43 @@
 
     private final SparseArray<Presentation> mPresentations = new SparseArray<>();
 
-    private final DisplayManager.DisplayListener mDisplayListener =
-            new DisplayManager.DisplayListener() {
+    private final DisplayTracker.Callback mDisplayCallback =
+            new DisplayTracker.Callback() {
+                @Override
+                public void onDisplayAdded(int displayId) {
+                    Trace.beginSection(
+                            "KeyguardDisplayManager#onDisplayAdded(displayId=" + displayId + ")");
+                    final Display display = mDisplayService.getDisplay(displayId);
+                    if (mShowing) {
+                        updateNavigationBarVisibility(displayId, false /* navBarVisible */);
+                        showPresentation(display);
+                    }
+                    Trace.endSection();
+                }
 
-        @Override
-        public void onDisplayAdded(int displayId) {
-            Trace.beginSection(
-                    "KeyguardDisplayManager#onDisplayAdded(displayId=" + displayId + ")");
-            final Display display = mDisplayService.getDisplay(displayId);
-            if (mShowing) {
-                updateNavigationBarVisibility(displayId, false /* navBarVisible */);
-                showPresentation(display);
-            }
-            Trace.endSection();
-        }
-
-        @Override
-        public void onDisplayChanged(int displayId) {
-
-        }
-
-        @Override
-        public void onDisplayRemoved(int displayId) {
-            Trace.beginSection(
-                    "KeyguardDisplayManager#onDisplayRemoved(displayId=" + displayId + ")");
-            hidePresentation(displayId);
-            Trace.endSection();
-        }
-    };
+                @Override
+                public void onDisplayRemoved(int displayId) {
+                    Trace.beginSection(
+                            "KeyguardDisplayManager#onDisplayRemoved(displayId=" + displayId + ")");
+                    hidePresentation(displayId);
+                    Trace.endSection();
+                }
+            };
 
     @Inject
     public KeyguardDisplayManager(Context context,
             Lazy<NavigationBarController> navigationBarControllerLazy,
             KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
+            DisplayTracker displayTracker,
+            @Main Executor mainExecutor,
             @UiBackground Executor uiBgExecutor) {
         mContext = context;
         mNavigationBarControllerLazy = navigationBarControllerLazy;
         mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
         uiBgExecutor.execute(() -> mMediaRouter = mContext.getSystemService(MediaRouter.class));
         mDisplayService = mContext.getSystemService(DisplayManager.class);
-        mDisplayService.registerDisplayListener(mDisplayListener, null /* handler */);
+        mDisplayTracker = displayTracker;
+        mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor);
     }
 
     private boolean isKeyguardShowable(Display display) {
@@ -109,7 +107,7 @@
             if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
             return false;
         }
-        if (display.getDisplayId() == DEFAULT_DISPLAY) {
+        if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
             if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
             return false;
         }
@@ -224,7 +222,7 @@
     protected boolean updateDisplays(boolean showing) {
         boolean changed = false;
         if (showing) {
-            final Display[] displays = mDisplayService.getDisplays();
+            final Display[] displays = mDisplayTracker.getAllDisplays();
             for (Display display : displays) {
                 int displayId = display.getDisplayId();
                 updateNavigationBarVisibility(displayId, false /* navBarVisible */);
@@ -247,7 +245,7 @@
     //  term solution in R.
     private void updateNavigationBarVisibility(int displayId, boolean navBarVisible) {
         // Leave this task to {@link StatusBarKeyguardViewManager}
-        if (displayId == DEFAULT_DISPLAY) return;
+        if (displayId == mDisplayTracker.getDefaultDisplayId()) return;
 
         NavigationBarView navBarView = mNavigationBarControllerLazy.get()
                 .getNavigationBarView(displayId);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
index deead19..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,
@@ -39,11 +40,11 @@
     var keyguardGoingAway: Boolean = false,
     var listeningForFaceAssistant: Boolean = false,
     var occludingAppRequestingFaceAuth: Boolean = false,
+    val postureAllowsListening: Boolean = false,
     var primaryUser: Boolean = false,
     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() {
@@ -72,7 +73,7 @@
             secureCameraLaunched.toString(),
             supportsDetect.toString(),
             switchingUser.toString(),
-            udfpsBouncerShowing.toString(),
+            alternateBouncerShowing.toString(),
             udfpsFingerDown.toString(),
             userNotTrustedOrDetectionIsNeeded.toString(),
         )
@@ -94,6 +95,7 @@
                 userId = model.userId
                 listening = model.listening
                 // keep sorted
+                alternateBouncerShowing = model.alternateBouncerShowing
                 biometricSettingEnabledForUser = model.biometricSettingEnabledForUser
                 bouncerFullyShown = model.bouncerFullyShown
                 faceAndFpNotAuthenticated = model.faceAndFpNotAuthenticated
@@ -110,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/KeyguardFingerprintListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
index 998dc09..57130ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
@@ -27,6 +27,7 @@
     override var userId: Int = 0,
     override var listening: Boolean = false,
     // keepSorted
+    var alternateBouncerShowing: Boolean = false,
     var biometricEnabledForUser: Boolean = false,
     var bouncerIsOrWillShow: Boolean = false,
     var canSkipBouncer: Boolean = false,
@@ -57,6 +58,7 @@
             userId.toString(),
             listening.toString(),
             // keep sorted
+            alternateBouncerShowing.toString(),
             biometricEnabledForUser.toString(),
             bouncerIsOrWillShow.toString(),
             canSkipBouncer.toString(),
@@ -96,6 +98,7 @@
                 userId = model.userId
                 listening = model.listening
                 // keep sorted
+                alternateBouncerShowing = model.alternateBouncerShowing
                 biometricEnabledForUser = model.biometricEnabledForUser
                 bouncerIsOrWillShow = model.bouncerIsOrWillShow
                 canSkipBouncer = model.canSkipBouncer
@@ -141,6 +144,7 @@
                 "userId",
                 "listening",
                 // keep sorted
+                "alternateBouncerShowing",
                 "biometricAllowedForUser",
                 "bouncerIsOrWillShow",
                 "canSkipBouncer",
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
deleted file mode 100644
index 08e9cf6..0000000
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.keyguard;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-
-/**
- * Base class for keyguard view.  {@link #reset} is where you should
- * reset the state of your view.  Use the {@link KeyguardViewCallback} via
- * {@link #getCallback()} to send information back (such as poking the wake lock,
- * or finishing the keyguard).
- *
- * Handles intercepting of media keys that still work when the keyguard is
- * showing.
- */
-public class KeyguardHostView extends FrameLayout {
-
-    protected ViewMediatorCallback mViewMediatorCallback;
-
-
-    public KeyguardHostView(Context context) {
-        this(context, null);
-    }
-
-    public KeyguardHostView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        super.dispatchDraw(canvas);
-        if (mViewMediatorCallback != null) {
-            mViewMediatorCallback.keyguardDoneDrawing();
-        }
-    }
-
-    public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) {
-        mViewMediatorCallback = viewMediatorCallback;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
deleted file mode 100644
index d4ca8e3..0000000
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
+++ /dev/null
@@ -1,519 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.keyguard;
-
-import android.app.ActivityManager;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.media.AudioManager;
-import android.os.SystemClock;
-import android.telephony.TelephonyManager;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.View.OnKeyListener;
-import android.view.ViewTreeObserver;
-import android.widget.FrameLayout;
-
-import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback;
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
-import com.android.keyguard.dagger.KeyguardBouncerScope;
-import com.android.settingslib.Utils;
-import com.android.systemui.R;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.util.ViewController;
-
-import java.io.File;
-
-import javax.inject.Inject;
-
-/** Controller for a {@link KeyguardHostView}. */
-@KeyguardBouncerScope
-public class KeyguardHostViewController extends ViewController<KeyguardHostView> {
-    private static final String TAG = "KeyguardViewBase";
-    public static final boolean DEBUG = KeyguardConstants.DEBUG;
-    // Whether the volume keys should be handled by keyguard. If true, then
-    // they will be handled here for specific media types such as music, otherwise
-    // the audio service will bring up the volume dialog.
-    private static final boolean KEYGUARD_MANAGES_VOLUME = false;
-
-    private static final String ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key";
-
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final KeyguardSecurityContainerController mKeyguardSecurityContainerController;
-    private final TelephonyManager mTelephonyManager;
-    private final ViewMediatorCallback mViewMediatorCallback;
-    private final AudioManager mAudioManager;
-
-    private ActivityStarter.OnDismissAction mDismissAction;
-    private Runnable mCancelAction;
-    private int mTranslationY;
-
-    private final KeyguardUpdateMonitorCallback mUpdateCallback =
-            new KeyguardUpdateMonitorCallback() {
-                @Override
-                public void onTrustGrantedForCurrentUser(
-                        boolean dismissKeyguard,
-                        boolean newlyUnlocked,
-                        TrustGrantFlags flags,
-                        String message
-                ) {
-                    if (dismissKeyguard) {
-                        if (!mView.isVisibleToUser()) {
-                            // The trust agent dismissed the keyguard without the user proving
-                            // that they are present (by swiping up to show the bouncer). That's
-                            // fine if the user proved presence via some other way to the trust
-                            // agent.
-                            Log.i(TAG, "TrustAgent dismissed Keyguard.");
-                        }
-                        mSecurityCallback.dismiss(
-                                false /* authenticated */,
-                                KeyguardUpdateMonitor.getCurrentUser(),
-                                /* bypassSecondaryLockScreen */ false,
-                                SecurityMode.Invalid
-                        );
-                    } else {
-                        if (flags.isInitiatedByUser() || flags.dismissKeyguardRequested()) {
-                            mViewMediatorCallback.playTrustedSound();
-                        }
-                    }
-                }
-            };
-
-    private final SecurityCallback mSecurityCallback = new SecurityCallback() {
-
-        @Override
-        public boolean dismiss(boolean authenticated, int targetUserId,
-                boolean bypassSecondaryLockScreen, SecurityMode expectedSecurityMode) {
-            return mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish(
-                    authenticated, targetUserId, bypassSecondaryLockScreen, expectedSecurityMode);
-        }
-
-        @Override
-        public void userActivity() {
-            mViewMediatorCallback.userActivity();
-        }
-
-        @Override
-        public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput) {
-            mViewMediatorCallback.setNeedsInput(needsInput);
-        }
-
-        /**
-         * Authentication has happened and it's time to dismiss keyguard. This function
-         * should clean up and inform KeyguardViewMediator.
-         *
-         * @param strongAuth whether the user has authenticated with strong authentication like
-         *                   pattern, password or PIN but not by trust agents or fingerprint
-         * @param targetUserId a user that needs to be the foreground user at the dismissal
-         *                    completion.
-         */
-        @Override
-        public void finish(boolean strongAuth, int targetUserId) {
-            // If there's a pending runnable because the user interacted with a widget
-            // and we're leaving keyguard, then run it.
-            boolean deferKeyguardDone = false;
-            if (mDismissAction != null) {
-                deferKeyguardDone = mDismissAction.onDismiss();
-                mDismissAction = null;
-                mCancelAction = null;
-            }
-            if (mViewMediatorCallback != null) {
-                if (deferKeyguardDone) {
-                    mViewMediatorCallback.keyguardDonePending(strongAuth, targetUserId);
-                } else {
-                    mViewMediatorCallback.keyguardDone(strongAuth, targetUserId);
-                }
-            }
-        }
-
-        @Override
-        public void reset() {
-            mViewMediatorCallback.resetKeyguard();
-        }
-
-        @Override
-        public void onCancelClicked() {
-            mViewMediatorCallback.onCancelClicked();
-        }
-    };
-
-    private OnKeyListener mOnKeyListener = (v, keyCode, event) -> interceptMediaKey(event);
-
-    @Inject
-    public KeyguardHostViewController(KeyguardHostView view,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
-            AudioManager audioManager,
-            TelephonyManager telephonyManager,
-            ViewMediatorCallback viewMediatorCallback,
-            KeyguardSecurityContainerController.Factory
-                    keyguardSecurityContainerControllerFactory) {
-        super(view);
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mAudioManager = audioManager;
-        mTelephonyManager = telephonyManager;
-        mViewMediatorCallback = viewMediatorCallback;
-        mKeyguardSecurityContainerController = keyguardSecurityContainerControllerFactory.create(
-                mSecurityCallback);
-    }
-
-    /** Initialize the Controller. */
-    public void onInit() {
-        mKeyguardSecurityContainerController.init();
-        updateResources();
-    }
-
-    @Override
-    protected void onViewAttached() {
-        mView.setViewMediatorCallback(mViewMediatorCallback);
-        // Update ViewMediator with the current input method requirements
-        mViewMediatorCallback.setNeedsInput(mKeyguardSecurityContainerController.needsInput());
-        mKeyguardUpdateMonitor.registerCallback(mUpdateCallback);
-        mView.setOnKeyListener(mOnKeyListener);
-        mKeyguardSecurityContainerController.showPrimarySecurityScreen(false);
-    }
-
-    @Override
-    protected void onViewDetached() {
-        mKeyguardUpdateMonitor.removeCallback(mUpdateCallback);
-        mView.setOnKeyListener(null);
-    }
-
-     /** Called before this view is being removed. */
-    public void cleanUp() {
-        mKeyguardSecurityContainerController.onPause();
-    }
-
-    public void resetSecurityContainer() {
-        mKeyguardSecurityContainerController.reset();
-    }
-
-    /**
-     * Dismisses the keyguard by going to the next screen or making it gone.
-     * @param targetUserId a user that needs to be the foreground user at the dismissal completion.
-     * @return True if the keyguard is done.
-     */
-    public boolean dismiss(int targetUserId) {
-        return mSecurityCallback.dismiss(false, targetUserId, false,
-                getCurrentSecurityMode());
-    }
-
-    /**
-     * Called when the Keyguard is actively shown on the screen.
-     */
-    public void onResume() {
-        if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode()));
-        mKeyguardSecurityContainerController.onResume(KeyguardSecurityView.SCREEN_ON);
-        mView.requestFocus();
-    }
-
-    public CharSequence getAccessibilityTitleForCurrentMode() {
-        return mKeyguardSecurityContainerController.getTitle();
-    }
-
-    /**
-     * Starts the animation when the Keyguard gets shown.
-     */
-    public void appear(int statusBarHeight) {
-        // We might still be collapsed and the view didn't have time to layout yet or still
-        // be small, let's wait on the predraw to do the animation in that case.
-        if (mView.getHeight() != 0 && mView.getHeight() != statusBarHeight) {
-            mKeyguardSecurityContainerController.startAppearAnimation();
-        } else {
-            mView.getViewTreeObserver().addOnPreDrawListener(
-                    new ViewTreeObserver.OnPreDrawListener() {
-                        @Override
-                        public boolean onPreDraw() {
-                            mView.getViewTreeObserver().removeOnPreDrawListener(this);
-                            mKeyguardSecurityContainerController.startAppearAnimation();
-                            return true;
-                        }
-                    });
-            mView.requestLayout();
-        }
-    }
-
-    /**
-     * Show a string explaining why the security view needs to be solved.
-     *
-     * @param reason a flag indicating which string should be shown, see
-     *               {@link KeyguardSecurityView#PROMPT_REASON_NONE},
-     *               {@link KeyguardSecurityView#PROMPT_REASON_RESTART},
-     *               {@link KeyguardSecurityView#PROMPT_REASON_TIMEOUT}, and
-     *               {@link KeyguardSecurityView#PROMPT_REASON_PREPARE_FOR_UPDATE}.
-     */
-    public void showPromptReason(int reason) {
-        mKeyguardSecurityContainerController.showPromptReason(reason);
-    }
-
-    public void showMessage(CharSequence message, ColorStateList colorState) {
-        mKeyguardSecurityContainerController.showMessage(message, colorState);
-    }
-
-    public void showErrorMessage(CharSequence customMessage) {
-        showMessage(customMessage, Utils.getColorError(mView.getContext()));
-    }
-
-    /**
-     * Sets an action to run when keyguard finishes.
-     *
-     * @param action
-     */
-    public void setOnDismissAction(ActivityStarter.OnDismissAction action, Runnable cancelAction) {
-        if (mCancelAction != null) {
-            mCancelAction.run();
-            mCancelAction = null;
-        }
-        mDismissAction = action;
-        mCancelAction = cancelAction;
-    }
-
-    public void cancelDismissAction() {
-        setOnDismissAction(null, null);
-    }
-
-    public void startDisappearAnimation(Runnable finishRunnable) {
-        if (!mKeyguardSecurityContainerController.startDisappearAnimation(finishRunnable)
-                && finishRunnable != null) {
-            finishRunnable.run();
-        }
-    }
-
-    /**
-     * Called when the Keyguard is not actively shown anymore on the screen.
-     */
-    public void onPause() {
-        if (DEBUG) {
-            Log.d(TAG, String.format("screen off, instance %s at %s",
-                    Integer.toHexString(hashCode()), SystemClock.uptimeMillis()));
-        }
-        mKeyguardSecurityContainerController.showPrimarySecurityScreen(true);
-        mKeyguardSecurityContainerController.onPause();
-        mView.clearFocus();
-    }
-
-    /**
-     * Called when the view needs to be shown.
-     */
-    public void showPrimarySecurityScreen() {
-        if (DEBUG) Log.d(TAG, "show()");
-        mKeyguardSecurityContainerController.showPrimarySecurityScreen(false);
-    }
-
-    /**
-     * Fades and translates in/out the security screen.
-     * Fades in as expansion approaches 0.
-     * Animation duration is between 0.33f and 0.67f of panel expansion fraction.
-     * @param fraction amount of the screen that should show.
-     */
-    public void setExpansion(float fraction) {
-        float scaledFraction = BouncerPanelExpansionCalculator.showBouncerProgress(fraction);
-        mView.setAlpha(MathUtils.constrain(1 - scaledFraction, 0f, 1f));
-        mView.setTranslationY(scaledFraction * mTranslationY);
-    }
-
-    /**
-     * When bouncer was visible and is starting to become hidden.
-     */
-    public void onStartingToHide() {
-        mKeyguardSecurityContainerController.onStartingToHide();
-    }
-
-    /** Called when bouncer visibility changes. */
-    public void onBouncerVisibilityChanged(@View.Visibility int visibility) {
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(visibility);
-    }
-
-    public boolean hasDismissActions() {
-        return mDismissAction != null || mCancelAction != null;
-    }
-
-    public SecurityMode getCurrentSecurityMode() {
-        return mKeyguardSecurityContainerController.getCurrentSecurityMode();
-    }
-
-    public int getTop() {
-        int top = mView.getTop();
-        // The password view has an extra top padding that should be ignored.
-        if (getCurrentSecurityMode() == SecurityMode.Password) {
-            View messageArea = mView.findViewById(R.id.keyguard_message_area);
-            top += messageArea.getTop();
-        }
-        return top;
-    }
-
-    public boolean handleBackKey() {
-        SecurityMode securityMode = mKeyguardSecurityContainerController.getCurrentSecurityMode();
-        if (securityMode != SecurityMode.None) {
-            mKeyguardSecurityContainerController.dismiss(
-                    false, KeyguardUpdateMonitor.getCurrentUser(), securityMode);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * In general, we enable unlocking the insecure keyguard with the menu key. However, there are
-     * some cases where we wish to disable it, notably when the menu button placement or technology
-     * is prone to false positives.
-     *
-     * @return true if the menu key should be enabled
-     */
-    public boolean shouldEnableMenuKey() {
-        final Resources res = mView.getResources();
-        final boolean configDisabled = res.getBoolean(R.bool.config_disableMenuKeyInLockScreen);
-        final boolean isTestHarness = ActivityManager.isRunningInTestHarness();
-        final boolean fileOverride = (new File(ENABLE_MENU_KEY_FILE)).exists();
-        return !configDisabled || isTestHarness || fileOverride;
-    }
-
-    /**
-     * @return true if the current bouncer is password
-     */
-    public boolean dispatchBackKeyEventPreIme() {
-        if (mKeyguardSecurityContainerController.getCurrentSecurityMode()
-                == SecurityMode.Password) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Allows the media keys to work when the keyguard is showing.
-     * The media keys should be of no interest to the actual keyguard view(s),
-     * so intercepting them here should not be of any harm.
-     * @param event The key event
-     * @return whether the event was consumed as a media key.
-     */
-    public boolean interceptMediaKey(KeyEvent event) {
-        int keyCode = event.getKeyCode();
-        if (event.getAction() == KeyEvent.ACTION_DOWN) {
-            switch (keyCode) {
-                case KeyEvent.KEYCODE_MEDIA_PLAY:
-                case KeyEvent.KEYCODE_MEDIA_PAUSE:
-                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-                    /* Suppress PLAY/PAUSE toggle when phone is ringing or
-                     * in-call to avoid music playback */
-                    if (mTelephonyManager != null &&
-                            mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
-                        return true;  // suppress key event
-                    }
-                case KeyEvent.KEYCODE_MUTE:
-                case KeyEvent.KEYCODE_HEADSETHOOK:
-                case KeyEvent.KEYCODE_MEDIA_STOP:
-                case KeyEvent.KEYCODE_MEDIA_NEXT:
-                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
-                case KeyEvent.KEYCODE_MEDIA_REWIND:
-                case KeyEvent.KEYCODE_MEDIA_RECORD:
-                case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
-                case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
-                    handleMediaKeyEvent(event);
-                    return true;
-                }
-
-                case KeyEvent.KEYCODE_VOLUME_UP:
-                case KeyEvent.KEYCODE_VOLUME_DOWN:
-                case KeyEvent.KEYCODE_VOLUME_MUTE: {
-                    if (KEYGUARD_MANAGES_VOLUME) {
-                        // Volume buttons should only function for music (local or remote).
-                        // TODO: Actually handle MUTE.
-                        mAudioManager.adjustSuggestedStreamVolume(
-                                keyCode == KeyEvent.KEYCODE_VOLUME_UP
-                                        ? AudioManager.ADJUST_RAISE
-                                        : AudioManager.ADJUST_LOWER /* direction */,
-                                AudioManager.STREAM_MUSIC /* stream */, 0 /* flags */);
-                        // Don't execute default volume behavior
-                        return true;
-                    } else {
-                        return false;
-                    }
-                }
-            }
-        } else if (event.getAction() == KeyEvent.ACTION_UP) {
-            switch (keyCode) {
-                case KeyEvent.KEYCODE_MUTE:
-                case KeyEvent.KEYCODE_HEADSETHOOK:
-                case KeyEvent.KEYCODE_MEDIA_PLAY:
-                case KeyEvent.KEYCODE_MEDIA_PAUSE:
-                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-                case KeyEvent.KEYCODE_MEDIA_STOP:
-                case KeyEvent.KEYCODE_MEDIA_NEXT:
-                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
-                case KeyEvent.KEYCODE_MEDIA_REWIND:
-                case KeyEvent.KEYCODE_MEDIA_RECORD:
-                case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
-                case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
-                    handleMediaKeyEvent(event);
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-
-    private void handleMediaKeyEvent(KeyEvent keyEvent) {
-        mAudioManager.dispatchMediaKeyEvent(keyEvent);
-    }
-
-    public void finish(boolean strongAuth, int currentUser) {
-        mSecurityCallback.finish(strongAuth, currentUser);
-    }
-
-    /**
-     * Apply keyguard configuration from the currently active resources. This can be called when the
-     * device configuration changes, to re-apply some resources that are qualified on the device
-     * configuration.
-     */
-    public void updateResources() {
-        int gravity;
-
-        Resources resources = mView.getResources();
-
-        if (resources.getBoolean(R.bool.can_use_one_handed_bouncer)) {
-            gravity = resources.getInteger(
-                    R.integer.keyguard_host_view_one_handed_gravity);
-        } else {
-            gravity = resources.getInteger(R.integer.keyguard_host_view_gravity);
-        }
-
-        mTranslationY = resources
-                .getDimensionPixelSize(R.dimen.keyguard_host_view_translation_y);
-        // Android SysUI uses a FrameLayout as the top-level, but Auto uses RelativeLayout.
-        // We're just changing the gravity here though (which can't be applied to RelativeLayout),
-        // so only attempt the update if mView is inside a FrameLayout.
-        if (mView.getLayoutParams() instanceof FrameLayout.LayoutParams) {
-            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mView.getLayoutParams();
-            if (lp.gravity != gravity) {
-                lp.gravity = gravity;
-                mView.setLayoutParams(lp);
-            }
-        }
-
-        if (mKeyguardSecurityContainerController != null) {
-            mKeyguardSecurityContainerController.updateResources();
-        }
-    }
-
-    /** Update keyguard position based on a tapped X coordinate. */
-    public void updateKeyguardPosition(float x) {
-        if (mKeyguardSecurityContainerController != null) {
-            mKeyguardSecurityContainerController.updateKeyguardPosition(x);
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index d1c9a30..7054393 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -51,26 +51,7 @@
     // The following is used to ignore callbacks from SecurityViews that are no longer current
     // (e.g. face unlock). This avoids unwanted asynchronous events from messing with the
     // state for the current security method.
-    private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() {
-        @Override
-        public void userActivity() { }
-        @Override
-        public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { }
-        @Override
-        public boolean isVerifyUnlockOnly() {
-            return false;
-        }
-        @Override
-        public void dismiss(boolean securityVerified, int targetUserId,
-                SecurityMode expectedSecurityMode) { }
-        @Override
-        public void dismiss(boolean authenticated, int targetId,
-                boolean bypassSecondaryLockScreen, SecurityMode expectedSecurityMode) { }
-        @Override
-        public void onUserInput() { }
-        @Override
-        public void reset() {}
-    };
+    private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() {};
 
     protected KeyguardInputViewController(T view, SecurityMode securityMode,
             KeyguardSecurityCallback keyguardSecurityCallback,
@@ -121,6 +102,7 @@
 
     @Override
     public void reset() {
+        mMessageAreaController.setMessage("", false);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index d221e22..a010c9a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -26,6 +26,7 @@
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
+import android.view.WindowInsets;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
@@ -156,6 +157,15 @@
         // TODO: Remove this workaround by ensuring such a race condition never happens.
         mMainExecutor.executeDelayed(
                 this::updateSwitchImeButton, DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON);
+        mView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+            @Override
+            public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+                if (!mKeyguardViewController.isBouncerShowing()) {
+                    mView.hideKeyboard();
+                }
+                return insets;
+            }
+        });
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
index bc72f79..bf9c3bb 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
@@ -25,7 +25,9 @@
      * @param targetUserId a user that needs to be the foreground user at the dismissal completion.
      * @param expectedSecurityMode The security mode that is invoking this dismiss.
      */
-    void dismiss(boolean securityVerified, int targetUserId, SecurityMode expectedSecurityMode);
+    default void dismiss(boolean securityVerified, int targetUserId,
+            SecurityMode expectedSecurityMode) {
+    }
 
     /**
      * Dismiss the given security screen.
@@ -35,19 +37,26 @@
      *                                  if any, during this dismissal.
      * @param expectedSecurityMode The security mode that is invoking this dismiss.
      */
-    void dismiss(boolean securityVerified, int targetUserId, boolean bypassSecondaryLockScreen,
-            SecurityMode expectedSecurityMode);
+    default boolean dismiss(boolean securityVerified, int targetUserId,
+            boolean bypassSecondaryLockScreen,
+            SecurityMode expectedSecurityMode) {
+        return false;
+    }
 
     /**
      * Manually report user activity to keep the device awake.
      */
-    void userActivity();
+    default void userActivity() {
+    }
 
     /**
      * Checks if keyguard is in "verify credentials" mode.
+     *
      * @return true if user has been asked to verify security.
      */
-    boolean isVerifyUnlockOnly();
+    default boolean isVerifyUnlockOnly() {
+        return false;
+    }
 
     /**
      * Call to report an unlock attempt.
@@ -56,12 +65,14 @@
      * @param timeoutMs timeout in milliseconds to wait before reattempting an unlock.
      *                  Only nonzero if 'success' is false
      */
-    void reportUnlockAttempt(int userId, boolean success, int timeoutMs);
+    default void reportUnlockAttempt(int userId, boolean success, int timeoutMs) {
+    }
 
     /**
      * Resets the keyguard view.
      */
-    void reset();
+    default void reset() {
+    }
 
     /**
      * Call when cancel button is pressed in bouncer.
@@ -73,5 +84,19 @@
     /**
      * Invoked whenever users are typing their password or drawing a pattern.
      */
-    void onUserInput();
+    default void onUserInput() {
+    }
+
+
+    /**
+     * Dismisses keyguard and go to unlocked state.
+     */
+    default void finish(boolean strongAuth, int targetUserId) {
+    }
+
+    /**
+     * Specifies that security mode has changed.
+     */
+    default void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput) {
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 5d7a6f1..c6f0eee 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -32,13 +32,13 @@
 import static androidx.constraintlayout.widget.ConstraintSet.TOP;
 import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
 
+import static com.android.systemui.animation.InterpolatorsAndroidX.DECELERATE_QUINT;
 import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
 
 import static java.lang.Integer.max;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.app.Activity;
@@ -49,6 +49,7 @@
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
@@ -73,6 +74,8 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
@@ -89,6 +92,7 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.settingslib.Utils;
+import com.android.settingslib.drawable.CircleFramedDrawable;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -135,7 +139,9 @@
     private static final float MIN_DRAG_SIZE = 10;
     // How much to scale the default slop by, to avoid accidental drags.
     private static final float SLOP_SCALE = 4f;
-
+    @VisibleForTesting
+    // How much the view scales down to during back gestures.
+    static final float MIN_BACK_SCALE = 0.9f;
     @VisibleForTesting
     KeyguardSecurityViewFlipper mSecurityViewFlipper;
     private GlobalSettings mGlobalSettings;
@@ -158,6 +164,8 @@
     private boolean mDisappearAnimRunning;
     private SwipeListener mSwipeListener;
     private ViewMode mViewMode = new DefaultViewMode();
+    private boolean mIsInteractable;
+    protected ViewMediatorCallback mViewMediatorCallback;
     /*
      * Using MODE_UNINITIALIZED to mean the view mode is set to DefaultViewMode, but init() has not
      * yet been called on it. This will happen when the ViewController is initialized.
@@ -230,39 +238,33 @@
                     }
                     updateChildren(0 /* translationY */, 1f /* alpha */);
                 }
-
-                private void updateChildren(int translationY, float alpha) {
-                    for (int i = 0; i < KeyguardSecurityContainer.this.getChildCount(); ++i) {
-                        View child = KeyguardSecurityContainer.this.getChildAt(i);
-                        child.setTranslationY(translationY);
-                        child.setAlpha(alpha);
-                    }
-                }
             };
 
-    // Used to notify the container when something interesting happens.
-    public interface SecurityCallback {
-        /**
-         * Potentially dismiss the current security screen, after validating that all device
-         * security has been unlocked. Otherwise show the next screen.
-         */
-        boolean dismiss(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen,
-                SecurityMode expectedSecurityMode);
+    private final OnBackAnimationCallback mBackCallback = new OnBackAnimationCallback() {
+        @Override
+        public void onBackCancelled() {
+            // TODO(b/259608500): Remove once back API auto animates progress to 0 on cancel.
+            resetScale();
+        }
 
-        void userActivity();
+        @Override
+        public void onBackInvoked() { }
 
-        void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput);
-
-        /**
-         * @param strongAuth   wheher the user has authenticated with strong authentication like
-         *                     pattern, password or PIN but not by trust agents or fingerprint
-         * @param targetUserId a user that needs to be the foreground user at the finish completion.
-         */
-        void finish(boolean strongAuth, int targetUserId);
-
-        void reset();
-
-        void onCancelClicked();
+        @Override
+        public void onBackProgressed(BackEvent event) {
+            float progress = event.getProgress();
+            // TODO(b/263819310): Update the interpolator to match spec.
+            float scale = MIN_BACK_SCALE
+                    +  (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress));
+            setScale(scale);
+        }
+    };
+    /**
+     * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture.
+     */
+    @NonNull
+    OnBackAnimationCallback getBackCallback() {
+        return mBackCallback;
     }
 
     public interface SwipeListener {
@@ -317,7 +319,7 @@
 
     public KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mSpringAnimation = new SpringAnimation(this, DynamicAnimation.Y);
+        mSpringAnimation = new SpringAnimation(this, DynamicAnimation.TRANSLATION_Y);
         mViewConfiguration = ViewConfiguration.get(context);
         mDoubleTapDetector = new GestureDetector(context, new DoubleTapListener());
     }
@@ -420,6 +422,11 @@
         mViewMode.reset();
     }
 
+    /** Set true if the view can be interacted with */
+    public void setInteractable(boolean isInteractable) {
+        mIsInteractable = isInteractable;
+    }
+
     @Override
     public boolean shouldDelayChildPressedState() {
         return true;
@@ -427,6 +434,10 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (!mIsInteractable) {
+            return true;
+        }
+
         boolean result =  mMotionEventListeners.stream().anyMatch(
                 listener -> listener.onInterceptTouchEvent(event))
                 || super.onInterceptTouchEvent(event);
@@ -480,12 +491,14 @@
             case MotionEvent.ACTION_MOVE:
                 mVelocityTracker.addMovement(event);
                 int pointerIndex = event.findPointerIndex(mActivePointerId);
-                float y = event.getY(pointerIndex);
-                if (mLastTouchY != -1) {
-                    float dy = y - mLastTouchY;
-                    setTranslationY(getTranslationY() + dy * TOUCH_Y_MULTIPLIER);
+                if (pointerIndex != -1) {
+                    float y = event.getY(pointerIndex);
+                    if (mLastTouchY != -1) {
+                        float dy = y - mLastTouchY;
+                        setTranslationY(getTranslationY() + dy * TOUCH_Y_MULTIPLIER);
+                    }
+                    mLastTouchY = y;
                 }
-                mLastTouchY = y;
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
@@ -562,6 +575,7 @@
      * This will run when the bouncer shows in all cases except when the user drags the bouncer up.
      */
     public void startAppearAnimation(SecurityMode securityMode) {
+        updateChildren(0 /* translationY */, 1f /* alpha */);
         mViewMode.startAppearAnimation(securityMode);
     }
 
@@ -613,6 +627,18 @@
         return insets.inset(0, 0, 0, inset);
     }
 
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (mViewMediatorCallback != null) {
+            mViewMediatorCallback.keyguardDoneDrawing();
+        }
+    }
+
+    public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) {
+        mViewMediatorCallback = viewMediatorCallback;
+    }
+
     private void showDialog(String title, String message) {
         if (mAlertDialog != null) {
             mAlertDialog.dismiss();
@@ -736,6 +762,23 @@
         mViewMode.onDensityOrFontScaleChanged();
     }
 
+    void resetScale() {
+        setScale(1);
+    }
+
+    private void setScale(float scale) {
+        setScaleX(scale);
+        setScaleY(scale);
+    }
+
+    private void updateChildren(int translationY, float alpha) {
+        for (int i = 0; i < getChildCount(); ++i) {
+            View child = getChildAt(i);
+            child.setTranslationY(translationY);
+            child.setAlpha(alpha);
+        }
+    }
+
     /**
      * Enscapsulates the differences between bouncer modes for the container.
      */
@@ -957,8 +1000,10 @@
         private Drawable findUserIcon(int userId) {
             Bitmap userIcon = UserManager.get(mView.getContext()).getUserIcon(userId);
             if (userIcon != null) {
-                return new BitmapDrawable(userIcon);
+                return CircleFramedDrawable.getInstance(mView.getContext(),
+                        userIcon);
             }
+
             return UserIcons.getDefaultUserIcon(mResources, userId, false);
         }
 
@@ -998,13 +1043,10 @@
 
             int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation);
 
-            AnimatorSet anims = new AnimatorSet();
             ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation);
-            ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mView, View.ALPHA, 0f);
-
-            anims.setInterpolator(Interpolators.STANDARD_ACCELERATE);
-            anims.playTogether(alphaAnim, yAnim);
-            anims.start();
+            yAnim.setInterpolator(Interpolators.STANDARD_ACCELERATE);
+            yAnim.setDuration(500);
+            yAnim.start();
         }
 
         private void setupUserSwitcher() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index a72a484..c32b853 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -17,7 +17,6 @@
 package com.android.keyguard;
 
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
-import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
 
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC;
 import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS;
@@ -29,18 +28,29 @@
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 
+import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
-import android.hardware.biometrics.BiometricSourceType;
+import android.content.res.Resources;
+import android.hardware.biometrics.BiometricOverlayConstants;
+import android.media.AudioManager;
 import android.metrics.LogMaker;
+import android.os.SystemClock;
 import android.os.UserHandle;
+import android.telephony.TelephonyManager;
 import android.util.Log;
+import android.util.MathUtils;
 import android.util.Slog;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.window.OnBackAnimationCallback;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -51,7 +61,6 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardSecurityContainer.BouncerUiEvent;
-import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback;
 import com.android.keyguard.KeyguardSecurityContainer.SwipeListener;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.keyguard.dagger.KeyguardBouncerScope;
@@ -65,6 +74,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.log.SessionTracker;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -73,6 +83,7 @@
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.settings.GlobalSettings;
 
+import java.io.File;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -93,7 +104,6 @@
     private final UiEventLogger mUiEventLogger;
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController;
-    private final SecurityCallback mSecurityCallback;
     private final ConfigurationController mConfigurationController;
     private final FalsingCollector mFalsingCollector;
     private final FalsingManager mFalsingManager;
@@ -103,6 +113,20 @@
     private final SessionTracker mSessionTracker;
     private final Optional<SideFpsController> mSideFpsController;
     private final FalsingA11yDelegate mFalsingA11yDelegate;
+    private int mTranslationY;
+    // Whether the volume keys should be handled by keyguard. If true, then
+    // they will be handled here for specific media types such as music, otherwise
+    // the audio service will bring up the volume dialog.
+    private static final boolean KEYGUARD_MANAGES_VOLUME = false;
+
+    private static final String ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key";
+
+    private final TelephonyManager mTelephonyManager;
+    private final ViewMediatorCallback mViewMediatorCallback;
+    private final AudioManager mAudioManager;
+    private View.OnKeyListener mOnKeyListener = (v, keyCode, event) -> interceptMediaKey(event);
+    private ActivityStarter.OnDismissAction mDismissAction;
+    private Runnable mCancelAction;
 
     private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
 
@@ -147,11 +171,6 @@
     };
 
     private KeyguardSecurityCallback mKeyguardSecurityCallback = new KeyguardSecurityCallback() {
-        public void userActivity() {
-            if (mSecurityCallback != null) {
-                mSecurityCallback.userActivity();
-            }
-        }
 
         @Override
         public void onUserInput() {
@@ -166,16 +185,23 @@
         }
 
         @Override
-        public void dismiss(boolean authenticated, int targetId,
+        public boolean dismiss(boolean authenticated, int targetId,
                 boolean bypassSecondaryLockScreen, SecurityMode expectedSecurityMode) {
-            mSecurityCallback.dismiss(authenticated, targetId, bypassSecondaryLockScreen,
-                    expectedSecurityMode);
+            return showNextSecurityScreenOrFinish(
+                    authenticated, targetId, bypassSecondaryLockScreen, expectedSecurityMode);
         }
 
+        @Override
+        public void userActivity() {
+            mViewMediatorCallback.userActivity();
+        }
+
+        @Override
         public boolean isVerifyUnlockOnly() {
             return false;
         }
 
+        @Override
         public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) {
             int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT;
             if (mView.isSidedSecurityMode()) {
@@ -212,12 +238,47 @@
                             : BouncerUiEvent.BOUNCER_PASSWORD_FAILURE, getSessionId());
         }
 
+        @Override
         public void reset() {
-            mSecurityCallback.reset();
+            mViewMediatorCallback.resetKeyguard();
         }
 
+        @Override
         public void onCancelClicked() {
-            mSecurityCallback.onCancelClicked();
+            mViewMediatorCallback.onCancelClicked();
+        }
+
+        /**
+         * Authentication has happened and it's time to dismiss keyguard. This function
+         * should clean up and inform KeyguardViewMediator.
+         *
+         * @param strongAuth whether the user has authenticated with strong authentication like
+         *                   pattern, password or PIN but not by trust agents or fingerprint
+         * @param targetUserId a user that needs to be the foreground user at the dismissal
+         *                    completion.
+         */
+        @Override
+        public void finish(boolean strongAuth, int targetUserId) {
+            // If there's a pending runnable because the user interacted with a widget
+            // and we're leaving keyguard, then run it.
+            boolean deferKeyguardDone = false;
+            if (mDismissAction != null) {
+                deferKeyguardDone = mDismissAction.onDismiss();
+                mDismissAction = null;
+                mCancelAction = null;
+            }
+            if (mViewMediatorCallback != null) {
+                if (deferKeyguardDone) {
+                    mViewMediatorCallback.keyguardDonePending(strongAuth, targetUserId);
+                } else {
+                    mViewMediatorCallback.keyguardDone(strongAuth, targetUserId);
+                }
+            }
+        }
+
+        @Override
+        public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput) {
+            mViewMediatorCallback.setNeedsInput(needsInput);
         }
     };
 
@@ -235,7 +296,7 @@
             }
             if (mUpdateMonitor.isFaceEnrolled()) {
                 mUpdateMonitor.requestActiveUnlock(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
                         "swipeUpOnBouncer");
             }
         }
@@ -257,29 +318,44 @@
                     KeyguardSecurityContainerController.this.onDensityOrFontScaleChanged();
                 }
             };
-    private boolean mBouncerVisible = false;
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
             new KeyguardUpdateMonitorCallback() {
                 @Override
-                public void onDevicePolicyManagerStateChanged() {
-                    showPrimarySecurityScreen(false);
-                }
-
-                @Override
-                public void onBiometricRunningStateChanged(boolean running,
-                        BiometricSourceType biometricSourceType) {
-                    if (biometricSourceType == FINGERPRINT) {
-                        updateSideFpsVisibility();
+                public void onTrustGrantedForCurrentUser(
+                        boolean dismissKeyguard,
+                        boolean newlyUnlocked,
+                        TrustGrantFlags flags,
+                        String message
+                ) {
+                    if (dismissKeyguard) {
+                        if (!mView.isVisibleToUser()) {
+                            // The trust agent dismissed the keyguard without the user proving
+                            // that they are present (by swiping up to show the bouncer). That's
+                            // fine if the user proved presence via some other way to the trust
+                            // agent.
+                            Log.i(TAG, "TrustAgent dismissed Keyguard.");
+                        }
+                        mKeyguardSecurityCallback.dismiss(
+                                false /* authenticated */,
+                                KeyguardUpdateMonitor.getCurrentUser(),
+                                /* bypassSecondaryLockScreen */ false,
+                                SecurityMode.Invalid
+                        );
+                    } else {
+                        if (flags.isInitiatedByUser() || flags.dismissKeyguardRequested()) {
+                            mViewMediatorCallback.playTrustedSound();
+                        }
                     }
                 }
 
                 @Override
-                public void onStrongAuthStateChanged(int userId) {
-                    updateSideFpsVisibility();
+                public void onDevicePolicyManagerStateChanged() {
+                    showPrimarySecurityScreen(false);
                 }
             };
 
-    private KeyguardSecurityContainerController(KeyguardSecurityContainer view,
+    @Inject
+    public KeyguardSecurityContainerController(KeyguardSecurityContainer view,
             AdminSecondaryLockScreenController.Factory adminSecondaryLockScreenControllerFactory,
             LockPatternUtils lockPatternUtils,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -287,7 +363,6 @@
             MetricsLogger metricsLogger,
             UiEventLogger uiEventLogger,
             KeyguardStateController keyguardStateController,
-            SecurityCallback securityCallback,
             KeyguardSecurityViewFlipperController securityViewFlipperController,
             ConfigurationController configurationController,
             FalsingCollector falsingCollector,
@@ -297,7 +372,11 @@
             GlobalSettings globalSettings,
             SessionTracker sessionTracker,
             Optional<SideFpsController> sideFpsController,
-            FalsingA11yDelegate falsingA11yDelegate) {
+            FalsingA11yDelegate falsingA11yDelegate,
+            TelephonyManager telephonyManager,
+            ViewMediatorCallback viewMediatorCallback,
+            AudioManager audioManager
+    ) {
         super(view);
         mLockPatternUtils = lockPatternUtils;
         mUpdateMonitor = keyguardUpdateMonitor;
@@ -305,7 +384,6 @@
         mMetricsLogger = metricsLogger;
         mUiEventLogger = uiEventLogger;
         mKeyguardStateController = keyguardStateController;
-        mSecurityCallback = securityCallback;
         mSecurityViewFlipperController = securityViewFlipperController;
         mAdminSecondaryLockScreenController = adminSecondaryLockScreenControllerFactory.create(
                 mKeyguardSecurityCallback);
@@ -319,11 +397,15 @@
         mSessionTracker = sessionTracker;
         mSideFpsController = sideFpsController;
         mFalsingA11yDelegate = falsingA11yDelegate;
+        mTelephonyManager = telephonyManager;
+        mViewMediatorCallback = viewMediatorCallback;
+        mAudioManager = audioManager;
     }
 
     @Override
     public void onInit() {
         mSecurityViewFlipperController.init();
+        updateResources();
         configureMode();
     }
 
@@ -334,6 +416,11 @@
         mView.addMotionEventListener(mGlobalTouchListener);
         mConfigurationController.addCallback(mConfigurationListener);
         mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
+        mView.setViewMediatorCallback(mViewMediatorCallback);
+        // Update ViewMediator with the current input method requirements
+        mViewMediatorCallback.setNeedsInput(needsInput());
+        mView.setOnKeyListener(mOnKeyListener);
+        showPrimarySecurityScreen(false);
     }
 
     @Override
@@ -346,38 +433,34 @@
 
     /** */
     public void onPause() {
+        if (DEBUG) {
+            Log.d(TAG, String.format("screen off, instance %s at %s",
+                    Integer.toHexString(hashCode()), SystemClock.uptimeMillis()));
+        }
+        showPrimarySecurityScreen(true);
         mAdminSecondaryLockScreenController.hide();
         if (mCurrentSecurityMode != SecurityMode.None) {
             getCurrentSecurityController().onPause();
         }
         mView.onPause();
-        // It might happen that onStartingToHide is not called when the device is locked while on
-        // bouncer.
-        setBouncerVisible(false);
+        mView.clearFocus();
     }
 
-    private void updateSideFpsVisibility() {
+    /**
+     * Shows and hides the side finger print sensor animation.
+     *
+     * @param isVisible sets whether we show or hide the side fps animation
+     */
+    public void updateSideFpsVisibility(boolean isVisible) {
         if (!mSideFpsController.isPresent()) {
             return;
         }
-        final boolean sfpsEnabled = getResources().getBoolean(
-                R.bool.config_show_sidefps_hint_on_bouncer);
-        final boolean fpsDetectionRunning = mUpdateMonitor.isFingerprintDetectionRunning();
-        final boolean isUnlockingWithFpAllowed =
-                mUpdateMonitor.isUnlockingWithFingerprintAllowed();
 
-        boolean toShow = mBouncerVisible && sfpsEnabled && fpsDetectionRunning
-                && isUnlockingWithFpAllowed;
-
-        if (DEBUG) {
-            Log.d(TAG, "sideFpsToShow=" + toShow + ", "
-                    + "mBouncerVisible=" + mBouncerVisible + ", "
-                    + "configEnabled=" + sfpsEnabled + ", "
-                    + "fpsDetectionRunning=" + fpsDetectionRunning + ", "
-                    + "isUnlockingWithFpAllowed=" + isUnlockingWithFpAllowed);
-        }
-        if (toShow) {
-            mSideFpsController.get().show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        if (isVisible) {
+            mSideFpsController.get().show(
+                    SideFpsUiRequestSource.PRIMARY_BOUNCER,
+                    BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+            );
         } else {
             mSideFpsController.get().hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
         }
@@ -389,12 +472,22 @@
      * @param turningOff true if the device is being turned off
      */
     public void showPrimarySecurityScreen(boolean turningOff) {
+        if (DEBUG) Log.d(TAG, "show()");
         SecurityMode securityMode = whitelistIpcs(() -> mSecurityModel.getSecurityMode(
                 KeyguardUpdateMonitor.getCurrentUser()));
         if (DEBUG) Log.v(TAG, "showPrimarySecurityScreen(turningOff=" + turningOff + ")");
         showSecurityScreen(securityMode);
     }
 
+    /**
+     * Show a string explaining why the security view needs to be solved.
+     *
+     * @param reason a flag indicating which string should be shown, see
+     *               {@link KeyguardSecurityView#PROMPT_REASON_NONE},
+     *               {@link KeyguardSecurityView#PROMPT_REASON_RESTART},
+     *               {@link KeyguardSecurityView#PROMPT_REASON_TIMEOUT}, and
+     *               {@link KeyguardSecurityView#PROMPT_REASON_PREPARE_FOR_UPDATE}.
+     */
     @Override
     public void showPromptReason(int reason) {
         if (mCurrentSecurityMode != SecurityMode.None) {
@@ -411,8 +504,32 @@
         }
     }
 
-    public SecurityMode getCurrentSecurityMode() {
-        return mCurrentSecurityMode;
+    /**
+     * Sets an action to run when keyguard finishes.
+     *
+     * @param action callback to be invoked when keyguard disappear animation completes.
+     */
+    public void setOnDismissAction(ActivityStarter.OnDismissAction action, Runnable cancelAction) {
+        if (mCancelAction != null) {
+            mCancelAction.run();
+            mCancelAction = null;
+        }
+        mDismissAction = action;
+        mCancelAction = cancelAction;
+    }
+
+    /**
+     * @return whether dismiss action or cancel action has been set.
+     */
+    public boolean hasDismissActions() {
+        return mDismissAction != null || mCancelAction != null;
+    }
+
+    /**
+     * Remove any dismiss action or cancel action that was set.
+     */
+    public void cancelDismissAction() {
+        setOnDismissAction(null, null);
     }
 
     /**
@@ -424,17 +541,64 @@
         mKeyguardSecurityCallback.dismiss(authenticated, targetUserId, expectedSecurityMode);
     }
 
+    /**
+     * Dismisses the keyguard by going to the next screen or making it gone.
+     * @param targetUserId a user that needs to be the foreground user at the dismissal completion.
+     * @return True if the keyguard is done.
+     */
+    public boolean dismiss(int targetUserId) {
+        return mKeyguardSecurityCallback.dismiss(false, targetUserId, false,
+                getCurrentSecurityMode());
+    }
+
+    public SecurityMode getCurrentSecurityMode() {
+        return mCurrentSecurityMode;
+    }
+
+    /**
+     * @return the top of the corresponding view.
+     */
+    public int getTop() {
+        int top = mView.getTop();
+        // The password view has an extra top padding that should be ignored.
+        if (getCurrentSecurityMode() == SecurityMode.Password) {
+            View messageArea = mView.findViewById(R.id.keyguard_message_area);
+            top += messageArea.getTop();
+        }
+        return top;
+    }
+
+    /** Set true if the view can be interacted with */
+    public void setInteractable(boolean isInteractable) {
+        mView.setInteractable(isInteractable);
+    }
+
+    /**
+     * Dismiss keyguard due to a user unlock event.
+     */
+    public void finish(boolean strongAuth, int currentUser) {
+        mKeyguardSecurityCallback.finish(strongAuth, currentUser);
+    }
+
+    /**
+     * @return the text of the KeyguardMessageArea.
+     */
+    public CharSequence getTitle() {
+        return mView.getTitle();
+    }
+
+    /**
+     *  Resets the state of the views.
+     */
     public void reset() {
         mView.reset();
         mSecurityViewFlipperController.reset();
     }
 
-    public CharSequence getTitle() {
-        return mView.getTitle();
-    }
-
     @Override
     public void onResume(int reason) {
+        if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode()));
+        mView.requestFocus();
         if (mCurrentSecurityMode != SecurityMode.None) {
             int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN;
             if (mView.isSidedSecurityMode()) {
@@ -445,45 +609,71 @@
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, state);
 
             getCurrentSecurityController().onResume(reason);
-            updateSideFpsVisibility();
         }
         mView.onResume(
                 mSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser()),
                 mKeyguardStateController.isFaceAuthEnabled());
     }
 
+    /**
+     * Show the bouncer and start appear animations.
+     *
+     */
+    public void appear() {
+        // We might still be collapsed and the view didn't have time to layout yet or still
+        // be small, let's wait on the predraw to do the animation in that case.
+        mView.getViewTreeObserver().addOnPreDrawListener(
+                new ViewTreeObserver.OnPreDrawListener() {
+                    @Override
+                    public boolean onPreDraw() {
+                        mView.getViewTreeObserver().removeOnPreDrawListener(this);
+                        startAppearAnimation();
+                        return true;
+                    }
+                });
+        mView.requestLayout();
+    }
+
     public void startAppearAnimation() {
         if (mCurrentSecurityMode != SecurityMode.None) {
-            mView.setAlpha(1f);
+            setAlpha(1f);
             mView.startAppearAnimation(mCurrentSecurityMode);
             getCurrentSecurityController().startAppearAnimation();
         }
     }
 
+    /** Set the alpha of the security container view */
+    public void setAlpha(float alpha) {
+        mView.setAlpha(alpha);
+    }
+
     public boolean startDisappearAnimation(Runnable onFinishRunnable) {
+        boolean didRunAnimation = false;
+
         if (mCurrentSecurityMode != SecurityMode.None) {
             mView.startDisappearAnimation(mCurrentSecurityMode);
-            return getCurrentSecurityController().startDisappearAnimation(onFinishRunnable);
+            didRunAnimation = getCurrentSecurityController().startDisappearAnimation(
+                    onFinishRunnable);
         }
 
-        return false;
+        if (!didRunAnimation && onFinishRunnable != null) {
+            onFinishRunnable.run();
+        }
+
+        return didRunAnimation;
     }
 
     public void onStartingToHide() {
         if (mCurrentSecurityMode != SecurityMode.None) {
             getCurrentSecurityController().onStartingToHide();
         }
-        setBouncerVisible(false);
     }
 
     /** Called when the bouncer changes visibility. */
-    public void onBouncerVisibilityChanged(@View.Visibility int visibility) {
-        setBouncerVisible(visibility == View.VISIBLE);
-    }
-
-    private void setBouncerVisible(boolean visible) {
-        mBouncerVisible = visible;
-        updateSideFpsVisibility();
+    public void onBouncerVisibilityChanged(boolean isVisible) {
+        if (!isVisible) {
+            mView.resetScale();
+        }
     }
 
     /**
@@ -578,7 +768,7 @@
             mUiEventLogger.log(uiEvent, getSessionId());
         }
         if (finish) {
-            mSecurityCallback.finish(strongAuth, targetUserId);
+            mKeyguardSecurityCallback.finish(strongAuth, targetUserId);
         }
         return finish;
     }
@@ -588,6 +778,117 @@
     }
 
     /**
+     * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture.
+     */
+    @NonNull
+    public OnBackAnimationCallback getBackCallback() {
+        return mView.getBackCallback();
+    }
+
+    /**
+     * @return whether we should dispatch the back key event before Ime.
+     */
+    public boolean dispatchBackKeyEventPreIme() {
+        return getCurrentSecurityMode() == SecurityMode.Password;
+    }
+
+    /**
+     * Allows the media keys to work when the keyguard is showing.
+     * The media keys should be of no interest to the actual keyguard view(s),
+     * so intercepting them here should not be of any harm.
+     * @param event The key event
+     * @return whether the event was consumed as a media key.
+     */
+    public boolean interceptMediaKey(KeyEvent event) {
+        int keyCode = event.getKeyCode();
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_MEDIA_PLAY:
+                case KeyEvent.KEYCODE_MEDIA_PAUSE:
+                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                    /* Suppress PLAY/PAUSE toggle when phone is ringing or
+                     * in-call to avoid music playback */
+                    if (mTelephonyManager != null
+                            && mTelephonyManager.getCallState()
+                            != TelephonyManager.CALL_STATE_IDLE) {
+                        return true;  // suppress key event
+                    }
+                    return false;
+                case KeyEvent.KEYCODE_MUTE:
+                case KeyEvent.KEYCODE_HEADSETHOOK:
+                case KeyEvent.KEYCODE_MEDIA_STOP:
+                case KeyEvent.KEYCODE_MEDIA_NEXT:
+                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+                case KeyEvent.KEYCODE_MEDIA_REWIND:
+                case KeyEvent.KEYCODE_MEDIA_RECORD:
+                case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+                case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
+                    handleMediaKeyEvent(event);
+                    return true;
+                }
+
+                case KeyEvent.KEYCODE_VOLUME_UP:
+                case KeyEvent.KEYCODE_VOLUME_DOWN:
+                case KeyEvent.KEYCODE_VOLUME_MUTE: {
+                    if (KEYGUARD_MANAGES_VOLUME) {
+                        // Volume buttons should only function for music (local or remote).
+                        // TODO: Actually handle MUTE.
+                        mAudioManager.adjustSuggestedStreamVolume(
+                                keyCode == KeyEvent.KEYCODE_VOLUME_UP
+                                        ? AudioManager.ADJUST_RAISE
+                                        : AudioManager.ADJUST_LOWER /* direction */,
+                                AudioManager.STREAM_MUSIC /* stream */, 0 /* flags */);
+                        // Don't execute default volume behavior
+                        return true;
+                    } else {
+                        return false;
+                    }
+                }
+            }
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_MUTE:
+                case KeyEvent.KEYCODE_HEADSETHOOK:
+                case KeyEvent.KEYCODE_MEDIA_PLAY:
+                case KeyEvent.KEYCODE_MEDIA_PAUSE:
+                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                case KeyEvent.KEYCODE_MEDIA_STOP:
+                case KeyEvent.KEYCODE_MEDIA_NEXT:
+                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+                case KeyEvent.KEYCODE_MEDIA_REWIND:
+                case KeyEvent.KEYCODE_MEDIA_RECORD:
+                case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+                case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
+                    handleMediaKeyEvent(event);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+
+    private void handleMediaKeyEvent(KeyEvent keyEvent) {
+        mAudioManager.dispatchMediaKeyEvent(keyEvent);
+    }
+
+    /**
+     * In general, we enable unlocking the insecure keyguard with the menu key. However, there are
+     * some cases where we wish to disable it, notably when the menu button placement or technology
+     * is prone to false positives.
+     *
+     * @return true if the menu key should be enabled
+     */
+    public boolean shouldEnableMenuKey() {
+        final Resources res = mView.getResources();
+        final boolean configDisabled = res.getBoolean(R.bool.config_disableMenuKeyInLockScreen);
+        final boolean isTestHarness = ActivityManager.isRunningInTestHarness();
+        final boolean fileOverride = (new File(ENABLE_MENU_KEY_FILE)).exists();
+        return !configDisabled || isTestHarness || fileOverride;
+    }
+
+
+    /**
      * Switches to the given security view unless it's already being shown, in which case
      * this is a no-op.
      *
@@ -615,7 +916,7 @@
             configureMode();
         }
 
-        mSecurityCallback.onSecurityModeChanged(
+        mKeyguardSecurityCallback.onSecurityModeChanged(
                 securityMode, newView != null && newView.needsInput());
     }
 
@@ -713,6 +1014,30 @@
      * configuration.
      */
     public void updateResources() {
+        int gravity;
+
+        Resources resources = mView.getResources();
+
+        if (resources.getBoolean(R.bool.can_use_one_handed_bouncer)) {
+            gravity = resources.getInteger(
+                    R.integer.keyguard_host_view_one_handed_gravity);
+        } else {
+            gravity = resources.getInteger(R.integer.keyguard_host_view_gravity);
+        }
+
+        mTranslationY = resources
+                .getDimensionPixelSize(R.dimen.keyguard_host_view_translation_y);
+        // Android SysUI uses a FrameLayout as the top-level, but Auto uses RelativeLayout.
+        // We're just changing the gravity here though (which can't be applied to RelativeLayout),
+        // so only attempt the update if mView is inside a FrameLayout.
+        if (mView.getLayoutParams() instanceof FrameLayout.LayoutParams) {
+            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mView.getLayoutParams();
+            if (lp.gravity != gravity) {
+                lp.gravity = gravity;
+                mView.setLayoutParams(lp);
+            }
+        }
+
         int newOrientation = getResources().getConfiguration().orientation;
         if (newOrientation != mLastOrientation) {
             mLastOrientation = newOrientation;
@@ -730,93 +1055,34 @@
     }
 
     private void reloadColors() {
-        resetViewFlipper();
+        reinflateViewFlipper();
         mView.reloadColors();
     }
 
     /** Handles density or font scale changes. */
     private void onDensityOrFontScaleChanged() {
-        resetViewFlipper();
+        reinflateViewFlipper();
         mView.onDensityOrFontScaleChanged();
     }
 
-    private void resetViewFlipper() {
+    /**
+     * Reinflate the view flipper child view.
+     */
+    public void reinflateViewFlipper() {
         mSecurityViewFlipperController.clearViews();
         mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
                 mKeyguardSecurityCallback);
     }
 
-    static class Factory {
-
-        private final KeyguardSecurityContainer mView;
-        private final AdminSecondaryLockScreenController.Factory
-                mAdminSecondaryLockScreenControllerFactory;
-        private final LockPatternUtils mLockPatternUtils;
-        private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-        private final KeyguardSecurityModel mKeyguardSecurityModel;
-        private final MetricsLogger mMetricsLogger;
-        private final UiEventLogger mUiEventLogger;
-        private final KeyguardStateController mKeyguardStateController;
-        private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController;
-        private final ConfigurationController mConfigurationController;
-        private final FalsingCollector mFalsingCollector;
-        private final FalsingManager mFalsingManager;
-        private final GlobalSettings mGlobalSettings;
-        private final FeatureFlags mFeatureFlags;
-        private final UserSwitcherController mUserSwitcherController;
-        private final SessionTracker mSessionTracker;
-        private final Optional<SideFpsController> mSidefpsController;
-        private final FalsingA11yDelegate mFalsingA11yDelegate;
-
-        @Inject
-        Factory(KeyguardSecurityContainer view,
-                AdminSecondaryLockScreenController.Factory
-                        adminSecondaryLockScreenControllerFactory,
-                LockPatternUtils lockPatternUtils,
-                KeyguardUpdateMonitor keyguardUpdateMonitor,
-                KeyguardSecurityModel keyguardSecurityModel,
-                MetricsLogger metricsLogger,
-                UiEventLogger uiEventLogger,
-                KeyguardStateController keyguardStateController,
-                KeyguardSecurityViewFlipperController securityViewFlipperController,
-                ConfigurationController configurationController,
-                FalsingCollector falsingCollector,
-                FalsingManager falsingManager,
-                UserSwitcherController userSwitcherController,
-                FeatureFlags featureFlags,
-                GlobalSettings globalSettings,
-                SessionTracker sessionTracker,
-                Optional<SideFpsController> sidefpsController,
-                FalsingA11yDelegate falsingA11yDelegate) {
-            mView = view;
-            mAdminSecondaryLockScreenControllerFactory = adminSecondaryLockScreenControllerFactory;
-            mLockPatternUtils = lockPatternUtils;
-            mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-            mKeyguardSecurityModel = keyguardSecurityModel;
-            mMetricsLogger = metricsLogger;
-            mUiEventLogger = uiEventLogger;
-            mKeyguardStateController = keyguardStateController;
-            mSecurityViewFlipperController = securityViewFlipperController;
-            mConfigurationController = configurationController;
-            mFalsingCollector = falsingCollector;
-            mFalsingManager = falsingManager;
-            mFeatureFlags = featureFlags;
-            mGlobalSettings = globalSettings;
-            mUserSwitcherController = userSwitcherController;
-            mSessionTracker = sessionTracker;
-            mSidefpsController = sidefpsController;
-            mFalsingA11yDelegate = falsingA11yDelegate;
-        }
-
-        public KeyguardSecurityContainerController create(
-                SecurityCallback securityCallback) {
-            return new KeyguardSecurityContainerController(mView,
-                    mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils,
-                    mKeyguardUpdateMonitor, mKeyguardSecurityModel, mMetricsLogger, mUiEventLogger,
-                    mKeyguardStateController, securityCallback, mSecurityViewFlipperController,
-                    mConfigurationController, mFalsingCollector, mFalsingManager,
-                    mUserSwitcherController, mFeatureFlags, mGlobalSettings, mSessionTracker,
-                    mSidefpsController, mFalsingA11yDelegate);
-        }
+    /**
+     * Fades and translates in/out the security screen.
+     * Fades in as expansion approaches 0.
+     * Animation duration is between 0.33f and 0.67f of panel expansion fraction.
+     * @param fraction amount of the screen that should show.
+     */
+    public void setExpansion(float fraction) {
+        float scaledFraction = BouncerPanelExpansionCalculator.showBouncerProgress(fraction);
+        mView.setAlpha(MathUtils.constrain(1 - scaledFraction, 0f, 1f));
+        mView.setTranslationY(scaledFraction * mTranslationY);
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
index 0b2b121..e3de8c7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
@@ -17,7 +17,6 @@
 package com.android.keyguard;
 
 import static android.app.slice.Slice.HINT_LIST_ITEM;
-import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.app.PendingIntent;
 import android.net.Uri;
@@ -43,6 +42,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.ViewController;
@@ -64,6 +64,7 @@
     private final ConfigurationController mConfigurationController;
     private final TunerService mTunerService;
     private final DumpManager mDumpManager;
+    private final DisplayTracker mDisplayTracker;
     private int mDisplayId;
     private LiveData<Slice> mLiveData;
     private Uri mKeyguardSliceUri;
@@ -108,12 +109,14 @@
             ActivityStarter activityStarter,
             ConfigurationController configurationController,
             TunerService tunerService,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            DisplayTracker displayTracker) {
         super(keyguardSliceView);
         mActivityStarter = activityStarter;
         mConfigurationController = configurationController;
         mTunerService = tunerService;
         mDumpManager = dumpManager;
+        mDisplayTracker = displayTracker;
     }
 
     @Override
@@ -124,7 +127,7 @@
         }
         mTunerService.addTunable(mTunable, Settings.Secure.KEYGUARD_SLICE_URI);
         // Make sure we always have the most current slice
-        if (mDisplayId == DEFAULT_DISPLAY && mLiveData != null) {
+        if (mDisplayId == mDisplayTracker.getDefaultDisplayId() && mLiveData != null) {
             mLiveData.observeForever(mObserver);
         }
         mConfigurationController.addCallback(mConfigurationListener);
@@ -137,7 +140,7 @@
     @Override
     protected void onViewDetached() {
         // TODO(b/117344873) Remove below work around after this issue be fixed.
-        if (mDisplayId == DEFAULT_DISPLAY) {
+        if (mDisplayId == mDisplayTracker.getDefaultDisplayId()) {
             mLiveData.removeObserver(mObserver);
         }
         mTunerService.removeTunable(mTunable);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index aec3063..f4c5815 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,8 +20,7 @@
 import android.util.Slog;
 
 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -61,17 +60,16 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             ConfigurationController configurationController,
             DozeParameters dozeParameters,
-            FeatureFlags featureFlags,
-            ScreenOffAnimationController screenOffAnimationController) {
+            ScreenOffAnimationController screenOffAnimationController,
+            KeyguardLogger logger) {
         super(keyguardStatusView);
         mKeyguardSliceViewController = keyguardSliceViewController;
         mKeyguardClockSwitchController = keyguardClockSwitchController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mConfigurationController = configurationController;
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
-                dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
-        mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
-                featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
+                dozeParameters, screenOffAnimationController, /* animateYPos= */ true,
+                logger.getBuffer());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 93027c1..be01377 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -26,7 +26,6 @@
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_PERMANENT;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED;
 import static android.hardware.biometrics.BiometricConstants.LockoutMode;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricSourceType.FACE;
 import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
 import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN;
@@ -63,20 +62,20 @@
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_KEYGUARD_INIT;
+import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
 
 import android.annotation.AnyThread;
 import android.annotation.MainThread;
 import android.annotation.SuppressLint;
-import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.ActivityTaskManager.RootTaskInfo;
 import android.app.AlarmManager;
-import android.app.UserSwitchObserver;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
@@ -84,7 +83,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
@@ -94,6 +92,7 @@
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
+import android.hardware.biometrics.SensorProperties;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintManager;
@@ -103,13 +102,10 @@
 import android.nfc.NfcAdapter;
 import android.os.CancellationSignal;
 import android.os.Handler;
-import android.os.IRemoteCallback;
 import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -141,6 +137,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -148,12 +145,14 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
 import com.android.systemui.log.SessionTracker;
+import com.android.systemui.plugins.WeatherData;
 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.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.settings.SecureSettings;
@@ -170,8 +169,10 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TimeZone;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
@@ -258,6 +259,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
@@ -269,21 +271,6 @@
     private static final ComponentName FALLBACK_HOME_COMPONENT = new ComponentName(
             "com.android.settings", "com.android.settings.FallbackHome");
 
-    /**
-     * If true, the system is in the half-boot-to-decryption-screen state.
-     * Prudently disable lockscreen.
-     */
-    public static final boolean CORE_APPS_ONLY;
-
-    static {
-        try {
-            CORE_APPS_ONLY = IPackageManager.Stub.asInterface(
-                    ServiceManager.getService("package")).isOnlyCoreApps();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
     private final Context mContext;
     private final UserTracker mUserTracker;
     private final KeyguardUpdateMonitorLogger mLogger;
@@ -323,7 +310,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;
@@ -345,18 +332,17 @@
     private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
             mCallbacks = Lists.newArrayList();
     private ContentObserver mDeviceProvisionedObserver;
-    private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver;
     private final ContentObserver mTimeFormatChangeObserver;
 
     private boolean mSwitchingUser;
 
     private boolean mDeviceInteractive;
-    private boolean mSfpsRequireScreenOnToAuthPrefEnabled;
     private final SubscriptionManager mSubscriptionManager;
     private final TelephonyListenerManager mTelephonyListenerManager;
     private final TrustManager mTrustManager;
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
+    private final DevicePostureController mPostureController;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final SecureSettings mSecureSettings;
     private final InteractionJankMonitor mInteractionJankMonitor;
@@ -365,7 +351,6 @@
     private final Executor mBackgroundExecutor;
     private final SensorPrivacyManager mSensorPrivacyManager;
     private final ActiveUnlockConfig mActiveUnlockConfig;
-    private final PowerManager mPowerManager;
     private final IDreamManager mDreamManager;
     private final TelephonyManager mTelephonyManager;
     @Nullable
@@ -373,7 +358,9 @@
     @Nullable
     private final FaceManager mFaceManager;
     private final LockPatternUtils mLockPatternUtils;
-    private final boolean mWakeOnFingerprintAcquiredStart;
+    @VisibleForTesting
+    @DevicePostureController.DevicePostureInt
+    protected int mConfigFaceAuthSupportedPosture;
 
     private KeyguardBypassController mKeyguardBypassController;
     private List<SubscriptionInfo> mSubscriptionInfo;
@@ -384,6 +371,8 @@
     private boolean mLogoutEnabled;
     private boolean mIsFaceEnrolled;
     private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    private int mPostureState = DEVICE_POSTURE_UNKNOWN;
+    private FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider;
 
     /**
      * Short delay before restarting fingerprint authentication after a successful try. This should
@@ -546,7 +535,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());
@@ -711,8 +701,18 @@
      */
     public void setKeyguardGoingAway(boolean goingAway) {
         mKeyguardGoingAway = goingAway;
-        // This is set specifically to stop face authentication from running.
-        updateBiometricListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY);
+        if (mKeyguardGoingAway) {
+            updateFaceListeningState(BIOMETRIC_ACTION_STOP,
+                    FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY);
+        }
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+    }
+
+    /**
+     * Whether keyguard is going away due to screen off or device entry.
+     */
+    public boolean isKeyguardGoingAway() {
+        return mKeyguardGoingAway;
     }
 
     /**
@@ -814,6 +814,19 @@
         }
     }
 
+    private void onBiometricDetected(int userId, BiometricSourceType biometricSourceType,
+            boolean isStrongBiometric) {
+        Assert.isMainThread();
+        Trace.beginSection("KeyGuardUpdateMonitor#onBiometricDetected");
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+            if (cb != null) {
+                cb.onBiometricDetected(userId, biometricSourceType, isStrongBiometric);
+            }
+        }
+        Trace.endSection();
+    }
+
     @VisibleForTesting
     protected void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) {
         Assert.isMainThread();
@@ -882,11 +895,6 @@
     private void handleFingerprintAcquired(
             @BiometricFingerprintConstants.FingerprintAcquired int acquireInfo) {
         Assert.isMainThread();
-        if (mWakeOnFingerprintAcquiredStart && acquireInfo == FINGERPRINT_ACQUIRED_START) {
-            mPowerManager.wakeUp(
-                    SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
-                    "com.android.systemui.keyguard:FINGERPRINT_ACQUIRED_START");
-        }
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -895,6 +903,20 @@
         }
     }
 
+    private void handleBiometricDetected(int authUserId, BiometricSourceType biometricSourceType,
+            boolean isStrongBiometric) {
+        Trace.beginSection("KeyGuardUpdateMonitor#handlerBiometricDetected");
+        onBiometricDetected(authUserId, biometricSourceType, isStrongBiometric);
+        if (biometricSourceType == FINGERPRINT) {
+            mLogger.logFingerprintDetected(authUserId, isStrongBiometric);
+        } else if (biometricSourceType == FACE) {
+            mLogger.logFaceDetected(authUserId, isStrongBiometric);
+            setFaceRunningState(BIOMETRIC_STATE_STOPPED);
+        }
+
+        Trace.endSection();
+    }
+
     private void handleFingerprintAuthenticated(int authUserId, boolean isStrongBiometric) {
         Trace.beginSection("KeyGuardUpdateMonitor#handlerFingerPrintAuthenticated");
         if (mHandler.hasCallbacks(mFpCancelNotReceived)) {
@@ -946,8 +968,14 @@
 
     private void onFingerprintCancelNotReceived() {
         mLogger.e("Fp cancellation not received, transitioning to STOPPED");
+        final boolean wasCancellingRestarting = mFingerprintRunningState
+                == BIOMETRIC_STATE_CANCELLING_RESTARTING;
         mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
-        KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_STOP);
+        if (wasCancellingRestarting) {
+            KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+        } else {
+            KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_STOP);
+        }
     }
 
     private void handleFingerprintError(int msgId, String errString) {
@@ -1034,6 +1062,12 @@
                     () -> updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE),
                     getBiometricLockoutDelay());
         } else {
+            boolean temporaryLockoutReset = wasLockout && !mFingerprintLockedOut;
+            if (temporaryLockoutReset) {
+                mLogger.d("temporaryLockoutReset - stopListeningForFingerprint() to stop"
+                        + " detectFingerprint");
+                stopListeningForFingerprint();
+            }
             updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         }
 
@@ -1317,7 +1351,8 @@
     }
 
     public boolean getUserHasTrust(int userId) {
-        return !isTrustDisabled() && mUserHasTrust.get(userId);
+        return !isTrustDisabled() && mUserHasTrust.get(userId)
+                && isUnlockingWithTrustAgentAllowed();
     }
 
     /**
@@ -1325,12 +1360,19 @@
      */
     public boolean getUserUnlockedWithBiometric(int userId) {
         BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId);
-        BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
         boolean fingerprintAllowed = fingerprint != null && fingerprint.mAuthenticated
                 && isUnlockingWithBiometricAllowed(fingerprint.mIsStrongBiometric);
-        boolean faceAllowed = face != null && face.mAuthenticated
+        return fingerprintAllowed || getUserUnlockedWithFace(userId);
+    }
+
+
+    /**
+     * Returns whether the user is unlocked with face.
+     */
+    public boolean getUserUnlockedWithFace(int userId) {
+        BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
+        return face != null && face.mAuthenticated
                 && isUnlockingWithBiometricAllowed(face.mIsStrongBiometric);
-        return fingerprintAllowed || faceAllowed;
     }
 
     /**
@@ -1405,6 +1447,10 @@
         return mUserTrustIsUsuallyManaged.get(userId);
     }
 
+    private boolean isUnlockingWithTrustAgentAllowed() {
+        return isUnlockingWithBiometricAllowed(true);
+    }
+
     public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) {
         // StrongAuthTracker#isUnlockingWithBiometricAllowed includes
         // STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent;
@@ -1535,11 +1581,12 @@
     @VisibleForTesting
     void setAssistantVisible(boolean assistantVisible) {
         mAssistantVisible = assistantVisible;
+        mLogger.logAssistantVisible(mAssistantVisible);
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
                 FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED);
         if (mAssistantVisible) {
             requestActiveUnlock(
-                    ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT,
+                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.ASSISTANT,
                     "assistant",
                     false);
         }
@@ -1669,7 +1716,7 @@
                 @Override
                 public void onAuthenticationFailed() {
                     requestActiveUnlockDismissKeyguard(
-                            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                            ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
                             "fingerprintFailure");
                     handleFingerprintAuthFailed();
                 }
@@ -1720,10 +1767,16 @@
                 }
             };
 
+    private final FingerprintManager.FingerprintDetectionCallback mFingerprintDetectionCallback =
+            (sensorId, userId, isStrongBiometric) -> {
+                // Trigger the fingerprint detected path so the bouncer can be shown
+                handleBiometricDetected(userId, FINGERPRINT, isStrongBiometric);
+            };
+
     private final FaceManager.FaceDetectionCallback mFaceDetectionCallback
             = (sensorId, userId, isStrongBiometric) -> {
-                // Trigger the face success path so the bouncer can be shown
-                handleFaceAuthenticated(userId, isStrongBiometric);
+                // Trigger the face detected path so the bouncer can be shown
+                handleBiometricDetected(userId, FACE, isStrongBiometric);
             };
 
     @VisibleForTesting
@@ -1734,11 +1787,11 @@
                 public void onAuthenticationFailed() {
                         String reason =
                                 mKeyguardBypassController.canBypass() ? "bypass"
-                                        : mUdfpsBouncerShowing ? "udfpsBouncer"
+                                        : mAlternateBouncerShowing ? "alternateBouncer"
                                                 : mPrimaryBouncerFullyShown ? "bouncer"
                                                         : "udfpsFpDown";
                         requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
                                 "faceFailure-" + reason);
 
                     handleFaceAuthFailed();
@@ -1765,7 +1818,7 @@
 
                     if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(errMsgId)) {
                         requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
                                 "faceError-" + errMsgId);
                     }
                 }
@@ -1777,13 +1830,24 @@
                     if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo(
                             acquireInfo)) {
                         requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
                                 "faceAcquireInfo-" + acquireInfo);
                     }
                 }
     };
 
     @VisibleForTesting
+    final DevicePostureController.Callback mPostureCallback =
+            new DevicePostureController.Callback() {
+                @Override
+                public void onPostureChanged(int posture) {
+                    mPostureState = posture;
+                    updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+                            FACE_AUTH_UPDATED_POSTURE_CHANGED);
+                }
+            };
+
+    @VisibleForTesting
     CancellationSignal mFingerprintCancelSignal;
     @VisibleForTesting
     CancellationSignal mFaceCancelSignal;
@@ -1906,8 +1970,23 @@
             FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason);
             updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
                     FACE_AUTH_UPDATED_STARTED_WAKING_UP);
-            requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - "
-                    + PowerManager.wakeReasonToString(pmWakeReason));
+
+            final ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin =
+                    mActiveUnlockConfig.isWakeupConsideredUnlockIntent(pmWakeReason)
+                            ? ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT
+                            : ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE;
+            final String reason = "wakingUp - " + PowerManager.wakeReasonToString(pmWakeReason);
+            if (mActiveUnlockConfig.shouldWakeupForceDismissKeyguard(pmWakeReason)) {
+                requestActiveUnlockDismissKeyguard(
+                        requestOrigin,
+                        reason
+                );
+            } else {
+                requestActiveUnlock(
+                        requestOrigin,
+                        reason
+                );
+            }
         } else {
             mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason);
         }
@@ -1931,6 +2010,11 @@
             }
         }
         mGoingToSleep = true;
+        // Resetting assistant visibility state as the device is going to sleep now.
+        // TaskStackChangeListener gets triggered a little late when we transition to AoD,
+        // which results in face auth running once on AoD.
+        mAssistantVisible = false;
+        mLogger.d("Started going to sleep, mGoingToSleep=true, mAssistantVisible=false");
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_GOING_TO_SLEEP);
     }
 
@@ -1943,9 +2027,9 @@
                 cb.onFinishedGoingToSleep(arg1);
             }
         }
-        // This is set specifically to stop face authentication from running.
-        updateBiometricListeningState(BIOMETRIC_ACTION_STOP,
+        updateFaceListeningState(BIOMETRIC_ACTION_STOP,
                 FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP);
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
     }
 
     private void handleScreenTurnedOff() {
@@ -2036,7 +2120,6 @@
             UiEventLogger uiEventLogger,
             // This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :(
             Provider<SessionTracker> sessionTrackerProvider,
-            PowerManager powerManager,
             TrustManager trustManager,
             SubscriptionManager subscriptionManager,
             UserManager userManager,
@@ -2048,7 +2131,9 @@
             @Nullable FaceManager faceManager,
             @Nullable FingerprintManager fingerprintManager,
             @Nullable BiometricManager biometricManager,
-            FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) {
+            FaceWakeUpTriggersConfig faceWakeUpTriggersConfig,
+            DevicePostureController devicePostureController,
+            Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) {
         mContext = context;
         mSubscriptionManager = subscriptionManager;
         mUserTracker = userTracker;
@@ -2071,23 +2156,23 @@
         mLogger = logger;
         mUiEventLogger = uiEventLogger;
         mSessionTrackerProvider = sessionTrackerProvider;
-        mPowerManager = powerManager;
         mTrustManager = trustManager;
         mUserManager = userManager;
         mDreamManager = dreamManager;
         mTelephonyManager = telephonyManager;
         mDevicePolicyManager = devicePolicyManager;
+        mPostureController = devicePostureController;
         mPackageManager = packageManager;
         mFpm = fingerprintManager;
         mFaceManager = faceManager;
         mActiveUnlockConfig.setKeyguardUpdateMonitor(this);
-        mWakeOnFingerprintAcquiredStart = context.getResources()
-                        .getBoolean(com.android.internal.R.bool.kg_wake_on_acquire_start);
         mFaceAcquiredInfoIgnoreList = Arrays.stream(
                 mContext.getResources().getIntArray(
                         R.array.config_face_acquire_device_entry_ignorelist))
                 .boxed()
                 .collect(Collectors.toSet());
+        mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger(
+                R.integer.config_face_auth_supported_posture);
         mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig;
 
         mHandler = new Handler(mainLooper) {
@@ -2116,7 +2201,7 @@
                         handleDevicePolicyManagerStateChanged(msg.arg1);
                         break;
                     case MSG_USER_SWITCHING:
-                        handleUserSwitching(msg.arg1, (IRemoteCallback) msg.obj);
+                        handleUserSwitching(msg.arg1, (CountDownLatch) msg.obj);
                         break;
                     case MSG_USER_SWITCH_COMPLETE:
                         handleUserSwitchComplete(msg.arg1);
@@ -2241,11 +2326,7 @@
                 mHandler, UserHandle.ALL);
 
         mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener);
-        try {
-            ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
-        } catch (RemoteException e) {
-            e.rethrowAsRuntimeException();
-        }
+        mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
 
         mTrustManager.registerTrustListener(this);
 
@@ -2278,6 +2359,9 @@
                         FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED));
             }
         });
+        if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) {
+            mPostureController.addCallback(mPostureCallback);
+        }
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT);
 
         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
@@ -2311,30 +2395,7 @@
                 Settings.System.getUriFor(Settings.System.TIME_12_24),
                 false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
 
-        updateSfpsRequireScreenOnToAuthPref();
-        mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) {
-            @Override
-            public void onChange(boolean selfChange) {
-                updateSfpsRequireScreenOnToAuthPref();
-            }
-        };
-
-        mContext.getContentResolver().registerContentObserver(
-                mSecureSettings.getUriFor(
-                        Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED),
-                false,
-                mSfpsRequireScreenOnToAuthPrefObserver,
-                getCurrentUser());
-    }
-
-    protected void updateSfpsRequireScreenOnToAuthPref() {
-        final int defaultSfpsRequireScreenOnToAuthValue =
-                mContext.getResources().getBoolean(
-                        com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0;
-        mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser(
-                Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
-                defaultSfpsRequireScreenOnToAuthValue,
-                getCurrentUser()) != 0;
+        mFingerprintInteractiveToAuthProvider = interactiveToAuthProvider.orElse(null);
     }
 
     private void initializeSimState() {
@@ -2401,17 +2462,17 @@
         return mIsFaceEnrolled;
     }
 
-    private final UserSwitchObserver mUserSwitchObserver = new UserSwitchObserver() {
+    private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() {
         @Override
-        public void onUserSwitching(int newUserId, IRemoteCallback reply) {
+        public void onUserChanging(int newUser, Context userContext, CountDownLatch latch) {
             mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCHING,
-                    newUserId, 0, reply));
+                    newUser, 0, latch));
         }
 
         @Override
-        public void onUserSwitchComplete(int newUserId) {
+        public void onUserChanged(int newUser, Context userContext) {
             mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCH_COMPLETE,
-                    newUserId, 0));
+                    newUser, 0));
         }
     };
 
@@ -2485,7 +2546,7 @@
         mAuthInterruptActive = active;
         updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
                 FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD);
-        requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "onReach");
+        requestActiveUnlock(ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE, "onReach");
     }
 
     /**
@@ -2555,7 +2616,7 @@
      * Attempts to trigger active unlock from trust agent.
      */
     private void requestActiveUnlock(
-            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
             String reason,
             boolean dismissKeyguard
     ) {
@@ -2566,7 +2627,7 @@
 
         final boolean allowRequest =
                 mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(requestOrigin);
-        if (requestOrigin == ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE
+        if (requestOrigin == ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE
                 && !allowRequest && mActiveUnlockConfig.isActiveUnlockEnabled()) {
             // instead of requesting the active unlock, initiate the unlock
             initiateActiveUnlock(reason);
@@ -2580,12 +2641,13 @@
         }
     }
 
+
     /**
      * Attempts to trigger active unlock from trust agent.
      * Only dismisses the keyguard under certain conditions.
      */
     public void requestActiveUnlock(
-            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
             String extraReason
     ) {
         final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
@@ -2593,7 +2655,7 @@
         requestActiveUnlock(
                 requestOrigin,
                 extraReason, canFaceBypass
-                        || mUdfpsBouncerShowing
+                        || mAlternateBouncerShowing
                         || mPrimaryBouncerFullyShown
                         || mAuthController.isUdfpsFingerDown());
     }
@@ -2602,7 +2664,7 @@
      * Attempts to trigger active unlock from trust agent with a request to dismiss the keyguard.
      */
     public void requestActiveUnlockDismissKeyguard(
-            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
             String extraReason
     ) {
         requestActiveUnlock(
@@ -2611,23 +2673,24 @@
     }
 
     /**
-     * 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.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
-                    "udfpsBouncer");
+                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
+                    "alternateBouncer");
         }
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
     }
 
     private boolean shouldTriggerActiveUnlock() {
         // Triggers:
         final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant();
-        final boolean awakeKeyguard = mPrimaryBouncerFullyShown || mUdfpsBouncerShowing
+        final boolean awakeKeyguard = mPrimaryBouncerFullyShown || mAlternateBouncerShowing
                 || (isKeyguardVisible() && !mGoingToSleep
                 && mStatusBarState != StatusBarState.SHADE_LOCKED);
 
@@ -2680,7 +2743,10 @@
 
     private boolean shouldListenForFaceAssistant() {
         BiometricAuthenticated face = mUserFaceAuthenticated.get(getCurrentUser());
-        return mAssistantVisible && mKeyguardOccluded
+        return mAssistantVisible
+                // There can be intermediate states where mKeyguardShowing is false but
+                // mKeyguardOccluded is true, we don't want to run face auth in such a scenario.
+                && (mKeyguardShowing && mKeyguardOccluded)
                 && !(face != null && face.mAuthenticated)
                 && !mUserHasTrust.get(getCurrentUser(), false);
     }
@@ -2703,7 +2769,7 @@
                         || shouldListenForFingerprintAssistant
                         || (mKeyguardOccluded && mIsDreaming)
                         || (mKeyguardOccluded && userDoesNotHaveTrust
-                            && (mOccludingAppRequestingFp || isUdfps));
+                            && (mOccludingAppRequestingFp || isUdfps || mAlternateBouncerShowing));
 
         // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
@@ -2729,8 +2795,11 @@
 
         boolean shouldListenSideFpsState = true;
         if (isSideFps) {
+            final boolean interactiveToAuthEnabled =
+                    mFingerprintInteractiveToAuthProvider != null &&
+                            mFingerprintInteractiveToAuthProvider.isEnabled(getCurrentUser());
             shouldListenSideFpsState =
-                    mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true;
+                    interactiveToAuthEnabled ? isDeviceInteractive() && !mGoingToSleep : true;
         }
 
         boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
@@ -2741,8 +2810,9 @@
                     System.currentTimeMillis(),
                     user,
                     shouldListen,
+                    mAlternateBouncerShowing,
                     biometricEnabledForUser,
-                        mPrimaryBouncerIsOrWillBeShowing,
+                    mPrimaryBouncerIsOrWillBeShowing,
                     userCanSkipBouncer,
                     mCredentialAttempted,
                     mDeviceInteractive,
@@ -2802,7 +2872,9 @@
         final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user);
         final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant();
         final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown();
-
+        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 =
@@ -2812,13 +2884,14 @@
                         || awakeKeyguard
                         || shouldListenForFaceAssistant
                         || isUdfpsFingerDown
-                        || mUdfpsBouncerShowing)
+                        || mAlternateBouncerShowing)
                 && !mSwitchingUser && !faceDisabledForUser && userNotTrustedOrDetectionIsNeeded
                 && !mKeyguardGoingAway && biometricEnabledForUser
                 && faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser
-                && (!mSecureCameraLaunched || mOccludingAppRequestingFace)
+                && (!mSecureCameraLaunched || mAlternateBouncerShowing)
                 && faceAndFpNotAuthenticated
-                && !mGoingToSleep;
+                && !mGoingToSleep
+                && isPostureAllowedForFaceAuth;
 
         // Aggregate relevant fields for debug logging.
         logListenerModelData(
@@ -2826,6 +2899,7 @@
                     System.currentTimeMillis(),
                     user,
                     shouldListen,
+                    mAlternateBouncerShowing,
                     mAuthInterruptActive,
                     biometricEnabledForUser,
                     mPrimaryBouncerFullyShown,
@@ -2838,11 +2912,11 @@
                     mKeyguardGoingAway,
                     shouldListenForFaceAssistant,
                     mOccludingAppRequestingFace,
+                    isPostureAllowedForFaceAuth,
                     mIsPrimaryUser,
                     mSecureCameraLaunched,
                     supportsDetect,
                     mSwitchingUser,
-                    mUdfpsBouncerShowing,
                     isUdfpsFingerDown,
                     userNotTrustedOrDetectionIsNeeded));
 
@@ -2885,11 +2959,7 @@
                 mLogger.v("startListeningForFingerprint - detect");
                 mFpm.detectFingerprint(
                         mFingerprintCancelSignal,
-                        (sensorId, user, isStrongBiometric) -> {
-                            mLogger.d("fingerprint detected");
-                            // Trigger the fingerprint success path so the bouncer can be shown
-                            handleFingerprintAuthenticated(user, isStrongBiometric);
-                        },
+                        mFingerprintDetectionCallback,
                         userId);
             } else {
                 mLogger.v("startListeningForFingerprint - authenticate");
@@ -2923,16 +2993,33 @@
                 getKeyguardSessionId(),
                 faceAuthUiEvent.getExtraInfo()
         );
-
+        mLogger.logFaceUnlockPossible(unlockPossible);
         if (unlockPossible) {
             mFaceCancelSignal = new CancellationSignal();
 
             // 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
@@ -2963,6 +3050,23 @@
         return isUnlockWithFacePossible(userId) || isUnlockWithFingerprintPossible(userId);
     }
 
+    /**
+     * If non-strong (i.e. weak or convenience) biometrics hardware is available, not disabled, and
+     * user has enrolled templates. This does NOT check if the device is encrypted or in lockdown.
+     *
+     * @param userId User that's trying to unlock.
+     * @return {@code true} if possible.
+     */
+    public boolean isUnlockingWithNonStrongBiometricsPossible(int userId) {
+        // This assumes that there is at most one face and at most one fingerprint sensor
+        return (mFaceManager != null && !mFaceSensorProperties.isEmpty()
+                && (mFaceSensorProperties.get(0).sensorStrength != SensorProperties.STRENGTH_STRONG)
+                && isUnlockWithFacePossible(userId))
+                || (mFpm != null && !mFingerprintSensorProperties.isEmpty()
+                && (mFingerprintSensorProperties.get(0).sensorStrength
+                != SensorProperties.STRENGTH_STRONG) && isUnlockWithFingerprintPossible(userId));
+    }
+
     @SuppressLint("MissingPermission")
     @VisibleForTesting
     boolean isUnlockWithFingerprintPossible(int userId) {
@@ -3081,7 +3185,7 @@
      * Handle {@link #MSG_USER_SWITCHING}
      */
     @VisibleForTesting
-    void handleUserSwitching(int userId, IRemoteCallback reply) {
+    void handleUserSwitching(int userId, CountDownLatch latch) {
         Assert.isMainThread();
         clearBiometricRecognized();
         mUserTrustIsUsuallyManaged.put(userId, mTrustManager.isTrustUsuallyManaged(userId));
@@ -3091,11 +3195,7 @@
                 cb.onUserSwitching(userId);
             }
         }
-        try {
-            reply.sendResult(null);
-        } catch (RemoteException e) {
-            mLogger.logException(e, "Ignored exception while userSwitching");
-        }
+        latch.countDown();
     }
 
     /**
@@ -3214,6 +3314,24 @@
     }
 
     /**
+     * @param data the weather data (temp, conditions, unit) for weather clock to use
+     */
+    public void sendWeatherData(WeatherData data) {
+        mHandler.post(()-> {
+            handleWeatherDataUpdate(data); });
+    }
+
+    private void handleWeatherDataUpdate(WeatherData data) {
+        Assert.isMainThread();
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+            if (cb != null) {
+                cb.onWeatherDataChanged(data);
+            }
+        }
+    }
+
+    /**
      * Handle {@link #MSG_BATTERY_UPDATE}
      */
     private void handleBatteryUpdate(BatteryStatus status) {
@@ -3332,7 +3450,8 @@
     /**
      * Handle {@link #MSG_KEYGUARD_RESET}
      */
-    private void handleKeyguardReset() {
+    @VisibleForTesting
+    protected void handleKeyguardReset() {
         mLogger.d("handleKeyguardReset");
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
                 FACE_AUTH_UPDATED_KEYGUARD_RESET);
@@ -3394,7 +3513,7 @@
         if (wasPrimaryBouncerFullyShown != mPrimaryBouncerFullyShown) {
             if (mPrimaryBouncerFullyShown) {
                 requestActiveUnlock(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
                         "bouncerFullyShown");
             }
             for (int i = 0; i < mCallbacks.size(); i++) {
@@ -3673,6 +3792,7 @@
                 if (info == null) {
                     return;
                 }
+                mLogger.logTaskStackChangedForAssistant(info.visible);
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_ASSISTANT_STACK_CHANGED,
                         info.visible));
             } catch (RemoteException e) {
@@ -3709,7 +3829,7 @@
     }
 
     // TODO: use these callbacks elsewhere in place of the existing notifyScreen*()
-    // (KeyguardViewMediator, KeyguardHostView)
+    // (KeyguardViewMediator, KeyguardSecurityContainer)
     /**
      * Dispatch wakeup events to:
      *  - update biometric listening states
@@ -3845,18 +3965,7 @@
             mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);
         }
 
-        if (mSfpsRequireScreenOnToAuthPrefObserver != null) {
-            mContext.getContentResolver().unregisterContentObserver(
-                    mSfpsRequireScreenOnToAuthPrefObserver);
-        }
-
-        try {
-            ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
-        } catch (RemoteException e) {
-            mLogger.logException(
-                    e,
-                    "RemoteException onDestroy. cannot unregister userSwitchObserver");
-        }
+        mUserTracker.removeCallback(mUserChangedCallback);
 
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
 
@@ -3876,7 +3985,6 @@
         pw.println("  getUserHasTrust()=" + getUserHasTrust(getCurrentUser()));
         pw.println("  getUserUnlockedWithBiometric()="
                 + getUserUnlockedWithBiometric(getCurrentUser()));
-        pw.println("  mWakeOnFingerprintAcquiredStart=" + mWakeOnFingerprintAcquiredStart);
         pw.println("  SIM States:");
         for (SimData data : mSimDatas.values()) {
             pw.println("    " + data.toString());
@@ -3922,12 +4030,18 @@
                 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));
-                pw.println("        mSfpsRequireScreenOnToAuthPrefEnabled="
-                        + mSfpsRequireScreenOnToAuthPrefEnabled);
+                if (isSfpsEnrolled()) {
+                    final boolean interactiveToAuthEnabled =
+                                    mFingerprintInteractiveToAuthProvider != null &&
+                                            mFingerprintInteractiveToAuthProvider
+                                            .isEnabled(getCurrentUser());
+                    pw.println("        interactiveToAuthEnabled="
+                            + interactiveToAuthEnabled);
+                }
             }
             new DumpsysTableLogger(
                     "KeyguardFingerprintListen",
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index e6b9ac8..0d4889a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -23,6 +23,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.settingslib.fuelgauge.BatteryStatus;
+import com.android.systemui.plugins.WeatherData;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 
 import java.util.TimeZone;
@@ -58,6 +59,11 @@
     public void onTimeFormatChanged(String timeFormat) { }
 
     /**
+     * Called when receive new weather data.
+     */
+    public void onWeatherDataChanged(WeatherData data) { }
+
+    /**
      * Called when the carrier PLMN or SPN changes.
      */
     public void onRefreshCarrierInfo() { }
@@ -208,7 +214,7 @@
     public void onBiometricAuthFailed(BiometricSourceType biometricSourceType) { }
 
     /**
-     * Called when a biometric is recognized.
+     * Called when a biometric is authenticated.
      * @param userId the user id for which the biometric sample was authenticated
      * @param biometricSourceType
      */
@@ -216,6 +222,14 @@
             boolean isStrongBiometric) { }
 
     /**
+     * Called when a biometric is detected but not successfully authenticated.
+     * @param userId the user id for which the biometric sample was detected
+     * @param biometricSourceType
+     */
+    public void onBiometricDetected(int userId, BiometricSourceType biometricSourceType,
+            boolean isStrongBiometric) { }
+
+    /**
      * Called when biometric authentication provides help string (e.g. "Try again")
      * @param msgId
      * @param helpString
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index bde0692..a678edc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -22,20 +22,24 @@
 import android.view.ViewPropertyAnimator;
 
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import com.google.errorprone.annotations.CompileTimeConstant;
+
 /**
  * Helper class for updating visibility of keyguard views based on keyguard and status bar state.
  * This logic is shared by both the keyguard status view and the keyguard user switcher.
  */
 public class KeyguardVisibilityHelper {
+    private static final String TAG = "KeyguardVisibilityHelper";
 
     private View mView;
     private final KeyguardStateController mKeyguardStateController;
@@ -44,29 +48,33 @@
     private boolean mAnimateYPos;
     private boolean mKeyguardViewVisibilityAnimating;
     private boolean mLastOccludedState = false;
-    private boolean mIsUnoccludeTransitionFlagEnabled = false;
     private final AnimationProperties mAnimationProperties = new AnimationProperties();
+    private final LogBuffer mLogBuffer;
 
     public KeyguardVisibilityHelper(View view,
             KeyguardStateController keyguardStateController,
             DozeParameters dozeParameters,
             ScreenOffAnimationController screenOffAnimationController,
-            boolean animateYPos) {
+            boolean animateYPos,
+            LogBuffer logBuffer) {
         mView = view;
         mKeyguardStateController = keyguardStateController;
         mDozeParameters = dozeParameters;
         mScreenOffAnimationController = screenOffAnimationController;
         mAnimateYPos = animateYPos;
+        mLogBuffer = logBuffer;
+    }
+
+    private void log(@CompileTimeConstant String message) {
+        if (mLogBuffer != null) {
+            mLogBuffer.log(TAG, LogLevel.DEBUG, message);
+        }
     }
 
     public boolean isVisibilityAnimating() {
         return mKeyguardViewVisibilityAnimating;
     }
 
-    public void setOcclusionTransitionFlagEnabled(boolean enabled) {
-        mIsUnoccludeTransitionFlagEnabled = enabled;
-    }
-
     /**
      * Set the visibility of a keyguard view based on some new state.
      */
@@ -94,6 +102,9 @@
                         .setStartDelay(mKeyguardStateController.getKeyguardFadingAwayDelay())
                         .setDuration(mKeyguardStateController.getShortenedFadingAwayDuration())
                         .start();
+                log("goingToFullShade && keyguardFadingAway");
+            } else {
+                log("goingToFullShade && !keyguardFadingAway");
             }
         } else if (oldStatusBarState == StatusBarState.SHADE_LOCKED && statusBarState == KEYGUARD) {
             mView.setVisibility(View.VISIBLE);
@@ -105,6 +116,7 @@
                     .setDuration(320)
                     .setInterpolator(Interpolators.ALPHA_IN)
                     .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
+            log("keyguardFadingAway transition w/ Y Aniamtion");
         } else if (statusBarState == KEYGUARD) {
             if (keyguardFadingAway) {
                 mKeyguardViewVisibilityAnimating = true;
@@ -125,33 +137,25 @@
                             true /* animate */);
                     animator.setDuration(duration)
                             .setStartDelay(delay);
+                    log("keyguardFadingAway transition w/ Y Aniamtion");
+                } else {
+                    log("keyguardFadingAway transition w/o Y Animation");
                 }
                 animator.start();
             } else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) {
+                log("ScreenOff transition");
                 mKeyguardViewVisibilityAnimating = true;
 
                 // Ask the screen off animation controller to animate the keyguard visibility for us
                 // since it may need to be cancelled due to keyguard lifecycle events.
                 mScreenOffAnimationController.animateInKeyguard(
                         mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
-            } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
-                // An activity was displayed over the lock screen, and has now gone away
-                mView.setVisibility(View.VISIBLE);
-                mView.setAlpha(0f);
-
-                mView.animate()
-                        .setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP)
-                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                        .alpha(1f)
-                        .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable)
-                        .start();
             } else {
+                log("Direct set Visibility to VISIBLE");
                 mView.setVisibility(View.VISIBLE);
-                if (!mIsUnoccludeTransitionFlagEnabled) {
-                    mView.setAlpha(1f);
-                }
             }
         } else {
+            log("Direct set Visibility to GONE");
             mView.setVisibility(View.GONE);
             mView.setAlpha(1f);
         }
@@ -162,14 +166,18 @@
     private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = () -> {
         mKeyguardViewVisibilityAnimating = false;
         mView.setVisibility(View.INVISIBLE);
+        log("Callback Set Visibility to INVISIBLE");
     };
 
     private final Runnable mAnimateKeyguardStatusViewGoneEndRunnable = () -> {
         mKeyguardViewVisibilityAnimating = false;
         mView.setVisibility(View.GONE);
+        log("CallbackSet Visibility to GONE");
     };
 
     private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> {
         mKeyguardViewVisibilityAnimating = false;
+        mView.setVisibility(View.VISIBLE);
+        log("Callback Set Visibility to VISIBLE");
     };
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 1322f16..0887b22 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -429,6 +429,7 @@
         pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
         pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
         pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
+        pw.println(" mDefaultPaddingPx: " + mDefaultPaddingPx);
 
         if (mView != null) {
             mView.dump(pw, args);
@@ -465,6 +466,17 @@
         }
     }
 
+    /**
+     * @return whether the userUnlockedWithBiometric state changed
+     */
+    private boolean updateUserUnlockedWithBiometric() {
+        final boolean wasUserUnlockedWithBiometric = mUserUnlockedWithBiometric;
+        mUserUnlockedWithBiometric =
+                mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
+                        KeyguardUpdateMonitor.getCurrentUser());
+        return wasUserUnlockedWithBiometric != mUserUnlockedWithBiometric;
+    }
+
     private StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
                 @Override
@@ -502,11 +514,7 @@
 
                 @Override
                 public void onBiometricsCleared() {
-                    final boolean wasUserUnlockedWithBiometric = mUserUnlockedWithBiometric;
-                    mUserUnlockedWithBiometric =
-                            mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
-                                    KeyguardUpdateMonitor.getCurrentUser());
-                    if (wasUserUnlockedWithBiometric != mUserUnlockedWithBiometric) {
+                    if (updateUserUnlockedWithBiometric()) {
                         updateVisibility();
                     }
                 }
@@ -515,10 +523,8 @@
                 public void onBiometricRunningStateChanged(boolean running,
                         BiometricSourceType biometricSourceType) {
                     final boolean wasRunningFps = mRunningFPS;
-                    final boolean wasUserUnlockedWithBiometric = mUserUnlockedWithBiometric;
-                    mUserUnlockedWithBiometric =
-                            mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
-                                    KeyguardUpdateMonitor.getCurrentUser());
+                    final boolean userUnlockedWithBiometricChanged =
+                            updateUserUnlockedWithBiometric();
 
                     if (biometricSourceType == FINGERPRINT) {
                         mRunningFPS = running;
@@ -536,8 +542,7 @@
                         }
                     }
 
-                    if (wasUserUnlockedWithBiometric != mUserUnlockedWithBiometric
-                            || wasRunningFps != mRunningFPS) {
+                    if (userUnlockedWithBiometricChanged || wasRunningFps != mRunningFPS) {
                         updateVisibility();
                     }
                 }
@@ -548,6 +553,7 @@
         @Override
         public void onUnlockedChanged() {
             mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
+            updateUserUnlockedWithBiometric();
             updateKeyguardShowing();
             updateVisibility();
         }
@@ -565,9 +571,7 @@
 
             updateKeyguardShowing();
             if (mIsKeyguardShowing) {
-                mUserUnlockedWithBiometric =
-                    mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
-                        KeyguardUpdateMonitor.getCurrentUser());
+                updateUserUnlockedWithBiometric();
             }
             updateVisibility();
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
index 41111e3..ad66909 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
@@ -92,12 +92,16 @@
     }
 
     void onLayout(int height) {
+        boolean shouldUpdateHeight = height != mHeight;
         mHeight = height;
         mStartRadius = height / 2f;
         mEndRadius = height / 4f;
-        mBackground.setCornerRadius(mStartRadius);
         mExpandAnimator.setFloatValues(mStartRadius, mEndRadius);
         mContractAnimator.setFloatValues(mEndRadius, mStartRadius);
+        // Set initial corner radius.
+        if (shouldUpdateHeight) {
+            mBackground.setCornerRadius(mStartRadius);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
index 0a4880e..3b0644e 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
@@ -33,7 +33,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.internal.widget.LockPatternUtils;
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 
@@ -46,7 +45,6 @@
 
     private final TextView mDigitText;
     private final TextView mKlondikeText;
-    private final LockPatternUtils mLockPatternUtils;
     private final PowerManager mPM;
 
     private int mDigit = -1;
@@ -107,7 +105,6 @@
         setOnHoverListener(new LiftToActivateListener(
                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE)));
 
-        mLockPatternUtils = new LockPatternUtils(context);
         mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
index b159714..35cae09 100644
--- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
+++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
@@ -47,7 +47,6 @@
 import com.android.systemui.R;
 
 import java.util.ArrayList;
-import java.util.Stack;
 
 /**
  * A View similar to a textView which contains password text and can animate when the text is
@@ -92,7 +91,6 @@
     private final int mGravity;
     private ArrayList<CharState> mTextChars = new ArrayList<>();
     private String mText = "";
-    private Stack<CharState> mCharPool = new Stack<>();
     private int mDotSize;
     private PowerManager mPM;
     private int mCharPadding;
@@ -310,13 +308,7 @@
     }
 
     private CharState obtainCharState(char c) {
-        CharState charState;
-        if(mCharPool.isEmpty()) {
-            charState = new CharState();
-        } else {
-            charState = mCharPool.pop();
-            charState.reset();
-        }
+        CharState charState = new CharState();
         charState.whichChar = c;
         return charState;
     }
@@ -343,8 +335,6 @@
                 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION;
                 charState.startRemoveAnimation(startDelay, maxDelay);
                 charState.removeDotSwapCallbacks();
-            } else {
-                mCharPool.push(charState);
             }
         }
         if (!animated) {
@@ -421,8 +411,6 @@
             public void onAnimationEnd(Animator animation) {
                 if (!mCancelled) {
                     mTextChars.remove(CharState.this);
-                    mCharPool.push(CharState.this);
-                    reset();
                     cancelAnimator(textTranslateAnimator);
                     textTranslateAnimator = null;
                 }
@@ -518,21 +506,6 @@
             }
         };
 
-        void reset() {
-            whichChar = 0;
-            currentTextSizeFactor = 0.0f;
-            currentDotSizeFactor = 0.0f;
-            currentWidthFactor = 0.0f;
-            cancelAnimator(textAnimator);
-            textAnimator = null;
-            cancelAnimator(dotAnimator);
-            dotAnimator = null;
-            cancelAnimator(widthAnimator);
-            widthAnimator = null;
-            currentTextTranslationY = 1.0f;
-            removeDotSwapCallbacks();
-        }
-
         void startRemoveAnimation(long startDelay, long widthDelay) {
             boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null)
                     || (dotAnimator != null && dotAnimationIsGrowing);
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index 676979c..b1a83fb 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -18,13 +18,12 @@
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.os.Handler;
-import android.os.UserHandle;
 import android.view.LayoutInflater;
 
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -34,6 +33,8 @@
 
 import dagger.Module;
 import dagger.Provides;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.CoroutineScope;
 
 /** Dagger Module for clocks. */
 @Module
@@ -44,17 +45,23 @@
     public static ClockRegistry getClockRegistry(
             @Application Context context,
             PluginManager pluginManager,
-            @Main Handler handler,
+            @Application CoroutineScope scope,
+            @Main CoroutineDispatcher mainDispatcher,
+            @Background CoroutineDispatcher bgDispatcher,
             FeatureFlags featureFlags,
             @Main Resources resources,
             LayoutInflater layoutInflater) {
-        return new ClockRegistry(
+        ClockRegistry registry = new ClockRegistry(
                 context,
                 pluginManager,
-                handler,
+                scope,
+                mainDispatcher,
+                bgDispatcher,
                 featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
-                UserHandle.USER_ALL,
+                /* handleAllUsers= */ true,
                 new DefaultClockProvider(context, layoutInflater, resources),
                 context.getString(R.string.lockscreen_clock_id_fallback));
+        registry.registerListeners();
+        return registry;
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
index 0cbf8bc..154b0ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
@@ -18,15 +18,15 @@
 
 import android.view.ViewGroup;
 
-import com.android.keyguard.KeyguardHostViewController;
+import com.android.keyguard.KeyguardSecurityContainerController;
 import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
 
 /**
- * Dagger Subcomponent for the {@link KeyguardBouncer}.
+ * Dagger Subcomponent for the {@link PrimaryBouncerInteractor}.
  */
 @Subcomponent(modules = {KeyguardBouncerModule.class})
 @KeyguardBouncerScope
@@ -37,6 +37,6 @@
         KeyguardBouncerComponent create(@BindsInstance @RootView ViewGroup bouncerContainer);
     }
 
-    /** Returns a {@link KeyguardHostViewController}. */
-    KeyguardHostViewController getKeyguardHostViewController();
+    /** Returns a {@link KeyguardSecurityContainerController}. */
+    KeyguardSecurityContainerController getSecurityContainerController();
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
index ef067b8..38f252a 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
@@ -23,13 +23,12 @@
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
-import com.android.keyguard.KeyguardHostView;
 import com.android.keyguard.KeyguardSecurityContainer;
 import com.android.keyguard.KeyguardSecurityViewFlipper;
 import com.android.systemui.R;
 import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 
 import java.util.Optional;
 
@@ -39,7 +38,7 @@
 import dagger.Provides;
 
 /**
- * Module to create and access view related to the {@link KeyguardBouncer}.
+ * Module to create and access view related to the {@link PrimaryBouncerInteractor}.
  */
 @Module
 public interface KeyguardBouncerModule {
@@ -47,19 +46,13 @@
     /** */
     @Provides
     @KeyguardBouncerScope
-    static KeyguardHostView providesKeyguardHostView(@RootView ViewGroup rootView,
+    static KeyguardSecurityContainer providesKeyguardSecurityContainer(@RootView ViewGroup rootView,
             LayoutInflater layoutInflater) {
-        KeyguardHostView hostView = (KeyguardHostView) layoutInflater.inflate(
-                R.layout.keyguard_host_view, rootView, false);
-        rootView.addView(hostView);
-        return hostView;
-    }
-
-    /** */
-    @Provides
-    @KeyguardBouncerScope
-    static KeyguardSecurityContainer providesKeyguardSecurityContainer(KeyguardHostView hostView) {
-        return hostView.findViewById(R.id.keyguard_security_container);
+        KeyguardSecurityContainer securityContainer =
+                (KeyguardSecurityContainer) layoutInflater.inflate(
+                        R.layout.keyguard_security_container_view, rootView, false);
+        rootView.addView(securityContainer);
+        return securityContainer;
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index b84fb08..379c78a 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,37 +16,49 @@
 
 package com.android.keyguard.logging
 
+import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController
 import com.android.systemui.log.dagger.KeyguardLog
-import com.android.systemui.plugins.log.ConstantStringsLogger
-import com.android.systemui.plugins.log.ConstantStringsLoggerImpl
 import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel.DEBUG
-import com.android.systemui.plugins.log.LogLevel.ERROR
-import com.android.systemui.plugins.log.LogLevel.INFO
-import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.KeyguardIndicationController
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
-private const val TAG = "KeyguardLog"
+private const val BIO_TAG = "KeyguardLog"
 
 /**
  * Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding
  * temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be
  * an overkill.
  */
-class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) :
-    ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) {
+class KeyguardLogger
+@Inject
+constructor(
+    @KeyguardLog val buffer: LogBuffer,
+) {
+    @JvmOverloads
+    fun log(
+        tag: String,
+        level: LogLevel,
+        @CompileTimeConstant msg: String,
+        ex: Throwable? = null,
+    ) = buffer.log(tag, level, msg, ex)
 
-    fun logException(ex: Exception, @CompileTimeConstant logMsg: String) {
-        buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex)
-    }
-
-    fun v(msg: String, arg: Any) {
-        buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" })
-    }
-
-    fun i(msg: String, arg: Any) {
-        buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
+    fun log(
+        tag: String,
+        level: LogLevel,
+        @CompileTimeConstant msg: String,
+        arg: Any,
+    ) {
+        buffer.log(
+            tag,
+            level,
+            {
+                str1 = msg
+                str2 = arg.toString()
+            },
+            { "$str1: $str2" }
+        )
     }
 
     @JvmOverloads
@@ -56,8 +68,8 @@
         msg: String? = null
     ) {
         buffer.log(
-            TAG,
-            DEBUG,
+            BIO_TAG,
+            LogLevel.DEBUG,
             {
                 str1 = context
                 str2 = "$msgId"
@@ -66,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/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 21d3b24..e53f6ad 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -132,6 +132,12 @@
         logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" })
     }
 
+    fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) {
+        logBuffer.log(TAG, DEBUG,
+                { bool1 = isFaceUnlockPossible },
+                {"isUnlockWithFacePossible: $bool1"})
+    }
+
     fun logFingerprintAuthForWrongUser(authUserId: Int) {
         logBuffer.log(TAG, DEBUG,
                 { int1 = authUserId },
@@ -161,6 +167,20 @@
         }, {"Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1"})
     }
 
+    fun logFaceDetected(userId: Int, isStrongBiometric: Boolean) {
+        logBuffer.log(TAG, DEBUG, {
+            int1 = userId
+            bool1 = isStrongBiometric
+        }, {"Face detected: userId: $int1, isStrongBiometric: $bool1"})
+    }
+
+    fun logFingerprintDetected(userId: Int, isStrongBiometric: Boolean) {
+        logBuffer.log(TAG, DEBUG, {
+            int1 = userId
+            bool1 = isStrongBiometric
+        }, {"Fingerprint detected: userId: $int1, isStrongBiometric: $bool1"})
+    }
+
     fun logFingerprintError(msgId: Int, originalErrMsg: String) {
         logBuffer.log(TAG, DEBUG, {
             str1 = originalErrMsg
@@ -366,7 +386,7 @@
     }
 
     fun logUserRequestedUnlock(
-        requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN,
+        requestOrigin: ActiveUnlockConfig.ActiveUnlockRequestOrigin,
         reason: String?,
         dismissKeyguard: Boolean
     ) {
@@ -425,4 +445,20 @@
             str1 = PowerManager.wakeReasonToString(pmWakeReason)
         }, { "Skip updating face listening state on wakeup from $str1"})
     }
+
+    fun logTaskStackChangedForAssistant(assistantVisible: Boolean) {
+        logBuffer.log(TAG, VERBOSE, {
+            bool1 = assistantVisible
+        }, {
+            "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1"
+        })
+    }
+
+    fun logAssistantVisible(assistantVisible: Boolean) {
+        logBuffer.log(TAG, VERBOSE, {
+            bool1 = assistantVisible
+        }, {
+            "Updating mAssistantVisible to new value: $bool1"
+        })
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
new file mode 100644
index 0000000..249b3fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.keyguard.logging
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.TrustModel
+import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+/** Logging helper for trust repository. */
+@SysUISingleton
+class TrustRepositoryLogger
+@Inject
+constructor(
+    @KeyguardUpdateMonitorLog private val logBuffer: LogBuffer,
+) {
+    fun onTrustChanged(
+        enabled: Boolean,
+        newlyUnlocked: Boolean,
+        userId: Int,
+        flags: Int,
+        trustGrantedMessages: List<String>?
+    ) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = enabled
+                bool2 = newlyUnlocked
+                int1 = userId
+                int2 = flags
+                str1 = trustGrantedMessages?.joinToString()
+            },
+            {
+                "onTrustChanged enabled: $bool1, newlyUnlocked: $bool2, " +
+                    "userId: $int1, flags: $int2, grantMessages: $str1"
+            }
+        )
+    }
+
+    fun trustListenerRegistered() {
+        logBuffer.log(TAG, LogLevel.VERBOSE, "TrustRepository#registerTrustListener")
+    }
+
+    fun trustListenerUnregistered() {
+        logBuffer.log(TAG, LogLevel.VERBOSE, "TrustRepository#unregisterTrustListener")
+    }
+
+    fun trustModelEmitted(value: TrustModel) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = value.userId
+                bool1 = value.isTrusted
+            },
+            { "trustModel emitted: userId: $int1 isTrusted: $bool1" }
+        )
+    }
+
+    fun isCurrentUserTrusted(isCurrentUserTrusted: Boolean) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { bool1 = isCurrentUserTrusted },
+            { "isCurrentUserTrusted emitted: $bool1" }
+        )
+    }
+
+    companion object {
+        const val TAG = "TrustRepositoryLog"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
index 0f00a04..603471b 100644
--- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
+++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
@@ -73,6 +73,10 @@
     @BinderThread
     fun onScreenTurnedOn() {
         foldAodAnimationController?.onScreenTurnedOn()
+    }
+
+    @BinderThread
+    fun onScreenTurnedOff() {
         pendingTasks.reset()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
index 9ac45b3..227f0ace 100644
--- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -34,7 +34,7 @@
     override fun start() {
         coroutineScope.launch {
             val listener = FlagListenable.Listener { event ->
-                if (event.flagId == Flags.CHOOSER_UNBUNDLED.id) {
+                if (event.flagName == Flags.CHOOSER_UNBUNDLED.name) {
                     launch { updateUnbundledChooserEnabled() }
                     event.requestNoRestart()
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index 3e0fa45..54939fd 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -35,6 +35,7 @@
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.settingslib.Utils
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.log.ScreenDecorationsLogger
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import java.util.concurrent.Executor
 
@@ -47,7 +48,8 @@
     pos: Int,
     val statusBarStateController: StatusBarStateController,
     val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    val mainExecutor: Executor
+    val mainExecutor: Executor,
+    val logger: ScreenDecorationsLogger,
 ) : ScreenDecorations.DisplayCutoutView(context, pos) {
     private var showScanningAnim = false
     private val rimPaint = Paint()
@@ -55,6 +57,7 @@
     private var rimAnimator: AnimatorSet? = null
     private val rimRect = RectF()
     private var cameraProtectionColor = Color.BLACK
+
     var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
             R.attr.wallpaperTextColorAccent)
     private var cameraProtectionAnimator: ValueAnimator? = null
@@ -175,15 +178,22 @@
         }
         if (showScanningAnim) {
             // Make sure that our measured height encompasses the extra space for the animation
-            mTotalBounds.union(mBoundingRect)
+            mTotalBounds.set(mBoundingRect)
             mTotalBounds.union(
                 rimRect.left.toInt(),
                 rimRect.top.toInt(),
                 rimRect.right.toInt(),
                 rimRect.bottom.toInt())
-            setMeasuredDimension(
-                resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
-                resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+            val measuredWidth = resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0)
+            val measuredHeight = resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)
+            logger.boundingRect(rimRect, "onMeasure: Face scanning animation")
+            logger.boundingRect(mBoundingRect, "onMeasure: Display cutout view bounding rect")
+            logger.boundingRect(mTotalBounds, "onMeasure: TotalBounds")
+            logger.onMeasureDimensions(widthMeasureSpec,
+                    heightMeasureSpec,
+                    measuredWidth,
+                    measuredHeight)
+            setMeasuredDimension(measuredWidth, measuredHeight)
         } else {
             setMeasuredDimension(
                 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index e6f559b..fb65588 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -36,10 +36,10 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.hardware.display.DisplayManager;
 import android.hardware.graphics.common.AlphaInterpretation;
 import android.hardware.graphics.common.DisplayDecorationSupport;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.provider.Settings.Secure;
@@ -64,6 +64,7 @@
 
 import com.android.internal.util.Preconditions;
 import com.android.settingslib.Utils;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.decor.CutoutDecorProviderFactory;
@@ -75,7 +76,9 @@
 import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
 import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
 import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
 import com.android.systemui.qs.SettingObserver;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
 import com.android.systemui.tuner.TunerService;
@@ -119,8 +122,11 @@
             R.id.display_cutout_right,
             R.id.display_cutout_bottom
     };
+    private final ScreenDecorationsLogger mLogger;
 
-    private DisplayManager mDisplayManager;
+    private final AuthController mAuthController;
+
+    private DisplayTracker mDisplayTracker;
     @VisibleForTesting
     protected boolean mIsRegistered;
     private final Context mContext;
@@ -128,7 +134,7 @@
     private final TunerService mTunerService;
     private final SecureSettings mSecureSettings;
     @VisibleForTesting
-    DisplayManager.DisplayListener mDisplayListener;
+    DisplayTracker.Callback mDisplayListener;
     private CameraAvailabilityListener mCameraListener;
     private final UserTracker mUserTracker;
     private final PrivacyDotViewController mDotViewController;
@@ -152,6 +158,7 @@
     private WindowManager mWindowManager;
     private int mRotation;
     private SettingObserver mColorInversionSetting;
+    @Nullable
     private DelayableExecutor mExecutor;
     private Handler mHandler;
     boolean mPendingConfigChange;
@@ -171,6 +178,7 @@
             DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
                     mFaceScanningViewId);
             if (overlay != null) {
+                mLogger.cameraProtectionBoundsForScanningOverlay(bounds);
                 overlay.setProtection(protectionPath, bounds);
                 overlay.enableShowProtection(true);
                 updateOverlayWindowVisibilityIfViewExists(
@@ -183,6 +191,7 @@
         }
 
         if (mScreenDecorHwcLayer != null) {
+            mLogger.hwcLayerCameraProtectionBounds(bounds);
             mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
             mScreenDecorHwcLayer.enableShowProtection(true);
             return;
@@ -196,11 +205,12 @@
             }
             ++setProtectionCnt;
             final DisplayCutoutView dcv = (DisplayCutoutView) view;
+            mLogger.dcvCameraBounds(id, bounds);
             dcv.setProtection(protectionPath, bounds);
             dcv.enableShowProtection(true);
         }
         if (setProtectionCnt == 0) {
-            Log.e(TAG, "CutoutView not initialized showCameraProtection");
+            mLogger.cutoutViewNotInitialized();
         }
     }
 
@@ -302,22 +312,41 @@
             SecureSettings secureSettings,
             TunerService tunerService,
             UserTracker userTracker,
+            DisplayTracker displayTracker,
             PrivacyDotViewController dotViewController,
             ThreadFactory threadFactory,
             PrivacyDotDecorProviderFactory dotFactory,
-            FaceScanningProviderFactory faceScanningFactory) {
+            FaceScanningProviderFactory faceScanningFactory,
+            ScreenDecorationsLogger logger,
+            AuthController authController) {
         mContext = context;
         mMainExecutor = mainExecutor;
         mSecureSettings = secureSettings;
         mTunerService = tunerService;
         mUserTracker = userTracker;
+        mDisplayTracker = displayTracker;
         mDotViewController = dotViewController;
         mThreadFactory = threadFactory;
         mDotFactory = dotFactory;
         mFaceScanningFactory = faceScanningFactory;
         mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
+        mLogger = logger;
+        mAuthController = authController;
     }
 
+
+    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
+        @Override
+        public void onFaceSensorLocationChanged() {
+            mLogger.onSensorLocationChanged();
+            if (mExecutor != null) {
+                mExecutor.execute(
+                        () -> updateOverlayProviderViews(
+                                new Integer[]{mFaceScanningViewId}));
+            }
+        }
+    };
+
     @Override
     public void start() {
         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
@@ -328,6 +357,7 @@
         mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
         mExecutor.execute(this::startOnScreenDecorationsThread);
         mDotViewController.setUiExecutor(mExecutor);
+        mAuthController.addCallback(mAuthControllerCallback);
     }
 
     private boolean isPrivacyDotEnabled() {
@@ -376,7 +406,6 @@
     private void startOnScreenDecorationsThread() {
         Trace.beginSection("ScreenDecorations#startOnScreenDecorationsThread");
         mWindowManager = mContext.getSystemService(WindowManager.class);
-        mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mContext.getDisplay().getDisplayInfo(mDisplayInfo);
         mRotation = mDisplayInfo.rotation;
         mDisplayMode = mDisplayInfo.getMode();
@@ -393,17 +422,7 @@
         setupDecorations();
         setupCameraListener();
 
-        mDisplayListener = new DisplayManager.DisplayListener() {
-            @Override
-            public void onDisplayAdded(int displayId) {
-                // do nothing
-            }
-
-            @Override
-            public void onDisplayRemoved(int displayId) {
-                // do nothing
-            }
-
+        mDisplayListener = new DisplayTracker.Callback() {
             @Override
             public void onDisplayChanged(int displayId) {
                 mContext.getDisplay().getDisplayInfo(mDisplayInfo);
@@ -474,8 +493,7 @@
                 }
             }
         };
-
-        mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+        mDisplayTracker.addDisplayChangeCallback(mDisplayListener, new HandlerExecutor(mHandler));
         updateConfiguration();
         Trace.endSection();
     }
@@ -1315,7 +1333,7 @@
 
             if (showProtection) {
                 // Make sure that our measured height encompasses the protection
-                mTotalBounds.union(mBoundingRect);
+                mTotalBounds.set(mBoundingRect);
                 mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top,
                         (int) protectionRect.right, (int) protectionRect.bottom);
                 setMeasuredDimension(
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index ffdd861..8578845 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -38,6 +38,7 @@
 import android.util.TimingsTraceLog;
 import android.view.SurfaceControl;
 import android.view.ThreadedRenderer;
+import android.view.View;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.systemui.dagger.GlobalRootComponent;
@@ -112,6 +113,11 @@
         // the theme set there.
         setTheme(R.style.Theme_SystemUI);
 
+        View.setTraceLayoutSteps(
+                SystemProperties.getBoolean("persist.debug.trace_layouts", false));
+        View.setTracedRequestLayoutClassClass(
+                SystemProperties.get("persist.debug.trace_request_layout_class", null));
+
         if (Process.myUserHandle().equals(UserHandle.SYSTEM)) {
             IntentFilter bootCompletedFilter = new
                     IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED);
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 0fc9ef9..632fcdc 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -22,8 +22,6 @@
 import android.os.HandlerThread;
 import android.util.Log;
 
-import androidx.annotation.Nullable;
-
 import com.android.systemui.dagger.GlobalRootComponent;
 import com.android.systemui.dagger.SysUIComponent;
 import com.android.systemui.dagger.WMComponent;
@@ -55,7 +53,6 @@
         mContext = context;
     }
 
-    @Nullable
     protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder();
 
     /**
@@ -72,11 +69,6 @@
      * Starts the initialization process. This stands up the Dagger graph.
      */
     public void init(boolean fromTest) throws ExecutionException, InterruptedException {
-        GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder();
-        if (globalBuilder == null) {
-            return;
-        }
-
         mRootComponent = getGlobalRootComponentBuilder()
                 .context(mContext)
                 .instrumentationTest(fromTest)
@@ -127,7 +119,6 @@
                     .setBackAnimation(Optional.ofNullable(null))
                     .setDesktopMode(Optional.ofNullable(null));
         }
-
         mSysUIComponent = builder.build();
         if (initializeComponents) {
             mSysUIComponent.init();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
index 55c095b..8aa3040 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui
 
-import android.app.Application
 import android.content.Context
 import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent
 import com.android.systemui.dagger.GlobalRootComponent
@@ -25,17 +24,7 @@
  * {@link SystemUIInitializer} that stands up AOSP SystemUI.
  */
 class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) {
-
-    override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? {
-        return when (Application.getProcessName()) {
-            SCREENSHOT_CROSS_PROFILE_PROCESS -> null
-            else -> DaggerReferenceGlobalRootComponent.builder()
-        }
-    }
-
-    companion object {
-        private const val SYSTEMUI_PROCESS = "com.android.systemui"
-        private const val SCREENSHOT_CROSS_PROFILE_PROCESS =
-                "$SYSTEMUI_PROCESS:screenshot_cross_profile"
+    override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder {
+        return DaggerReferenceGlobalRootComponent.builder()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
index fbb909f..2c97d62 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -67,8 +68,8 @@
     }
 
     @Inject
-    public AccessibilityButtonModeObserver(Context context) {
-        super(context, Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
+    public AccessibilityButtonModeObserver(Context context, UserTracker userTracker) {
+        super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
index b32ebcc..53a21b3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
@@ -23,6 +23,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
 
 import javax.inject.Inject;
 
@@ -48,8 +49,8 @@
     }
 
     @Inject
-    public AccessibilityButtonTargetsObserver(Context context) {
-        super(context, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+    public AccessibilityButtonTargetsObserver(Context context, UserTracker userTracker) {
+        super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
new file mode 100644
index 0000000..799a4d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.accessibility
+
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.ColorCorrectionTile
+import com.android.systemui.qs.tiles.ColorInversionTile
+import com.android.systemui.qs.tiles.DreamTile
+import com.android.systemui.qs.tiles.FontScalingTile
+import com.android.systemui.qs.tiles.NightDisplayTile
+import com.android.systemui.qs.tiles.OneHandedModeTile
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface AccessibilityModule {
+
+    /** Inject ColorInversionTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(ColorInversionTile.TILE_SPEC)
+    fun bindColorInversionTile(colorInversionTile: ColorInversionTile): QSTileImpl<*>
+
+    /** Inject NightDisplayTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(NightDisplayTile.TILE_SPEC)
+    fun bindNightDisplayTile(nightDisplayTile: NightDisplayTile): QSTileImpl<*>
+
+    /** Inject ReduceBrightColorsTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(ReduceBrightColorsTile.TILE_SPEC)
+    fun bindReduceBrightColorsTile(reduceBrightColorsTile: ReduceBrightColorsTile): QSTileImpl<*>
+
+    /** Inject OneHandedModeTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(OneHandedModeTile.TILE_SPEC)
+    fun bindOneHandedModeTile(oneHandedModeTile: OneHandedModeTile): QSTileImpl<*>
+
+    /** Inject ColorCorrectionTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(ColorCorrectionTile.TILE_SPEC)
+    fun bindColorCorrectionTile(colorCorrectionTile: ColorCorrectionTile): QSTileImpl<*>
+
+    /** Inject DreamTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(DreamTile.TILE_SPEC)
+    fun bindDreamTile(dreamTile: DreamTile): QSTileImpl<*>
+
+    /** Inject FontScalingTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(FontScalingTile.TILE_SPEC)
+    fun bindFontScalingTile(fontScalingTile: FontScalingTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
index e4e0da6..326773f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
@@ -27,6 +27,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.settings.UserTracker;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -44,6 +45,7 @@
 public abstract class SecureSettingsContentObserver<T> {
 
     private final ContentResolver mContentResolver;
+    private final UserTracker mUserTracker;
     @VisibleForTesting
     final ContentObserver mContentObserver;
 
@@ -52,9 +54,11 @@
     @VisibleForTesting
     final List<T> mListeners = new ArrayList<>();
 
-    protected SecureSettingsContentObserver(Context context, String secureSettingsKey) {
+    protected SecureSettingsContentObserver(Context context, UserTracker userTracker,
+            String secureSettingsKey) {
         mKey = secureSettingsKey;
         mContentResolver = context.getContentResolver();
+        mUserTracker = userTracker;
         mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
             @Override
             public void onChange(boolean selfChange) {
@@ -103,7 +107,7 @@
      * See {@link Settings.Secure}.
      */
     public final String getSettingsValue() {
-        return Settings.Secure.getStringForUser(mContentResolver, mKey, UserHandle.USER_CURRENT);
+        return Settings.Secure.getStringForUser(mContentResolver, mKey, mUserTracker.getUserId());
     }
 
     private void updateValueChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index dab73e9..ddac25b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -35,14 +35,11 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.os.UserHandle;
 import android.util.Log;
-import android.view.Display;
 import android.view.IWindowManager;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
-import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
 
@@ -52,6 +49,8 @@
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -179,7 +178,9 @@
 
     private final SystemActionsBroadcastReceiver mReceiver;
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final Optional<Recents> mRecentsOptional;
+    private final DisplayTracker mDisplayTracker;
     private Locale mLocale;
     private final AccessibilityManager mA11yManager;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -190,13 +191,17 @@
 
     @Inject
     public SystemActions(Context context,
+            UserTracker userTracker,
             NotificationShadeWindowController notificationShadeController,
             ShadeController shadeController,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
-            Optional<Recents> recentsOptional) {
+            Optional<Recents> recentsOptional,
+            DisplayTracker displayTracker) {
         mContext = context;
+        mUserTracker = userTracker;
         mShadeController = shadeController;
         mRecentsOptional = recentsOptional;
+        mDisplayTracker = displayTracker;
         mReceiver = new SystemActionsBroadcastReceiver();
         mLocale = mContext.getResources().getConfiguration().getLocales().get(0);
         mA11yManager = (AccessibilityManager) mContext.getSystemService(
@@ -205,7 +210,8 @@
         // Saving in instance variable since to prevent GC since
         // NotificationShadeWindowController.registerCallback() only keeps weak references.
         mNotificationShadeCallback =
-                (keyguardShowing, keyguardOccluded, bouncerShowing, mDozing, panelExpanded) ->
+                (keyguardShowing, keyguardOccluded, bouncerShowing, mDozing, panelExpanded,
+                            isDreaming) ->
                         registerOrUnregisterDismissNotificationShadeAction();
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
     }
@@ -343,6 +349,7 @@
 
     /**
      * Register a system action.
+     *
      * @param actionId the action ID to register.
      */
     public void register(int actionId) {
@@ -440,6 +447,7 @@
 
     /**
      * Unregister a system action.
+     *
      * @param actionId the action ID to unregister.
      */
     public void unregister(int actionId) {
@@ -475,7 +483,8 @@
     }
 
     private void handleNotifications() {
-        mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::animateExpandNotificationsPanel);
+        mCentralSurfacesOptionalLazy.get().ifPresent(
+                CentralSurfaces::animateExpandNotificationsPanel);
     }
 
     private void handleQuickSettings() {
@@ -507,7 +516,7 @@
 
     private void handleTakeScreenshot() {
         ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
-        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+        screenshotHelper.takeScreenshot(
                 SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null);
     }
 
@@ -517,7 +526,7 @@
 
     private void handleAccessibilityButton() {
         AccessibilityManager.getInstance(mContext).notifyAccessibilityButtonClicked(
-                Display.DEFAULT_DISPLAY);
+                mDisplayTracker.getDefaultDisplayId());
     }
 
     private void handleAccessibilityButtonChooser() {
@@ -525,7 +534,7 @@
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         final String chooserClassName = AccessibilityButtonChooserActivity.class.getName();
         intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
-        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+        mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
     }
 
     private void handleAccessibilityShortcut() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index ab11fce..b3574bf 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -39,6 +39,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.CommandQueue;
 
 import java.io.PrintWriter;
@@ -62,6 +63,7 @@
     private final AccessibilityManager mAccessibilityManager;
     private final CommandQueue mCommandQueue;
     private final OverviewProxyService mOverviewProxyService;
+    private final DisplayTracker mDisplayTracker;
 
     private WindowMagnificationConnectionImpl mWindowMagnificationConnectionImpl;
     private SysUiState mSysUiState;
@@ -102,7 +104,8 @@
     @Inject
     public WindowMagnification(Context context, @Main Handler mainHandler,
             CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
-            SysUiState sysUiState, OverviewProxyService overviewProxyService) {
+            SysUiState sysUiState, OverviewProxyService overviewProxyService,
+            DisplayTracker displayTracker) {
         mContext = context;
         mHandler = mainHandler;
         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
@@ -110,6 +113,7 @@
         mModeSwitchesController = modeSwitchesController;
         mSysUiState = sysUiState;
         mOverviewProxyService = overviewProxyService;
+        mDisplayTracker = displayTracker;
         mMagnificationControllerSupplier = new ControllerSupplier(context,
                 mHandler, this, context.getSystemService(DisplayManager.class), sysUiState);
     }
@@ -130,14 +134,14 @@
     private void updateSysUiStateFlag() {
         //TODO(b/187510533): support multi-display once SysuiState supports it.
         final WindowMagnificationController controller =
-                mMagnificationControllerSupplier.valueAt(Display.DEFAULT_DISPLAY);
+                mMagnificationControllerSupplier.valueAt(mDisplayTracker.getDefaultDisplayId());
         if (controller != null) {
             controller.updateSysUIStateFlag();
         } else {
             // The instance is initialized when there is an IPC request. Considering
             // self-crash cases, we need to reset the flag in such situation.
             mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, false)
-                    .commitUpdate(Display.DEFAULT_DISPLAY);
+                    .commitUpdate(mDisplayTracker.getDefaultDisplayId());
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
index 9af8300..7441e03 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
@@ -43,6 +43,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Prefs;
 import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.util.List;
 
@@ -60,6 +61,7 @@
     private static final float DEFAULT_POSITION_Y_PERCENT = 0.9f;
 
     private final Context mContext;
+    private final SecureSettings mSecureSettings;
     private final AccessibilityFloatingMenuView mMenuView;
     private final MigrationTooltipView mMigrationTooltipView;
     private final DockTooltipView mDockTooltipView;
@@ -77,7 +79,7 @@
             new ContentObserver(mHandler) {
                 @Override
                 public void onChange(boolean selfChange) {
-                    mMenuView.setSizeType(getSizeType(mContext));
+                    mMenuView.setSizeType(getSizeType());
                 }
             };
 
@@ -85,8 +87,8 @@
             new ContentObserver(mHandler) {
                 @Override
                 public void onChange(boolean selfChange) {
-                    mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
-                            getOpacityValue(mContext));
+                    mMenuView.updateOpacityWith(isFadeEffectEnabled(),
+                            getOpacityValue());
                 }
             };
 
@@ -98,16 +100,19 @@
                 }
             };
 
-    public AccessibilityFloatingMenu(Context context) {
+    public AccessibilityFloatingMenu(Context context, SecureSettings secureSettings) {
         mContext = context;
+        mSecureSettings = secureSettings;
         mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context));
         mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
         mDockTooltipView = new DockTooltipView(mContext, mMenuView);
     }
 
     @VisibleForTesting
-    AccessibilityFloatingMenu(Context context, AccessibilityFloatingMenuView menuView) {
+    AccessibilityFloatingMenu(Context context, SecureSettings secureSettings,
+            AccessibilityFloatingMenuView menuView) {
         mContext = context;
+        mSecureSettings = secureSettings;
         mMenuView = menuView;
         mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
         mDockTooltipView = new DockTooltipView(mContext, mMenuView);
@@ -130,10 +135,10 @@
 
         mMenuView.show();
         mMenuView.onTargetsChanged(targetList);
-        mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
-                getOpacityValue(mContext));
-        mMenuView.setSizeType(getSizeType(mContext));
-        mMenuView.setShapeType(getShapeType(mContext));
+        mMenuView.updateOpacityWith(isFadeEffectEnabled(),
+                getOpacityValue());
+        mMenuView.setSizeType(getSizeType());
+        mMenuView.setShapeType(getShapeType());
         mMenuView.setOnDragEndListener(this::onDragEnd);
 
         showMigrationTooltipIfNecessary();
@@ -170,17 +175,17 @@
     // Migration tooltip was the android S feature. It's just used on the Android version from R
     // to S. In addition, it only shows once.
     private void showMigrationTooltipIfNecessary() {
-        if (isMigrationTooltipPromptEnabled(mContext)) {
+        if (isMigrationTooltipPromptEnabled()) {
             mMigrationTooltipView.show();
 
-            Settings.Secure.putInt(mContext.getContentResolver(),
+            mSecureSettings.putInt(
                     ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, /* disabled */ 0);
         }
     }
 
-    private static boolean isMigrationTooltipPromptEnabled(Context context) {
-        return Settings.Secure.getInt(
-                context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
+    private boolean isMigrationTooltipPromptEnabled() {
+        return mSecureSettings.getInt(
+                ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
                 DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1;
     }
 
@@ -212,57 +217,61 @@
         }
     }
 
-    private static boolean isFadeEffectEnabled(Context context) {
-        return Settings.Secure.getInt(
-                context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
+    private boolean isFadeEffectEnabled() {
+        return mSecureSettings.getInt(
+                ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
                 DEFAULT_FADE_EFFECT_IS_ENABLED) == /* enabled */ 1;
     }
 
-    private static float getOpacityValue(Context context) {
-        return Settings.Secure.getFloat(
-                context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_OPACITY,
+    private float getOpacityValue() {
+        return mSecureSettings.getFloat(
+                ACCESSIBILITY_FLOATING_MENU_OPACITY,
                 DEFAULT_OPACITY_VALUE);
     }
 
-    private static int getSizeType(Context context) {
-        return Settings.Secure.getInt(
-                context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
+    private int getSizeType() {
+        return mSecureSettings.getInt(
+                ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
     }
 
-    private static int getShapeType(Context context) {
-        return Settings.Secure.getInt(
-                context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
+    private int getShapeType() {
+        return mSecureSettings.getInt(
+                ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
                 ShapeType.OVAL);
     }
 
     private void registerContentObservers() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
                 /* notifyForDescendants */ false, mContentObserver,
                 UserHandle.USER_CURRENT);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+                /* notifyForDescendants */ false, mContentObserver,
+                UserHandle.USER_CURRENT);
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE,
                 /* notifyForDescendants */ false, mSizeContentObserver,
                 UserHandle.USER_CURRENT);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
                 /* notifyForDescendants */ false, mFadeOutContentObserver,
                 UserHandle.USER_CURRENT);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY),
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY,
                 /* notifyForDescendants */ false, mFadeOutContentObserver,
                 UserHandle.USER_CURRENT);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES),
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 /* notifyForDescendants */ false,
                 mEnabledA11yServicesContentObserver, UserHandle.USER_CURRENT);
     }
 
     private void unregisterContentObservers() {
-        mContext.getContentResolver().unregisterContentObserver(mContentObserver);
-        mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver);
-        mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver);
-        mContext.getContentResolver().unregisterContentObserver(
+        mSecureSettings.unregisterContentObserver(mContentObserver);
+        mSecureSettings.unregisterContentObserver(mSizeContentObserver);
+        mSecureSettings.unregisterContentObserver(mFadeOutContentObserver);
+        mSecureSettings.unregisterContentObserver(
                 mEnabledA11yServicesContentObserver);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 403941f..6216b89 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -31,6 +31,7 @@
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver.AccessibilityButtonMode;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.util.settings.SecureSettings;
 
 import javax.inject.Inject;
 
@@ -44,6 +45,7 @@
     private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
     private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final SecureSettings mSecureSettings;
 
     private Context mContext;
     @VisibleForTesting
@@ -85,11 +87,13 @@
     public AccessibilityFloatingMenuController(Context context,
             AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
             AccessibilityButtonModeObserver accessibilityButtonModeObserver,
-            KeyguardUpdateMonitor keyguardUpdateMonitor) {
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            SecureSettings secureSettings) {
         mContext = context;
         mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver;
         mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mSecureSettings = secureSettings;
 
         mIsKeyguardVisible = false;
     }
@@ -159,7 +163,7 @@
 
     private void showFloatingMenu() {
         if (mFloatingMenu == null) {
-            mFloatingMenu = new AccessibilityFloatingMenu(mContext);
+            mFloatingMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings);
         }
 
         mFloatingMenu.show();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
new file mode 100644
index 0000000..54f933a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.accessibility.fontscaling
+
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import android.os.Bundle
+import android.provider.Settings
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.SeekBar
+import android.widget.SeekBar.OnSeekBarChangeListener
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.settings.SystemSettings
+
+/** The Dialog that contains a seekbar for changing the font size. */
+class FontScalingDialog(context: Context, private val systemSettings: SystemSettings) :
+    SystemUIDialog(context) {
+    private val strEntryValues: Array<String> =
+        context.resources.getStringArray(com.android.settingslib.R.array.entryvalues_font_size)
+    private lateinit var title: TextView
+    private lateinit var doneButton: Button
+    private lateinit var seekBarWithIconButtonsView: SeekBarWithIconButtonsView
+
+    private val configuration: Configuration =
+        Configuration(context.getResources().getConfiguration())
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setTitle(R.string.font_scaling_dialog_title)
+        setView(LayoutInflater.from(context).inflate(R.layout.font_scaling_dialog, null))
+        setPositiveButton(
+            R.string.quick_settings_done,
+            /* onClick = */ null,
+            /* dismissOnClick = */ true
+        )
+        super.onCreate(savedInstanceState)
+
+        title = requireViewById(com.android.internal.R.id.alertTitle)
+        doneButton = requireViewById(com.android.internal.R.id.button1)
+        seekBarWithIconButtonsView = requireViewById(R.id.font_scaling_slider)
+
+        seekBarWithIconButtonsView.setMax((strEntryValues).size - 1)
+
+        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, 1.0f)
+        seekBarWithIconButtonsView.setProgress(fontSizeValueToIndex(currentScale))
+
+        seekBarWithIconButtonsView.setOnSeekBarChangeListener(
+            object : OnSeekBarChangeListener {
+                override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+                    systemSettings.putString(Settings.System.FONT_SCALE, strEntryValues[progress])
+                }
+
+                override fun onStartTrackingTouch(seekBar: SeekBar) {
+                    // Do nothing
+                }
+
+                override fun onStopTrackingTouch(seekBar: SeekBar) {
+                    // Do nothing
+                }
+            }
+        )
+        doneButton.setOnClickListener { dismiss() }
+    }
+
+    private fun fontSizeValueToIndex(value: Float): Int {
+        var lastValue = strEntryValues[0].toFloat()
+        for (i in 1 until strEntryValues.size) {
+            val thisValue = strEntryValues[i].toFloat()
+            if (value < lastValue + (thisValue - lastValue) * .5f) {
+                return i - 1
+            }
+            lastValue = thisValue
+        }
+        return strEntryValues.size - 1
+    }
+
+    override fun onConfigurationChanged(configuration: Configuration) {
+        super.onConfigurationChanged(configuration)
+
+        val configDiff = configuration.diff(this.configuration)
+        this.configuration.setTo(configuration)
+
+        if (configDiff and ActivityInfo.CONFIG_FONT_SCALE != 0) {
+            title.post {
+                title.setTextAppearance(R.style.TextAppearance_Dialog_Title)
+                doneButton.setTextAppearance(R.style.Widget_Dialog_Button)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 7c2673c..1ea173e 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -1,7 +1,5 @@
 package com.android.systemui.assist;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
 
@@ -35,8 +33,11 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.util.settings.SecureSettings;
 
 import javax.inject.Inject;
 
@@ -119,6 +120,9 @@
     private final UiController mUiController;
     protected final Lazy<SysUiState> mSysUiState;
     protected final AssistLogger mAssistLogger;
+    private final UserTracker mUserTracker;
+    private final DisplayTracker mDisplayTracker;
+    private final SecureSettings mSecureSettings;
 
     private final DeviceProvisionedController mDeviceProvisionedController;
     private final CommandQueue mCommandQueue;
@@ -135,7 +139,10 @@
             Lazy<SysUiState> sysUiState,
             DefaultUiController defaultUiController,
             AssistLogger assistLogger,
-            @Main Handler uiHandler) {
+            @Main Handler uiHandler,
+            UserTracker userTracker,
+            DisplayTracker displayTracker,
+            SecureSettings secureSettings) {
         mContext = context;
         mDeviceProvisionedController = controller;
         mCommandQueue = commandQueue;
@@ -143,6 +150,9 @@
         mAssistDisclosure = new AssistDisclosure(context, uiHandler);
         mPhoneStateMonitor = phoneStateMonitor;
         mAssistLogger = assistLogger;
+        mUserTracker = userTracker;
+        mDisplayTracker = displayTracker;
+        mSecureSettings = secureSettings;
 
         registerVoiceInteractionSessionListener();
 
@@ -206,7 +216,7 @@
                                     .setFlag(
                                             SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED,
                                             hints.getBoolean(CONSTRAINED_KEY, false))
-                                    .commitUpdate(DEFAULT_DISPLAY);
+                                    .commitUpdate(mDisplayTracker.getDefaultDisplayId());
                         }
                     }
                 });
@@ -273,7 +283,7 @@
                 CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
                 false /* force */);
 
-        boolean structureEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+        boolean structureEnabled = mSecureSettings.getIntForUser(
                 Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
 
         final SearchManager searchManager =
@@ -300,7 +310,7 @@
                 @Override
                 public void run() {
                     mContext.startActivityAsUser(intent, opts.toBundle(),
-                            new UserHandle(UserHandle.USER_CURRENT));
+                            mUserTracker.getUserHandle());
                 }
             });
         } catch (ActivityNotFoundException e) {
diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
index 621b99d..6721c5d 100644
--- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
 import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
 import com.android.systemui.people.widget.PeopleBackupHelper
+import com.android.systemui.settings.UserFileManagerImpl
 
 /**
  * Helper for backing up elements in SystemUI
@@ -58,17 +59,8 @@
 
     override fun onCreate(userHandle: UserHandle, operationType: Int) {
         super.onCreate()
-        // The map in mapOf is guaranteed to be order preserving
-        val controlsMap = mapOf(CONTROLS to getPPControlsFile(this))
-        NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also {
-            addHelper(NO_OVERWRITE_FILES_BACKUP_KEY, it)
-        }
 
-        // Conversations widgets backup only works for system user, because widgets' information is
-        // stored in system user's SharedPreferences files and we can't open those from other users.
-        if (!userHandle.isSystem) {
-            return
-        }
+        addControlsHelper(userHandle.identifier)
 
         val keys = PeopleBackupHelper.getFilesToBackup()
         addHelper(
@@ -95,6 +87,18 @@
         sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF)
     }
 
+    private fun addControlsHelper(userId: Int) {
+        val file = UserFileManagerImpl.createFile(
+            userId = userId,
+            fileName = CONTROLS,
+        )
+        // The map in mapOf is guaranteed to be order preserving
+        val controlsMap = mapOf(file.getPath() to getPPControlsFile(this, userId))
+        NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also {
+            addHelper(NO_OVERWRITE_FILES_BACKUP_KEY, it)
+        }
+    }
+
     /**
      * Helper class for restoring files ONLY if they are not present.
      *
@@ -136,17 +140,21 @@
     }
 }
 
-private fun getPPControlsFile(context: Context): () -> Unit {
+private fun getPPControlsFile(context: Context, userId: Int): () -> Unit {
     return {
-        val filesDir = context.filesDir
-        val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
+        val file = UserFileManagerImpl.createFile(
+            userId = userId,
+            fileName = BackupHelper.CONTROLS,
+        )
         if (file.exists()) {
-            val dest =
-                Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
+            val dest = UserFileManagerImpl.createFile(
+                userId = userId,
+                fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME,
+            )
             file.copyTo(dest)
             val jobScheduler = context.getSystemService(JobScheduler::class.java)
             jobScheduler?.schedule(
-                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
+                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context, userId)
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 03d999f..ce42534 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -24,6 +24,7 @@
 import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -195,7 +196,13 @@
         return false;
     }
 
-    void onBatteryLevelChanged(int level, boolean pluggedIn) {
+    /**
+     * Update battery level
+     *
+     * @param level     int between 0 and 100 (representing percentage value)
+     * @param pluggedIn whether the device is plugged in or not
+     */
+    public void onBatteryLevelChanged(@IntRange(from = 0, to = 100) int level, boolean pluggedIn) {
         mDrawable.setCharging(pluggedIn);
         mDrawable.setBatteryLevel(level);
         mCharging = pluggedIn;
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
new file mode 100644
index 0000000..4173790
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.battery
+
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.BatterySaverTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface BatterySaverModule {
+
+    /** Inject BatterySaverTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(BatterySaverTile.TILE_SPEC)
+    fun bindBatterySaverTile(batterySaverTile: BatterySaverTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
index e4c197f..1404053 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
@@ -35,13 +35,13 @@
 
     override val actsAsConfirmButton: Boolean = true
 
-    override fun shouldAnimateForTransition(
+    override fun shouldAnimateIconViewForTransition(
             @BiometricState oldState: Int,
             @BiometricState newState: Int
     ): Boolean = when (newState) {
         STATE_PENDING_CONFIRMATION -> true
         STATE_AUTHENTICATED -> false
-        else -> super.shouldAnimateForTransition(oldState, newState)
+        else -> super.shouldAnimateIconViewForTransition(oldState, newState)
     }
 
     @RawRes
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index b962cc4..709ddf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.RawRes
 import android.content.Context
+import android.content.Context.FINGERPRINT_SERVICE
 import android.content.res.Configuration
 import android.hardware.fingerprint.FingerprintManager
 import android.view.DisplayInfo
@@ -46,6 +47,8 @@
 
     private var isDeviceFolded: Boolean = false
     private val isSideFps: Boolean
+    private val isReverseDefaultRotation =
+            context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
     private val screenSizeFoldProvider: ScreenSizeFoldProvider = ScreenSizeFoldProvider(context)
     var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1)
         set(value) {
@@ -64,19 +67,14 @@
                 R.dimen.biometric_dialog_fingerprint_icon_width),
                 context.resources.getDimensionPixelSize(
                         R.dimen.biometric_dialog_fingerprint_icon_height))
-        var sideFps = false
-        (context.getSystemService(Context.FINGERPRINT_SERVICE)
-                as FingerprintManager?)?.let { fpm ->
-            for (prop in fpm.sensorPropertiesInternal) {
-                if (prop.isAnySidefpsType) {
-                    sideFps = true
-                }
-            }
-        }
-        isSideFps = sideFps
+        isSideFps =
+            (context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager?)?.let { fpm ->
+                fpm.sensorPropertiesInternal.any { it.isAnySidefpsType }
+            } ?: false
+        preloadAssets(context)
         val displayInfo = DisplayInfo()
         context.display?.getDisplayInfo(displayInfo)
-        if (isSideFps && displayInfo.rotation == Surface.ROTATION_180) {
+        if (isSideFps && getRotationFromDefault(displayInfo.rotation) == Surface.ROTATION_180) {
             iconView.rotation = 180f
         }
         screenSizeFoldProvider.registerCallback(this, context.mainExecutor)
@@ -86,7 +84,7 @@
     private fun updateIconSideFps(@BiometricState lastState: Int, @BiometricState newState: Int) {
         val displayInfo = DisplayInfo()
         context.display?.getDisplayInfo(displayInfo)
-        val rotation = displayInfo.rotation
+        val rotation = getRotationFromDefault(displayInfo.rotation)
         val iconAnimation = getSideFpsAnimationForTransition(rotation)
         val iconViewOverlayAnimation =
                 getSideFpsOverlayAnimationForTransition(lastState, newState, rotation) ?: return
@@ -104,12 +102,14 @@
 
         iconView.frame = 0
         iconViewOverlay.frame = 0
-        if (shouldAnimateForTransition(lastState, newState)) {
-            iconView.playAnimation()
-            iconViewOverlay.playAnimation()
-        } else if (lastState == STATE_IDLE && newState == STATE_AUTHENTICATING_ANIMATING_IN) {
+        if (shouldAnimateSfpsIconViewForTransition(lastState, newState)) {
             iconView.playAnimation()
         }
+
+        if (shouldAnimateIconViewOverlayForTransition(lastState, newState)) {
+            iconViewOverlay.playAnimation()
+        }
+
         LottieColorUtils.applyDynamicColors(context, iconView)
         LottieColorUtils.applyDynamicColors(context, iconViewOverlay)
     }
@@ -127,7 +127,7 @@
         }
 
         iconView.frame = 0
-        if (shouldAnimateForTransition(lastState, newState)) {
+        if (shouldAnimateIconViewForTransition(lastState, newState)) {
             iconView.playAnimation()
         }
         LottieColorUtils.applyDynamicColors(context, iconView)
@@ -160,7 +160,32 @@
         return if (id != null) context.getString(id) else null
     }
 
-    protected open fun shouldAnimateForTransition(
+    protected open fun shouldAnimateIconViewForTransition(
+            @BiometricState oldState: Int,
+            @BiometricState newState: Int
+    ) = when (newState) {
+        STATE_HELP,
+        STATE_ERROR -> true
+        STATE_AUTHENTICATING_ANIMATING_IN,
+        STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP
+        STATE_AUTHENTICATED -> true
+        else -> false
+    }
+
+    private fun shouldAnimateSfpsIconViewForTransition(
+            @BiometricState oldState: Int,
+            @BiometricState newState: Int
+    ) = when (newState) {
+        STATE_HELP,
+        STATE_ERROR -> true
+        STATE_AUTHENTICATING_ANIMATING_IN,
+        STATE_AUTHENTICATING ->
+            oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE
+        STATE_AUTHENTICATED -> true
+        else -> false
+    }
+
+    protected open fun shouldAnimateIconViewOverlayForTransition(
             @BiometricState oldState: Int,
             @BiometricState newState: Int
     ) = when (newState) {
@@ -202,22 +227,33 @@
         return if (id != null) return id else null
     }
 
+    private fun getRotationFromDefault(rotation: Int): Int =
+            if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
+
     @RawRes
-    private fun getSideFpsAnimationForTransition(rotation: Int): Int = when (rotation) {
-        Surface.ROTATION_90 -> if (isDeviceFolded) {
-            R.raw.biometricprompt_folded_base_topleft
-        } else {
-            R.raw.biometricprompt_portrait_base_topleft
-        }
-        Surface.ROTATION_270 -> if (isDeviceFolded) {
-            R.raw.biometricprompt_folded_base_bottomright
-        } else {
-            R.raw.biometricprompt_portrait_base_bottomright
-        }
-        else -> if (isDeviceFolded) {
-            R.raw.biometricprompt_folded_base_default
-        } else {
-            R.raw.biometricprompt_landscape_base
+    private fun getSideFpsAnimationForTransition(rotation: Int): Int {
+        when (rotation) {
+            Surface.ROTATION_90 -> if (context.isInRearDisplayMode()) {
+                return R.raw.biometricprompt_rear_portrait_reverse_base
+            } else if (isDeviceFolded) {
+                return R.raw.biometricprompt_folded_base_topleft
+            } else {
+                return R.raw.biometricprompt_portrait_base_topleft
+            }
+            Surface.ROTATION_270 -> if (context.isInRearDisplayMode()) {
+                return R.raw.biometricprompt_rear_portrait_base
+            } else if (isDeviceFolded) {
+                return R.raw.biometricprompt_folded_base_bottomright
+            } else {
+                return R.raw.biometricprompt_portrait_base_bottomright
+            }
+            else -> if (context.isInRearDisplayMode()) {
+                return R.raw.biometricprompt_rear_landscape_base
+            } else if (isDeviceFolded) {
+                return R.raw.biometricprompt_folded_base_default
+            } else {
+                return R.raw.biometricprompt_landscape_base
+            }
         }
     }
 
@@ -297,6 +333,40 @@
         else -> null
     }
 
+    private fun preloadAssets(context: Context) {
+        if (isSideFps) {
+            cacheLottieAssetsInContext(
+                context,
+                R.raw.biometricprompt_fingerprint_to_error_landscape,
+                R.raw.biometricprompt_folded_base_bottomright,
+                R.raw.biometricprompt_folded_base_default,
+                R.raw.biometricprompt_folded_base_topleft,
+                R.raw.biometricprompt_landscape_base,
+                R.raw.biometricprompt_portrait_base_bottomright,
+                R.raw.biometricprompt_portrait_base_topleft,
+                R.raw.biometricprompt_symbol_error_to_fingerprint_landscape,
+                R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright,
+                R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft,
+                R.raw.biometricprompt_symbol_error_to_success_landscape,
+                R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright,
+                R.raw.biometricprompt_symbol_error_to_success_portrait_topleft,
+                R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright,
+                R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft,
+                R.raw.biometricprompt_symbol_fingerprint_to_success_landscape,
+                R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright,
+                R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft
+            )
+        } else {
+            cacheLottieAssetsInContext(
+                context,
+                R.raw.fingerprint_dialogue_error_to_fingerprint_lottie,
+                R.raw.fingerprint_dialogue_error_to_success_lottie,
+                R.raw.fingerprint_dialogue_fingerprint_to_error_lottie,
+                R.raw.fingerprint_dialogue_fingerprint_to_success_lottie
+            )
+        }
+    }
+
     override fun onFoldUpdated(isFolded: Boolean) {
         isDeviceFolded = isFolded
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index e12c170..a7b6e6a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -658,6 +658,7 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         mIconController.onConfigurationChanged(newConfig);
+        updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 092339a..3302073 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -18,6 +18,7 @@
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
 
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
@@ -76,6 +77,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeReceiver;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.data.repository.BiometricType;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -85,8 +87,10 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
@@ -125,7 +129,6 @@
     private float mScaleFactor = 1f;
     // sensor locations without any resolution scaling nor rotation adjustments:
     @Nullable private final Point mFaceSensorLocationDefault;
-    @Nullable private final Point mFingerprintSensorLocationDefault;
     // cached sensor locations:
     @Nullable private Point mFaceSensorLocation;
     @Nullable private Point mFingerprintSensorLocation;
@@ -150,6 +153,7 @@
     @Nullable private List<FingerprintSensorPropertiesInternal> mUdfpsProps;
     @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
 
+    @NonNull private final Map<Integer, Boolean> mFpEnrolledForUser = new HashMap<>();
     @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
     @NonNull private final SparseBooleanArray mSfpsEnrolledForUser;
     @NonNull private final SensorPrivacyManager mSensorPrivacyManager;
@@ -161,7 +165,6 @@
     private final @Background DelayableExecutor mBackgroundExecutor;
     private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
 
-
     private final VibratorHelper mVibratorHelper;
 
     private void vibrateSuccess(int modality) {
@@ -331,27 +334,35 @@
         mExecution.assertIsMainThread();
         Log.d(TAG, "handleEnrollmentsChanged, userId: " + userId + ", sensorId: " + sensorId
                 + ", hasEnrollments: " + hasEnrollments);
-        if (mUdfpsProps == null) {
-            Log.d(TAG, "handleEnrollmentsChanged, mUdfpsProps is null");
-        } else {
-            for (FingerprintSensorPropertiesInternal prop : mUdfpsProps) {
+        BiometricType sensorBiometricType = BiometricType.UNKNOWN;
+        if (mFpProps != null) {
+            for (FingerprintSensorPropertiesInternal prop: mFpProps) {
                 if (prop.sensorId == sensorId) {
-                    mUdfpsEnrolledForUser.put(userId, hasEnrollments);
+                    mFpEnrolledForUser.put(userId, hasEnrollments);
+                    if (prop.isAnyUdfpsType()) {
+                        sensorBiometricType = BiometricType.UNDER_DISPLAY_FINGERPRINT;
+                        mUdfpsEnrolledForUser.put(userId, hasEnrollments);
+                    } else if (prop.isAnySidefpsType()) {
+                        sensorBiometricType = BiometricType.SIDE_FINGERPRINT;
+                        mSfpsEnrolledForUser.put(userId, hasEnrollments);
+                    } else if (prop.sensorType == TYPE_REAR) {
+                        sensorBiometricType = BiometricType.REAR_FINGERPRINT;
+                    }
+                    break;
                 }
             }
         }
-
-        if (mSidefpsProps == null) {
-            Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null");
-        } else {
-            for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) {
+        if (mFaceProps != null && sensorBiometricType == BiometricType.UNKNOWN) {
+            for (FaceSensorPropertiesInternal prop : mFaceProps) {
                 if (prop.sensorId == sensorId) {
-                    mSfpsEnrolledForUser.put(userId, hasEnrollments);
+                    sensorBiometricType = BiometricType.FACE;
+                    break;
                 }
             }
         }
         for (Callback cb : mCallbacks) {
             cb.onEnrollmentsChanged();
+            cb.onEnrollmentsChanged(sensorBiometricType, userId, hasEnrollments);
         }
     }
 
@@ -574,11 +585,23 @@
     @Nullable private Point getFingerprintSensorLocationInNaturalOrientation() {
         if (getUdfpsLocation() != null) {
             return getUdfpsLocation();
+        } else {
+            int xFpLocation = mCachedDisplayInfo.getNaturalWidth() / 2;
+            try {
+                xFpLocation = mContext.getResources().getDimensionPixelSize(
+                        com.android.systemui.R.dimen
+                                .physical_fingerprint_sensor_center_screen_location_x);
+            } catch (Resources.NotFoundException e) {
+            }
+
+            return new Point(
+                    (int) (xFpLocation * mScaleFactor),
+                    (int) (mContext.getResources().getDimensionPixelSize(
+                            com.android.systemui.R.dimen
+                                    .physical_fingerprint_sensor_center_screen_location_y)
+                            * mScaleFactor)
+            );
         }
-        return new Point(
-                (int) (mFingerprintSensorLocationDefault.x * mScaleFactor),
-                (int) (mFingerprintSensorLocationDefault.y * mScaleFactor)
-        );
     }
 
     /**
@@ -604,6 +627,11 @@
         }
     }
 
+    /** Get FP sensor properties */
+    public @Nullable List<FingerprintSensorPropertiesInternal> getFingerprintProperties() {
+        return mFpProps;
+    }
+
     /**
      * @return where the face sensor exists in pixels in the current device orientation. Returns
      * null if no face sensor exists.
@@ -757,19 +785,6 @@
         }
 
         mDisplay = mContext.getDisplay();
-        mDisplay.getDisplayInfo(mCachedDisplayInfo);
-        int xFpLocation = mCachedDisplayInfo.getNaturalWidth() / 2;
-        try {
-            xFpLocation = mContext.getResources().getDimensionPixelSize(
-                    com.android.systemui.R.dimen
-                            .physical_fingerprint_sensor_center_screen_location_x);
-        } catch (Resources.NotFoundException e) {
-        }
-        mFingerprintSensorLocationDefault = new Point(
-                xFpLocation,
-                mContext.getResources().getDimensionPixelSize(com.android.systemui.R.dimen
-                        .physical_fingerprint_sensor_center_screen_location_y)
-        );
         updateSensorLocations();
 
         IntentFilter filter = new IntentFilter();
@@ -828,7 +843,7 @@
     }
 
     @Override
-    public void setBiometicContextListener(IBiometricContextListener listener) {
+    public void setBiometricContextListener(IBiometricContextListener listener) {
         mBiometricContextListener = listener;
         notifyDozeChanged(mStatusBarStateController.isDozing(),
                 mWakefulnessLifecycle.getWakefulness());
@@ -1081,6 +1096,13 @@
         return mSfpsEnrolledForUser.get(userId);
     }
 
+    /**
+     * Whether the passed userId has enrolled at least one fingerprint.
+     */
+    public boolean isFingerprintEnrolled(int userId) {
+        return mFpEnrolledForUser.getOrDefault(userId, false);
+    }
+
     private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
         mCurrentDialogArgs = args;
 
@@ -1222,7 +1244,6 @@
         pw.println("  mScaleFactor=" + mScaleFactor);
         pw.println("  faceAuthSensorLocationDefault=" + mFaceSensorLocationDefault);
         pw.println("  faceAuthSensorLocation=" + getFaceSensorLocation());
-        pw.println("  fingerprintSensorLocationDefault=" + mFingerprintSensorLocationDefault);
         pw.println("  fingerprintSensorLocationInNaturalOrientation="
                 + getFingerprintSensorLocationInNaturalOrientation());
         pw.println("  fingerprintSensorLocation=" + getFingerprintSensorLocation());
@@ -1263,6 +1284,16 @@
         default void onEnrollmentsChanged() {}
 
         /**
+         * Called when UDFPS enrollments have changed. This is called after boot and on changes to
+         * enrollment.
+         */
+        default void onEnrollmentsChanged(
+                @NonNull BiometricType biometricType,
+                int userId,
+                boolean hasEnrollments
+        ) {}
+
+        /**
          * Called when the biometric prompt starts showing.
          */
         default void onBiometricPromptShown() {}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
index b3b6fa2..d6ad4da 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
@@ -24,14 +24,15 @@
 import android.graphics.drawable.Drawable
 import android.util.Log
 import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieCompositionFactory
 import com.android.systemui.biometrics.AuthBiometricView.BiometricState
 
 private const val TAG = "AuthIconController"
 
 /** Controller for animating the BiometricPrompt icon/affordance. */
 abstract class AuthIconController(
-        protected val context: Context,
-        protected val iconView: LottieAnimationView
+    protected val context: Context,
+    protected val iconView: LottieAnimationView
 ) : Animatable2.AnimationCallback() {
 
     /** If this controller should ignore events and pause. */
@@ -94,4 +95,12 @@
     open fun handleAnimationEnd(drawable: Drawable) {}
 
     open fun onConfigurationChanged(newConfig: Configuration) {}
+
+    // TODO(b/251476085): Migrate this to an extension at the appropriate level?
+    /** Load the given [rawResources] immediately so they are cached for use in the [context]. */
+    protected fun cacheLottieAssetsInContext(context: Context, vararg rawResources: Int) {
+        for (res in rawResources) {
+            LottieCompositionFactory.fromRawRes(context, res)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
index 4b57d45..4b32759 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
@@ -55,11 +55,11 @@
     private val fadeDuration = 83L
     private val retractDuration = 400L
     private var alphaInDuration: Long = 0
-    private var unlockedRippleInProgress: Boolean = false
     private val dwellShader = DwellRippleShader()
     private val dwellPaint = Paint()
     private val rippleShader = RippleShader()
     private val ripplePaint = Paint()
+    private var unlockedRippleAnimator: AnimatorSet? = null
     private var fadeDwellAnimator: Animator? = null
     private var retractDwellAnimator: Animator? = null
     private var dwellPulseOutAnimator: Animator? = null
@@ -75,7 +75,7 @@
         }
     private var radius: Float = 0f
         set(value) {
-            rippleShader.setMaxSize(value * 2f, value * 2f)
+            rippleShader.rippleSize.setMaxSize(value * 2f, value * 2f)
             field = value
         }
     private var origin: Point = Point()
@@ -86,8 +86,9 @@
 
     init {
         rippleShader.color = 0xffffffff.toInt() // default color
-        rippleShader.progress = 0f
+        rippleShader.rawProgress = 0f
         rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
+        setupRippleFadeParams()
         ripplePaint.shader = rippleShader
 
         dwellShader.color = 0xffffffff.toInt() // default color
@@ -205,7 +206,7 @@
      * Plays a ripple animation that grows to the dwellRadius with distortion.
      */
     fun startDwellRipple(isDozing: Boolean) {
-        if (unlockedRippleInProgress || dwellPulseOutAnimator?.isRunning == true) {
+        if (unlockedRippleAnimator?.isRunning == true || dwellPulseOutAnimator?.isRunning == true) {
             return
         }
 
@@ -262,16 +263,14 @@
      * Ripple that bursts outwards from the position of the sensor to the edges of the screen
      */
     fun startUnlockedRipple(onAnimationEnd: Runnable?) {
-        if (unlockedRippleInProgress) {
-            return // Ignore if ripple effect is already playing
-        }
+        unlockedRippleAnimator?.cancel()
 
         val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
             interpolator = Interpolators.LINEAR_OUT_SLOW_IN
             duration = AuthRippleController.RIPPLE_ANIMATION_DURATION
             addUpdateListener { animator ->
                 val now = animator.currentPlayTime
-                rippleShader.progress = animator.animatedValue as Float
+                rippleShader.rawProgress = animator.animatedValue as Float
                 rippleShader.time = now.toFloat()
 
                 invalidate()
@@ -289,28 +288,26 @@
             }
         }
 
-        val animatorSet = AnimatorSet().apply {
+        unlockedRippleAnimator = AnimatorSet().apply {
             playTogether(
                 rippleAnimator,
                 alphaInAnimator
             )
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationStart(animation: Animator?) {
-                    unlockedRippleInProgress = true
-                    rippleShader.rippleFill = false
                     drawRipple = true
                     visibility = VISIBLE
                 }
 
                 override fun onAnimationEnd(animation: Animator?) {
                     onAnimationEnd?.run()
-                    unlockedRippleInProgress = false
                     drawRipple = false
                     visibility = GONE
+                    unlockedRippleAnimator = null
                 }
             })
         }
-        animatorSet.start()
+        unlockedRippleAnimator?.start()
     }
 
     fun resetRippleAlpha() {
@@ -342,10 +339,22 @@
         )
     }
 
+    private fun setupRippleFadeParams() {
+        with(rippleShader) {
+            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
+        }
+    }
+
     override fun onDraw(canvas: Canvas?) {
         // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
         // the active effect area. Values here should be kept in sync with the
-        // animation implementation in the ripple shader.
+        // animation implementation in the ripple shader. (Twice bigger)
         if (drawDwell) {
             val maskRadius = (1 - (1 - dwellShader.progress) * (1 - dwellShader.progress) *
                     (1 - dwellShader.progress)) * dwellRadius * 2f
@@ -354,10 +363,8 @@
         }
 
         if (drawRipple) {
-            val mask = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
-                    (1 - rippleShader.progress)) * radius * 2f
             canvas?.drawCircle(origin.x.toFloat(), origin.y.toFloat(),
-                    mask, ripplePaint)
+                    rippleShader.rippleSize.currentWidth, ripplePaint)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java
new file mode 100644
index 0000000..902bb18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.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.biometrics;
+
+/** Provides the status of the interactive to auth feature. */
+public interface FingerprintInteractiveToAuthProvider {
+    /**
+     *
+     * @param userId the user Id.
+     * @return true if the InteractiveToAuthFeature is enabled, false if disabled.
+     */
+    boolean isEnabled(int userId);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 1afa9b2..ac6a22c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -19,6 +19,8 @@
 import android.animation.AnimatorListenerAdapter
 import android.app.ActivityTaskManager
 import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
 import android.graphics.PixelFormat
 import android.graphics.PorterDuff
 import android.graphics.PorterDuffColorFilter
@@ -54,12 +56,18 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.recents.OverviewProxyService
 import com.android.systemui.util.concurrency.DelayableExecutor
 import java.io.PrintWriter
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 private const val TAG = "SideFpsController"
 
@@ -79,9 +87,12 @@
     displayManager: DisplayManager,
     @Main private val mainExecutor: DelayableExecutor,
     @Main private val handler: Handler,
+    private val alternateBouncerInteractor: AlternateBouncerInteractor,
+    @Application private val scope: CoroutineScope,
+    private val featureFlags: FeatureFlags,
     dumpManager: DumpManager
 ) : Dumpable {
-    val requests: HashSet<SideFpsUiRequestSource> = HashSet()
+    private val requests: HashSet<SideFpsUiRequestSource> = HashSet()
 
     @VisibleForTesting
     val sensorProps: FingerprintSensorPropertiesInternal =
@@ -89,13 +100,17 @@
             ?: throw IllegalStateException("no side fingerprint sensor")
 
     @VisibleForTesting
-    val orientationListener =
-        BiometricDisplayListener(
+    val orientationReasonListener =
+        OrientationReasonListener(
             context,
             displayManager,
             handler,
-            BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
-        ) { onOrientationChanged() }
+            sensorProps,
+            { reason -> onOrientationChanged(reason) },
+            BiometricOverlayConstants.REASON_UNKNOWN
+        )
+
+    @VisibleForTesting val orientationListener = orientationReasonListener.orientationListener
 
     @VisibleForTesting
     val overviewProxyListener =
@@ -111,7 +126,7 @@
         context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
 
     private val isReverseDefaultRotation =
-        context.getResources().getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
+        context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
 
     private var overlayHideAnimator: ViewPropertyAnimator? = null
 
@@ -157,26 +172,50 @@
                 override fun show(
                     sensorId: Int,
                     @BiometricOverlayConstants.ShowReason reason: Int
-                ) =
-                    if (reason.isReasonToAutoShow(activityTaskManager)) {
-                        show(SideFpsUiRequestSource.AUTO_SHOW)
+                ) {
+                    if (
+                        reason.isReasonToAutoShow(activityTaskManager) &&
+                            !context.isInRearDisplayMode()
+                    ) {
+                        show(SideFpsUiRequestSource.AUTO_SHOW, reason)
                     } else {
                         hide(SideFpsUiRequestSource.AUTO_SHOW)
                     }
+                }
 
                 override fun hide(sensorId: Int) = hide(SideFpsUiRequestSource.AUTO_SHOW)
             }
         )
         overviewProxyService.addCallback(overviewProxyListener)
+        listenForAlternateBouncerVisibility()
+
         dumpManager.registerDumpable(this)
     }
 
+    private fun listenForAlternateBouncerVisibility() {
+        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true)
+        if (featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)) {
+            scope.launch {
+                alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
+                    if (isVisible) {
+                        show(SideFpsUiRequestSource.ALTERNATE_BOUNCER)
+                    } else {
+                        hide(SideFpsUiRequestSource.ALTERNATE_BOUNCER)
+                    }
+                }
+            }
+        }
+    }
+
     /** Shows the side fps overlay if not already shown. */
-    fun show(request: SideFpsUiRequestSource) {
+    fun show(
+        request: SideFpsUiRequestSource,
+        @BiometricOverlayConstants.ShowReason reason: Int = BiometricOverlayConstants.REASON_UNKNOWN
+    ) {
         requests.add(request)
         mainExecutor.execute {
             if (overlayView == null) {
-                createOverlayForDisplay()
+                createOverlayForDisplay(reason)
             } else {
                 Log.v(TAG, "overlay already shown")
             }
@@ -200,13 +239,13 @@
         }
     }
 
-    private fun onOrientationChanged() {
+    private fun onOrientationChanged(@BiometricOverlayConstants.ShowReason reason: Int) {
         if (overlayView != null) {
-            createOverlayForDisplay()
+            createOverlayForDisplay(reason)
         }
     }
 
-    private fun createOverlayForDisplay() {
+    private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
         val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
         overlayView = view
         val display = context.display!!
@@ -237,7 +276,8 @@
                 updateOverlayParams(display, it.bounds)
             }
         }
-        lottie.addOverlayDynamicColor(context)
+        orientationReasonListener.reason = reason
+        lottie.addOverlayDynamicColor(context, reason)
 
         /**
          * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
@@ -268,10 +308,12 @@
         val isDefaultOrientation =
             if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
         val size = windowManager.maximumWindowMetrics.bounds
+
         val displayWidth = if (isDefaultOrientation) size.width() else size.height()
         val displayHeight = if (isDefaultOrientation) size.height() else size.width()
         val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
         val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
+
         val sensorBounds =
             if (overlayOffsets.isYAligned()) {
                 Rect(
@@ -297,6 +339,7 @@
 
         overlayViewParams.x = sensorBounds.left
         overlayViewParams.y = sensorBounds.top
+
         windowManager.updateViewLayout(overlayView, overlayViewParams)
     }
 
@@ -306,7 +349,12 @@
         }
         // hide after a few seconds if the sensor is oriented down and there are
         // large overlapping system bars
-        val rotation = context.display?.rotation
+        var rotation = context.display?.rotation
+
+        if (rotation != null) {
+            rotation = getRotationFromDefault(rotation)
+        }
+
         if (
             windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar() &&
                 ((rotation == Surface.ROTATION_270 && overlayOffsets.isYAligned()) ||
@@ -384,17 +432,36 @@
 private fun WindowInsets.hasBigNavigationBar(): Boolean =
     getInsets(WindowInsets.Type.navigationBars()).bottom >= 70
 
-private fun LottieAnimationView.addOverlayDynamicColor(context: Context) {
+private fun LottieAnimationView.addOverlayDynamicColor(
+    context: Context,
+    @BiometricOverlayConstants.ShowReason reason: Int
+) {
     fun update() {
         val c = context.getColor(R.color.biometric_dialog_accent)
         val chevronFill = context.getColor(R.color.sfps_chevron_fill)
-        for (key in listOf(".blue600", ".blue400")) {
-            addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
-                PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP)
+        val isKeyguard = reason == REASON_AUTH_KEYGUARD
+        if (isKeyguard) {
+            for (key in listOf(".blue600", ".blue400")) {
+                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+                    PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP)
+                }
             }
-        }
-        addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
-            PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
+            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
+            }
+        } else if (!isDarkMode(context)) {
+            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
+            }
+        } else if (isDarkMode(context)) {
+            for (key in listOf(".blue600", ".blue400")) {
+                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+                    PorterDuffColorFilter(
+                        context.getColor(R.color.settingslib_color_blue400),
+                        PorterDuff.Mode.SRC_ATOP
+                    )
+                }
+            }
         }
     }
 
@@ -405,6 +472,29 @@
     }
 }
 
+private fun isDarkMode(context: Context): Boolean {
+    val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+    return darkMode == Configuration.UI_MODE_NIGHT_YES
+}
+
+@VisibleForTesting
+class OrientationReasonListener(
+    context: Context,
+    displayManager: DisplayManager,
+    handler: Handler,
+    sensorProps: FingerprintSensorPropertiesInternal,
+    onOrientationChanged: (reason: Int) -> Unit,
+    @BiometricOverlayConstants.ShowReason var reason: Int
+) {
+    val orientationListener =
+        BiometricDisplayListener(
+            context,
+            displayManager,
+            handler,
+            BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
+        ) { onOrientationChanged(reason) }
+}
+
 /**
  * The source of a request to show the side fps visual indicator. This is distinct from
  * [BiometricOverlayConstants] which corrresponds with the reason fingerprint authentication is
@@ -415,4 +505,5 @@
     AUTO_SHOW,
     /** Pin, pattern or password bouncer */
     PRIMARY_BOUNCER,
+    ALTERNATE_BOUNCER
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 4130cf5..ef7dcb7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -190,11 +190,6 @@
     open fun listenForTouchesOutsideView(): Boolean = false
 
     /**
-     * Called on touches outside of the view if listenForTouchesOutsideView returns true
-     */
-    open fun onTouchOutsideView() {}
-
-    /**
      * Called when a view should announce an accessibility event.
      */
     open fun doAnnounceForAccessibility(str: String) {}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index f3136ba..3fd00ae 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -73,6 +73,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -86,6 +87,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.Execution;
+import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.SystemClock;
 
 import java.io.PrintWriter;
@@ -149,6 +151,8 @@
     @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
     @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     @Nullable private final TouchProcessor mTouchProcessor;
+    @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @NonNull private final SecureSettings mSecureSettings;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
@@ -220,6 +224,10 @@
     @Override
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("mSensorProps=(" + mSensorProps + ")");
+        pw.println("Using new touch detection framework: " + mFeatureFlags.isEnabled(
+                Flags.UDFPS_NEW_TOUCH_DETECTION));
+        pw.println("Using ellipse touch detection: " + mFeatureFlags.isEnabled(
+                Flags.UDFPS_ELLIPSE_DETECTION));
     }
 
     public class UdfpsOverlayController extends IUdfpsOverlayController.Stub {
@@ -232,12 +240,12 @@
                             mShadeExpansionStateManager, mKeyguardViewManager,
                             mKeyguardUpdateMonitor, mDialogManager, mDumpManager,
                             mLockscreenShadeTransitionController, mConfigurationController,
-                            mSystemClock, mKeyguardStateController,
+                            mKeyguardStateController,
                             mUnlockedScreenOffAnimationController,
-                            mUdfpsDisplayMode, requestId, reason, callback,
+                            mUdfpsDisplayMode, mSecureSettings, requestId, reason, callback,
                             (view, event, fromUdfpsView) -> onTouch(requestId, event,
                                     fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
-                            mPrimaryBouncerInteractor)));
+                            mPrimaryBouncerInteractor, mAlternateBouncerInteractor)));
         }
 
         @Override
@@ -329,13 +337,13 @@
         if (!mOverlayParams.equals(overlayParams)) {
             mOverlayParams = overlayParams;
 
-            final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer();
+            final boolean wasShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState();
 
             // When the bounds change it's always necessary to re-create the overlay's window with
             // new LayoutParams. If the overlay needs to be shown, this will re-create and show the
             // overlay with the updated LayoutParams. Otherwise, the overlay will remain hidden.
             redrawOverlay();
-            if (wasShowingAltAuth) {
+            if (wasShowingAlternateBouncer) {
                 mKeyguardViewManager.showBouncer(true);
             }
         }
@@ -543,9 +551,6 @@
         final UdfpsView udfpsView = mOverlay.getOverlayView();
         boolean handled = false;
         switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_OUTSIDE:
-                udfpsView.onTouchOutsideView();
-                return true;
             case MotionEvent.ACTION_DOWN:
             case MotionEvent.ACTION_HOVER_ENTER:
                 Trace.beginSection("UdfpsController.onTouch.ACTION_DOWN");
@@ -719,7 +724,9 @@
             @NonNull Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider,
             @NonNull @BiometricsBackground Executor biometricsExecutor,
             @NonNull PrimaryBouncerInteractor primaryBouncerInteractor,
-            @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) {
+            @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor,
+            @NonNull AlternateBouncerInteractor alternateBouncerInteractor,
+            @NonNull SecureSettings secureSettings) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -759,6 +766,8 @@
 
         mBiometricExecutor = biometricsExecutor;
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
+        mAlternateBouncerInteractor = alternateBouncerInteractor;
+        mSecureSettings = secureSettings;
 
         mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
                 ? singlePointerTouchProcessor : null;
@@ -853,9 +862,7 @@
                 onFingerUp(mOverlay.getRequestId(), oldView);
             }
             final boolean removed = mOverlay.hide();
-            if (mKeyguardViewManager.isShowingAlternateBouncer()) {
-                mKeyguardViewManager.hideAlternateBouncer(true);
-            }
+            mKeyguardViewManager.hideAlternateBouncer(true);
             Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed);
         } else {
             Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden");
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 8db4927..b6b5d26 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -50,6 +50,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
@@ -59,7 +60,7 @@
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.settings.SecureSettings
 
 private const val TAG = "UdfpsControllerOverlay"
 
@@ -86,10 +87,10 @@
         private val dumpManager: DumpManager,
         private val transitionController: LockscreenShadeTransitionController,
         private val configurationController: ConfigurationController,
-        private val systemClock: SystemClock,
         private val keyguardStateController: KeyguardStateController,
         private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
         private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
+        private val secureSettings: SecureSettings,
         val requestId: Long,
         @ShowReason val requestReason: Int,
         private val controllerCallback: IUdfpsOverlayControllerCallback,
@@ -97,7 +98,8 @@
         private val activityLaunchAnimator: ActivityLaunchAnimator,
         private val featureFlags: FeatureFlags,
         private val primaryBouncerInteractor: PrimaryBouncerInteractor,
-        private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
+        private val alternateBouncerInteractor: AlternateBouncerInteractor,
+        private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -131,7 +133,7 @@
     /** A helper if the [requestReason] was due to enrollment. */
     val enrollHelper: UdfpsEnrollHelper? =
         if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) {
-            UdfpsEnrollHelper(context, fingerprintManager, requestReason)
+            UdfpsEnrollHelper(context, fingerprintManager, secureSettings, requestReason)
         } else {
             null
         }
@@ -255,14 +257,14 @@
                     dumpManager,
                     transitionController,
                     configurationController,
-                    systemClock,
                     keyguardStateController,
                     unlockedScreenOffAnimationController,
                     dialogManager,
                     controller,
                     activityLaunchAnimator,
                     featureFlags,
-                    primaryBouncerInteractor
+                    primaryBouncerInteractor,
+                    alternateBouncerInteractor,
                 )
             }
             REASON_AUTH_BP -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
index d5c763d3..cfa8ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -24,11 +24,12 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.Build;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.systemui.util.settings.SecureSettings;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -51,8 +52,8 @@
         void onLastStepAcquired();
     }
 
-    @NonNull private final Context mContext;
     @NonNull private final FingerprintManager mFingerprintManager;
+    @NonNull private final SecureSettings mSecureSettings;
     // IUdfpsOverlayController reason
     private final int mEnrollReason;
     private final boolean mAccessibilityEnabled;
@@ -70,10 +71,11 @@
     @Nullable Listener mListener;
 
     public UdfpsEnrollHelper(@NonNull Context context,
-            @NonNull FingerprintManager fingerprintManager, int reason) {
+            @NonNull FingerprintManager fingerprintManager, SecureSettings secureSettings,
+            int reason) {
 
-        mContext = context;
         mFingerprintManager = fingerprintManager;
+        mSecureSettings = secureSettings;
         mEnrollReason = reason;
 
         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
@@ -84,8 +86,7 @@
         // Number of pixels per mm
         float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
                 context.getResources().getDisplayMetrics());
-        boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                NEW_COORDS_OVERRIDE, 0,
+        boolean useNewCoords = mSecureSettings.getIntForUser(NEW_COORDS_OVERRIDE, 0,
                 UserHandle.USER_CURRENT) != 0;
         if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) {
             Log.v(TAG, "Using new coordinates");
@@ -210,8 +211,7 @@
 
         float scale = SCALE;
         if (Build.IS_ENG || Build.IS_USERDEBUG) {
-            scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
-                    SCALE_OVERRIDE, SCALE,
+            scale = mSecureSettings.getFloatForUser(SCALE_OVERRIDE, SCALE,
                     UserHandle.USER_CURRENT);
         }
         final int index = mLocationsEnrolled - mCenterTouchCount;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
index 63144fc..addbee9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -39,16 +40,14 @@
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
-import com.android.systemui.statusbar.phone.KeyguardBouncer
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateBouncer
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.time.SystemClock
 import java.io.PrintWriter
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
@@ -65,25 +64,26 @@
     dumpManager: DumpManager,
     private val lockScreenShadeTransitionController: LockscreenShadeTransitionController,
     private val configurationController: ConfigurationController,
-    private val systemClock: SystemClock,
     private val keyguardStateController: KeyguardStateController,
     private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
     systemUIDialogManager: SystemUIDialogManager,
     private val udfpsController: UdfpsController,
     private val activityLaunchAnimator: ActivityLaunchAnimator,
     featureFlags: FeatureFlags,
-    private val primaryBouncerInteractor: PrimaryBouncerInteractor
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    private val alternateBouncerInteractor: AlternateBouncerInteractor,
 ) :
     UdfpsAnimationViewController<UdfpsKeyguardView>(
         view,
         statusBarStateController,
         shadeExpansionStateManager,
         systemUIDialogManager,
-        dumpManager
+        dumpManager,
     ) {
     private val useExpandedOverlay: Boolean =
         featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
-    private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER)
+    private val isModernAlternateBouncerEnabled: Boolean =
+        featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)
     private var showingUdfpsBouncer = false
     private var udfpsRequested = false
     private var qsExpansion = 0f
@@ -91,7 +91,6 @@
     private var statusBarState = 0
     private var transitionToFullShadeProgress = 0f
     private var lastDozeAmount = 0f
-    private var lastUdfpsBouncerShowTime: Long = -1
     private var panelExpansionFraction = 0f
     private var launchTransitionFadingAway = false
     private var isLaunchingActivity = false
@@ -108,12 +107,6 @@
                 )
             }
         }
-    /**
-     * Hidden amount of input (pin/pattern/password) bouncer. This is used
-     * [KeyguardBouncer.EXPANSION_VISIBLE] (0f) to [KeyguardBouncer.EXPANSION_HIDDEN] (1f). Only
-     * used for the non-modernBouncer.
-     */
-    private var inputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN
     private var inputBouncerExpansion = 0f // only used for modernBouncer
 
     private val stateListener: StatusBarStateController.StateListener =
@@ -147,21 +140,6 @@
             }
         }
 
-    private val mPrimaryBouncerExpansionCallback: PrimaryBouncerExpansionCallback =
-        object : PrimaryBouncerExpansionCallback {
-            override fun onExpansionChanged(expansion: Float) {
-                inputBouncerHiddenAmount = expansion
-                updateAlpha()
-                updatePauseAuth()
-            }
-
-            override fun onVisibilityChanged(isVisible: Boolean) {
-                updateBouncerHiddenAmount()
-                updateAlpha()
-                updatePauseAuth()
-            }
-        }
-
     private val configurationListener: ConfigurationController.ConfigurationListener =
         object : ConfigurationController.ConfigurationListener {
             override fun onUiModeChanged() {
@@ -244,20 +222,8 @@
             }
         }
 
-    private val mAlternateBouncer: AlternateBouncer =
-        object : AlternateBouncer {
-            override fun showAlternateBouncer(): Boolean {
-                return showUdfpsBouncer(true)
-            }
-
-            override fun hideAlternateBouncer(): Boolean {
-                return showUdfpsBouncer(false)
-            }
-
-            override fun isShowingAlternateBouncer(): Boolean {
-                return showingUdfpsBouncer
-            }
-
+    private val occludingAppBiometricUI: OccludingAppBiometricUI =
+        object : OccludingAppBiometricUI {
             override fun requestUdfps(request: Boolean, color: Int) {
                 udfpsRequested = request
                 view.requestUdfps(request, color)
@@ -275,16 +241,17 @@
 
     override fun onInit() {
         super.onInit()
-        keyguardViewManager.setAlternateBouncer(mAlternateBouncer)
+        keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
     }
 
     init {
-        if (isModernBouncerEnabled) {
-            view.repeatWhenAttached {
-                // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
-                // can make the view not visible; and we still want to listen for events
-                // that may make the view visible again.
-                repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) }
+        view.repeatWhenAttached {
+            // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
+            // can make the view not visible; and we still want to listen for events
+            // that may make the view visible again.
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                listenForBouncerExpansion(this)
+                if (isModernAlternateBouncerEnabled) listenForAlternateBouncerVisibility(this)
             }
         }
     }
@@ -300,8 +267,18 @@
         }
     }
 
+    @VisibleForTesting
+    internal suspend fun listenForAlternateBouncerVisibility(scope: CoroutineScope): Job {
+        return scope.launch {
+            alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
+                showUdfpsBouncer(isVisible)
+            }
+        }
+    }
+
     public override fun onViewAttached() {
         super.onViewAttached()
+        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true)
         val dozeAmount = statusBarStateController.dozeAmount
         lastDozeAmount = dozeAmount
         stateListener.onDozeAmountChanged(dozeAmount, dozeAmount)
@@ -312,21 +289,14 @@
         statusBarState = statusBarStateController.state
         qsExpansion = keyguardViewManager.qsExpansion
         keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback)
-        if (!isModernBouncerEnabled) {
-            val bouncer = keyguardViewManager.primaryBouncer
-            bouncer?.expansion?.let {
-                mPrimaryBouncerExpansionCallback.onExpansionChanged(it)
-                bouncer.addBouncerExpansionCallback(mPrimaryBouncerExpansionCallback)
-            }
-            updateBouncerHiddenAmount()
-        }
         configurationController.addCallback(configurationListener)
         shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
         updateScaleFactor()
         view.updatePadding()
         updateAlpha()
         updatePauseAuth()
-        keyguardViewManager.setAlternateBouncer(mAlternateBouncer)
+        keyguardViewManager.setLegacyAlternateBouncer(legacyAlternateBouncer)
+        keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
         lockScreenShadeTransitionController.udfpsKeyguardViewController = this
         activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
         view.mUseExpandedOverlay = useExpandedOverlay
@@ -334,10 +304,12 @@
 
     override fun onViewDetached() {
         super.onViewDetached()
+        alternateBouncerInteractor.setAlternateBouncerUIAvailable(false)
         faceDetectRunning = false
         keyguardStateController.removeCallback(keyguardStateControllerCallback)
         statusBarStateController.removeCallback(stateListener)
-        keyguardViewManager.removeAlternateAuthInterceptor(mAlternateBouncer)
+        keyguardViewManager.removeLegacyAlternateBouncer(legacyAlternateBouncer)
+        keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI)
         keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
         configurationController.removeCallback(configurationListener)
         shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
@@ -346,17 +318,20 @@
         }
         activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
         keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
-        if (!isModernBouncerEnabled) {
-            keyguardViewManager.primaryBouncer?.removeBouncerExpansionCallback(
-                mPrimaryBouncerExpansionCallback
-            )
-        }
     }
 
     override fun dump(pw: PrintWriter, args: Array<String>) {
         super.dump(pw, args)
-        pw.println("isModernBouncerEnabled=$isModernBouncerEnabled")
+        pw.println("isModernAlternateBouncerEnabled=$isModernAlternateBouncerEnabled")
         pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer")
+        pw.println(
+            "altBouncerInteractor#isAlternateBouncerVisible=" +
+                "${alternateBouncerInteractor.isVisibleState()}"
+        )
+        pw.println(
+            "altBouncerInteractor#canShowAlternateBouncerForFingerprint=" +
+                "${alternateBouncerInteractor.canShowAlternateBouncerForFingerprint()}"
+        )
         pw.println("faceDetectRunning=$faceDetectRunning")
         pw.println("statusBarState=" + StatusBarState.toString(statusBarState))
         pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress")
@@ -366,11 +341,7 @@
         pw.println("udfpsRequestedByApp=$udfpsRequested")
         pw.println("launchTransitionFadingAway=$launchTransitionFadingAway")
         pw.println("lastDozeAmount=$lastDozeAmount")
-        if (isModernBouncerEnabled) {
-            pw.println("inputBouncerExpansion=$inputBouncerExpansion")
-        } else {
-            pw.println("inputBouncerHiddenAmount=$inputBouncerHiddenAmount")
-        }
+        pw.println("inputBouncerExpansion=$inputBouncerExpansion")
         view.dump(pw)
     }
 
@@ -385,9 +356,6 @@
         val udfpsAffordanceWasNotShowing = shouldPauseAuth()
         showingUdfpsBouncer = show
         if (showingUdfpsBouncer) {
-            lastUdfpsBouncerShowTime = systemClock.uptimeMillis()
-        }
-        if (showingUdfpsBouncer) {
             if (udfpsAffordanceWasNotShowing) {
                 view.animateInUdfpsBouncer(null)
             }
@@ -400,7 +368,6 @@
         } else {
             keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
         }
-        updateBouncerHiddenAmount()
         updateAlpha()
         updatePauseAuth()
         return true
@@ -441,49 +408,17 @@
     }
 
     fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean {
-        return if (isModernBouncerEnabled) {
-            inputBouncerExpansion >= bouncerExpansionThreshold
-        } else {
-            inputBouncerHiddenAmount < bouncerExpansionThreshold
-        }
+        return inputBouncerExpansion >= bouncerExpansionThreshold
     }
 
     fun isInputBouncerFullyVisible(): Boolean {
-        return if (isModernBouncerEnabled) {
-            inputBouncerExpansion == 1f
-        } else {
-            keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateBouncer
-        }
+        return inputBouncerExpansion == 1f
     }
 
     override fun listenForTouchesOutsideView(): Boolean {
         return true
     }
 
-    override fun onTouchOutsideView() {
-        maybeShowInputBouncer()
-    }
-
-    /**
-     * If we were previously showing the udfps bouncer, hide it and instead show the regular
-     * (pin/pattern/password) bouncer.
-     *
-     * Does nothing if we weren't previously showing the UDFPS bouncer.
-     */
-    private fun maybeShowInputBouncer() {
-        if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) {
-            keyguardViewManager.showPrimaryBouncer(true)
-        }
-    }
-
-    /**
-     * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the
-     * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer.
-     */
-    private fun hasUdfpsBouncerShownWithMinTime(): Boolean {
-        return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200
-    }
-
     /**
      * Set the progress we're currently transitioning to the full shade. 0.0f means we're not
      * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down
@@ -529,11 +464,7 @@
     }
 
     private fun getInputBouncerHiddenAmt(): Float {
-        return if (isModernBouncerEnabled) {
-            1f - inputBouncerExpansion
-        } else {
-            inputBouncerHiddenAmount
-        }
+        return 1f - inputBouncerExpansion
     }
 
     /** Update the scale factor based on the device's resolution. */
@@ -541,18 +472,20 @@
         udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) }
     }
 
-    private fun updateBouncerHiddenAmount() {
-        if (isModernBouncerEnabled) {
-            return
+    private val legacyAlternateBouncer: LegacyAlternateBouncer =
+        object : LegacyAlternateBouncer {
+            override fun showAlternateBouncer(): Boolean {
+                return showUdfpsBouncer(true)
+            }
+
+            override fun hideAlternateBouncer(): Boolean {
+                return showUdfpsBouncer(false)
+            }
+
+            override fun isShowingAlternateBouncer(): Boolean {
+                return showingUdfpsBouncer
+            }
         }
-        val altBouncerShowing = keyguardViewManager.isShowingAlternateBouncer
-        if (altBouncerShowing || !keyguardViewManager.primaryBouncerIsOrWillBeShowing()) {
-            inputBouncerHiddenAmount = 1f
-        } else if (keyguardViewManager.isBouncerShowing) {
-            // input bouncer is fully showing
-            inputBouncerHiddenAmount = 0f
-        }
-    }
 
     companion object {
         const val TAG = "UdfpsKeyguardViewController"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
index 4a8877e..e61c614 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
@@ -111,10 +111,6 @@
         }
     }
 
-    fun onTouchOutsideView() {
-        animationViewController?.onTouchOutsideView()
-    }
-
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
         Log.v(TAG, "onAttachedToWindow")
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt
index d0d6f4c..3d56326 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt
@@ -36,6 +36,7 @@
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityManager
 import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.R
 import java.lang.annotation.Retention
 import java.lang.annotation.RetentionPolicy
 
@@ -117,4 +118,7 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD)
     internal annotation class CredentialType
-}
\ No newline at end of file
+}
+
+fun Context.isInRearDisplayMode(): Boolean = resources.getIntArray(
+        com.android.internal.R.array.config_rearDisplayDeviceStates).isNotEmpty()
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
index 8572242..682d38a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Point
 import android.graphics.Rect
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
 import kotlin.math.cos
 import kotlin.math.pow
@@ -50,7 +51,8 @@
         return result <= 1
     }
 
-    private fun calculateSensorPoints(sensorBounds: Rect): List<Point> {
+    @VisibleForTesting
+    fun calculateSensorPoints(sensorBounds: Rect): List<Point> {
         val sensorX = sensorBounds.centerX()
         val sensorY = sensorBounds.centerY()
         val cornerOffset: Int = sensorBounds.width() / 4
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
index 62bedc6..48d4845 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
@@ -26,28 +26,28 @@
      * Value obtained from [MotionEvent.getPointerId], or [MotionEvent.INVALID_POINTER_ID] if the ID
      * is not available.
      */
-    val pointerId: Int,
+    val pointerId: Int = MotionEvent.INVALID_POINTER_ID,
 
     /** [MotionEvent.getRawX] mapped to natural orientation and native resolution. */
-    val x: Float,
+    val x: Float = 0f,
 
     /** [MotionEvent.getRawY] mapped to natural orientation and native resolution. */
-    val y: Float,
+    val y: Float = 0f,
 
     /** [MotionEvent.getTouchMinor] mapped to natural orientation and native resolution. */
-    val minor: Float,
+    val minor: Float = 0f,
 
     /** [MotionEvent.getTouchMajor] mapped to natural orientation and native resolution. */
-    val major: Float,
+    val major: Float = 0f,
 
     /** [MotionEvent.getOrientation] mapped to natural orientation. */
-    val orientation: Float,
+    val orientation: Float = 0f,
 
     /** [MotionEvent.getEventTime]. */
-    val time: Long,
+    val time: Long = 0,
 
     /** [MotionEvent.getDownTime]. */
-    val gestureStart: Long,
+    val gestureStart: Long = 0,
 ) {
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
index 338bf66..39ea936 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
@@ -27,6 +27,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 
+private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270)
+
 /**
  * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations.
  */
@@ -41,74 +43,75 @@
     ): TouchProcessorResult {
 
         fun preprocess(): PreprocessedTouch {
-            // TODO(b/253085297): Add multitouch support. pointerIndex can be > 0 for ACTION_MOVE.
-            val pointerIndex = 0
-            val touchData = event.normalize(pointerIndex, overlayParams)
-            val isGoodOverlap =
-                overlapDetector.isGoodOverlap(touchData, overlayParams.nativeSensorBounds)
-            return PreprocessedTouch(touchData, previousPointerOnSensorId, isGoodOverlap)
+            val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) }
+            val pointersOnSensor =
+                touchData
+                    .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) }
+                    .map { it.pointerId }
+            return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor)
         }
 
         return when (event.actionMasked) {
-            MotionEvent.ACTION_DOWN -> processActionDown(preprocess())
-            MotionEvent.ACTION_MOVE -> processActionMove(preprocess())
-            MotionEvent.ACTION_UP -> processActionUp(preprocess())
-            MotionEvent.ACTION_CANCEL ->
-                processActionCancel(event.normalize(pointerIndex = 0, overlayParams))
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_POINTER_DOWN,
+            MotionEvent.ACTION_MOVE,
+            MotionEvent.ACTION_HOVER_ENTER,
+            MotionEvent.ACTION_HOVER_MOVE -> processActionMove(preprocess())
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_POINTER_UP,
+            MotionEvent.ACTION_HOVER_EXIT ->
+                processActionUp(preprocess(), event.getPointerId(event.actionIndex))
+            MotionEvent.ACTION_CANCEL -> processActionCancel(NormalizedTouchData())
             else ->
                 Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked))
         }
     }
 }
 
+/**
+ * [data] contains a list of NormalizedTouchData for pointers in the motionEvent ordered by
+ * pointerIndex
+ *
+ * [previousPointerOnSensorId] the pointerId of the previous pointer on the sensor,
+ * [MotionEvent.INVALID_POINTER_ID] if none
+ *
+ * [pointersOnSensor] contains a list of ids of pointers on the sensor
+ */
 private data class PreprocessedTouch(
-    val data: NormalizedTouchData,
+    val data: List<NormalizedTouchData>,
     val previousPointerOnSensorId: Int,
-    val isGoodOverlap: Boolean,
+    val pointersOnSensor: List<Int>,
 )
 
-private fun processActionDown(touch: PreprocessedTouch): TouchProcessorResult {
-    return if (touch.isGoodOverlap) {
-        ProcessedTouch(InteractionEvent.DOWN, pointerOnSensorId = touch.data.pointerId, touch.data)
-    } else {
-        val event =
-            if (touch.data.pointerId == touch.previousPointerOnSensorId) {
-                InteractionEvent.UP
-            } else {
-                InteractionEvent.UNCHANGED
-            }
-        ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
-    }
-}
-
 private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult {
     val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID
-    val interactionEvent =
-        when {
-            touch.isGoodOverlap && !hadPointerOnSensor -> InteractionEvent.DOWN
-            !touch.isGoodOverlap && hadPointerOnSensor -> InteractionEvent.UP
-            else -> InteractionEvent.UNCHANGED
-        }
-    val pointerOnSensorId =
-        when (interactionEvent) {
-            InteractionEvent.UNCHANGED -> touch.previousPointerOnSensorId
-            InteractionEvent.DOWN -> touch.data.pointerId
-            else -> INVALID_POINTER_ID
-        }
-    return ProcessedTouch(interactionEvent, pointerOnSensorId, touch.data)
+    val hasPointerOnSensor = touch.pointersOnSensor.isNotEmpty()
+    val pointerOnSensorId = touch.pointersOnSensor.firstOrNull() ?: INVALID_POINTER_ID
+
+    return if (!hadPointerOnSensor && hasPointerOnSensor) {
+        val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
+        ProcessedTouch(InteractionEvent.DOWN, data.pointerId, data)
+    } else if (hadPointerOnSensor && !hasPointerOnSensor) {
+        ProcessedTouch(InteractionEvent.UP, INVALID_POINTER_ID, NormalizedTouchData())
+    } else {
+        val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
+        ProcessedTouch(InteractionEvent.UNCHANGED, pointerOnSensorId, data)
+    }
 }
 
-private fun processActionUp(touch: PreprocessedTouch): TouchProcessorResult {
-    return if (touch.isGoodOverlap) {
-        ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
+private fun processActionUp(touch: PreprocessedTouch, actionId: Int): TouchProcessorResult {
+    // Finger lifted and it was the only finger on the sensor
+    return if (touch.pointersOnSensor.size == 1 && touch.pointersOnSensor.contains(actionId)) {
+        ProcessedTouch(
+            InteractionEvent.UP,
+            pointerOnSensorId = INVALID_POINTER_ID,
+            NormalizedTouchData()
+        )
     } else {
-        val event =
-            if (touch.previousPointerOnSensorId != INVALID_POINTER_ID) {
-                InteractionEvent.UP
-            } else {
-                InteractionEvent.UNCHANGED
-            }
-        ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
+        // Pick new pointerOnSensor that's not the finger that was lifted
+        val pointerOnSensorId = touch.pointersOnSensor.find { it != actionId } ?: INVALID_POINTER_ID
+        val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
+        ProcessedTouch(InteractionEvent.UNCHANGED, data.pointerId, data)
     }
 }
 
@@ -129,19 +132,27 @@
     val nativeY = naturalTouch.y / overlayParams.scaleFactor
     val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor
     val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor
+    var nativeOrientation: Float = getOrientation(pointerIndex)
+    if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) {
+        nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat()
+    }
     return NormalizedTouchData(
         pointerId = getPointerId(pointerIndex),
         x = nativeX,
         y = nativeY,
         minor = nativeMinor,
         major = nativeMajor,
-        // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O.
-        orientation = getOrientation(pointerIndex),
+        orientation = nativeOrientation,
         time = eventTime,
         gestureStart = downTime,
     )
 }
 
+private fun toRadVerticalFromRotated(rad: Double): Double {
+    val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI
+    return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI
+}
+
 /**
  * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device
  * is in the [Surface.ROTATION_0] orientation.
@@ -152,7 +163,7 @@
 ): PointF {
     val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex))
     val rot = overlayParams.rotation
-    if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+    if (SUPPORTED_ROTATIONS.contains(rot)) {
         RotationUtils.rotatePointF(
             touchPoint,
             RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
index 8e062bd..653c12e 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
@@ -46,11 +46,11 @@
     @VisibleForTesting
     protected View mDialogView;
     private MediaOutputDialogFactory mMediaOutputDialogFactory;
-    private String mSwitchBroadcastApp;
+    private String mCurrentBroadcastApp;
     private String mOutputPackageName;
 
     public BroadcastDialog(Context context, MediaOutputDialogFactory mediaOutputDialogFactory,
-            String switchBroadcastApp, String outputPkgName, UiEventLogger uiEventLogger) {
+            String currentBroadcastApp, String outputPkgName, UiEventLogger uiEventLogger) {
         super(context);
         if (DEBUG) {
             Log.d(TAG, "Init BroadcastDialog");
@@ -58,7 +58,7 @@
 
         mContext = getContext();
         mMediaOutputDialogFactory = mediaOutputDialogFactory;
-        mSwitchBroadcastApp = switchBroadcastApp;
+        mCurrentBroadcastApp = currentBroadcastApp;
         mOutputPackageName = outputPkgName;
         mUiEventLogger = uiEventLogger;
     }
@@ -77,20 +77,18 @@
 
         TextView title = mDialogView.requireViewById(R.id.dialog_title);
         TextView subTitle = mDialogView.requireViewById(R.id.dialog_subtitle);
-        title.setText(
-                mContext.getString(R.string.bt_le_audio_broadcast_dialog_title,
-                        MediaDataUtils.getAppLabel(mContext, mOutputPackageName,
-                                mContext.getString(
-                                        R.string.bt_le_audio_broadcast_dialog_unknown_name))));
-        subTitle.setText(
-                mContext.getString(R.string.bt_le_audio_broadcast_dialog_sub_title,
-                        mSwitchBroadcastApp));
+        title.setText(mContext.getString(
+                R.string.bt_le_audio_broadcast_dialog_title, mCurrentBroadcastApp));
+        String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName,
+                mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name));
+        subTitle.setText(mContext.getString(
+                R.string.bt_le_audio_broadcast_dialog_sub_title, switchBroadcastApp));
 
         Button switchBroadcast = mDialogView.requireViewById(R.id.switch_broadcast);
         Button changeOutput = mDialogView.requireViewById(R.id.change_output);
         Button cancelBtn = mDialogView.requireViewById(R.id.cancel);
         switchBroadcast.setText(mContext.getString(
-                R.string.bt_le_audio_broadcast_dialog_switch_app, mSwitchBroadcastApp), null);
+                R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp));
         changeOutput.setOnClickListener((view) -> {
             mMediaOutputDialogFactory.create(mOutputPackageName, true, null);
             dismiss();
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java
index 8a54345..1b699e8 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java
@@ -47,10 +47,15 @@
         mMediaOutputDialogFactory = mediaOutputDialogFactory;
     }
 
-    public void createBroadcastDialog(String switchAppName, String outputPkgName,
+    /** Creates a [BroadcastDialog] for the user to switch broadcast or change the output device
+     *
+     * @param currentBroadcastAppName Indicates the APP name currently broadcasting
+     * @param outputPkgName Indicates the output media package name to be switched
+     */
+    public void createBroadcastDialog(String currentBroadcastAppName, String outputPkgName,
             boolean aboveStatusBar, View view) {
         BroadcastDialog broadcastDialog = new BroadcastDialog(mContext, mMediaOutputDialogFactory,
-                switchAppName, outputPkgName, mUiEventLogger);
+                currentBroadcastAppName, outputPkgName, mUiEventLogger);
         if (view != null) {
             mDialogLaunchAnimator.showFromView(broadcastDialog, view);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index 58d40d3..4227a7a 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
@@ -25,13 +25,13 @@
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
 import android.os.RemoteException
-import android.os.UserHandle
 import android.util.Log
 import android.view.WindowManager
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.ActivityIntentHelper
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.system.ActivityManagerKt.isInForeground
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -55,6 +55,7 @@
     private val cameraIntents: CameraIntentsWrapper,
     private val contentResolver: ContentResolver,
     @Main private val uiExecutor: Executor,
+    private val userTracker: UserTracker
 ) {
     /**
      * Whether the camera application can be launched for the camera launch gesture.
@@ -111,7 +112,7 @@
                         Intent.FLAG_ACTIVITY_NEW_TASK,
                         null,
                         activityOptions.toBundle(),
-                        UserHandle.CURRENT.identifier,
+                        userTracker.userId,
                     )
                 } catch (e: RemoteException) {
                     Log.w(
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
index 867faf9..cc43e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
@@ -20,15 +20,13 @@
 import android.content.Intent
 import android.provider.MediaStore
 import android.text.TextUtils
-
 import com.android.systemui.R
 
 class CameraIntents {
     companion object {
-        val DEFAULT_SECURE_CAMERA_INTENT_ACTION =
-                MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE
-        val DEFAULT_INSECURE_CAMERA_INTENT_ACTION =
-                MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
+        val DEFAULT_SECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE
+        val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
+        private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA
         const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"
 
         @JvmStatic
@@ -44,18 +42,14 @@
         @JvmStatic
         fun getInsecureCameraIntent(context: Context): Intent {
             val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION)
-            getOverrideCameraPackage(context)?.let {
-                intent.setPackage(it)
-            }
+            getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
             return intent
         }
 
         @JvmStatic
         fun getSecureCameraIntent(context: Context): Intent {
             val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION)
-            getOverrideCameraPackage(context)?.let {
-                intent.setPackage(it)
-            }
+            getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
             return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
         }
 
@@ -68,5 +62,11 @@
         fun isInsecureCameraIntent(intent: Intent?): Boolean {
             return intent?.getAction()?.equals(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ?: false
         }
+
+        /** Returns an [Intent] that can be used to start the camera in video mode. */
+        @JvmStatic
+        fun getVideoCameraIntent(): Intent {
+            return Intent(VIDEO_CAMERA_INTENT_ACTION)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
index cf02f8f..a434617 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
@@ -21,7 +21,9 @@
 import javax.inject.Inject
 
 /** Injectable wrapper around [CameraIntents]. */
-class CameraIntentsWrapper @Inject constructor(
+class CameraIntentsWrapper
+@Inject
+constructor(
     private val context: Context,
 ) {
 
@@ -40,4 +42,9 @@
     fun getInsecureCameraIntent(): Intent {
         return CameraIntents.getInsecureCameraIntent(context)
     }
+
+    /** Returns an [Intent] that can be used to start the camera in video mode. */
+    fun getVideoCameraIntent(): Intent {
+        return CameraIntents.getVideoCameraIntent()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index 1454210..5ca36ab 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -20,7 +20,7 @@
 import android.content.res.Configuration
 import android.graphics.PixelFormat
 import android.os.SystemProperties
-import android.util.DisplayMetrics
+import android.view.Surface
 import android.view.View
 import android.view.WindowManager
 import com.android.internal.annotations.VisibleForTesting
@@ -36,7 +36,6 @@
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.leak.RotationUtils
 import com.android.systemui.util.time.SystemClock
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -73,7 +72,7 @@
         height = WindowManager.LayoutParams.MATCH_PARENT
         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
         format = PixelFormat.TRANSLUCENT
-        type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
+        type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
         fitInsetsTypes = 0 // Ignore insets from all system bars
         title = "Wired Charging Animation"
         flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -172,30 +171,28 @@
     }
 
     private fun layoutRipple() {
-        val displayMetrics = DisplayMetrics()
-        context.display.getRealMetrics(displayMetrics)
-        val width = displayMetrics.widthPixels
-        val height = displayMetrics.heightPixels
+        val bounds = windowManager.currentWindowMetrics.bounds
+        val width = bounds.width()
+        val height = bounds.height()
         val maxDiameter = Integer.max(width, height) * 2f
         rippleView.setMaxSize(maxDiameter, maxDiameter)
-        when (RotationUtils.getExactRotation(context)) {
-            RotationUtils.ROTATION_LANDSCAPE -> {
+        when (context.display.rotation) {
+            Surface.ROTATION_0 -> {
+                rippleView.setCenter(
+                        width * normalizedPortPosX, height * normalizedPortPosY)
+            }
+            Surface.ROTATION_90 -> {
                 rippleView.setCenter(
                         width * normalizedPortPosY, height * (1 - normalizedPortPosX))
             }
-            RotationUtils.ROTATION_UPSIDE_DOWN -> {
+            Surface.ROTATION_180 -> {
                 rippleView.setCenter(
                         width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
             }
-            RotationUtils.ROTATION_SEASCAPE -> {
+            Surface.ROTATION_270 -> {
                 rippleView.setCenter(
                         width * (1 - normalizedPortPosY), height * normalizedPortPosX)
             }
-            else -> {
-                // ROTATION_NONE
-                rippleView.setCenter(
-                        width * normalizedPortPosX, height * normalizedPortPosY)
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
index 36103f8..cef415c 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -33,7 +33,9 @@
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
+import com.android.systemui.surfaceeffects.ripple.RippleShader;
 import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
 import com.android.systemui.surfaceeffects.ripple.RippleView;
 
@@ -44,10 +46,12 @@
  */
 final class WirelessChargingLayout extends FrameLayout {
     private static final long CIRCLE_RIPPLE_ANIMATION_DURATION = 1500;
-    private static final long ROUNDED_BOX_RIPPLE_ANIMATION_DURATION = 1750;
+    private static final long ROUNDED_BOX_RIPPLE_ANIMATION_DURATION = 3000;
     private static final int SCRIM_COLOR = 0x4C000000;
     private static final int SCRIM_FADE_DURATION = 300;
     private RippleView mRippleView;
+    // This is only relevant to the rounded box ripple.
+    private RippleShader.SizeAtProgress[] mSizeAtProgressArray;
 
     WirelessChargingLayout(Context context, int transmittingBatteryLevel, int batteryLevel,
             boolean isDozing, RippleShape rippleShape) {
@@ -125,20 +129,23 @@
         AnimatorSet animatorSet = new AnimatorSet();
         animatorSet.playTogether(textSizeAnimator, textOpacityAnimator, textFadeAnimator);
 
-        ValueAnimator scrimFadeInAnimator = ObjectAnimator.ofArgb(this,
-                "backgroundColor", Color.TRANSPARENT, SCRIM_COLOR);
-        scrimFadeInAnimator.setDuration(SCRIM_FADE_DURATION);
-        scrimFadeInAnimator.setInterpolator(Interpolators.LINEAR);
-        ValueAnimator scrimFadeOutAnimator = ObjectAnimator.ofArgb(this,
-                "backgroundColor", SCRIM_COLOR, Color.TRANSPARENT);
-        scrimFadeOutAnimator.setDuration(SCRIM_FADE_DURATION);
-        scrimFadeOutAnimator.setInterpolator(Interpolators.LINEAR);
-        scrimFadeOutAnimator.setStartDelay((rippleShape == RippleShape.CIRCLE
-                ? CIRCLE_RIPPLE_ANIMATION_DURATION : ROUNDED_BOX_RIPPLE_ANIMATION_DURATION)
-                - SCRIM_FADE_DURATION);
-        AnimatorSet animatorSetScrim = new AnimatorSet();
-        animatorSetScrim.playTogether(scrimFadeInAnimator, scrimFadeOutAnimator);
-        animatorSetScrim.start();
+        // For tablet docking animation, we don't play the background scrim.
+        if (!Utilities.isTablet(context)) {
+            ValueAnimator scrimFadeInAnimator = ObjectAnimator.ofArgb(this,
+                    "backgroundColor", Color.TRANSPARENT, SCRIM_COLOR);
+            scrimFadeInAnimator.setDuration(SCRIM_FADE_DURATION);
+            scrimFadeInAnimator.setInterpolator(Interpolators.LINEAR);
+            ValueAnimator scrimFadeOutAnimator = ObjectAnimator.ofArgb(this,
+                    "backgroundColor", SCRIM_COLOR, Color.TRANSPARENT);
+            scrimFadeOutAnimator.setDuration(SCRIM_FADE_DURATION);
+            scrimFadeOutAnimator.setInterpolator(Interpolators.LINEAR);
+            scrimFadeOutAnimator.setStartDelay((rippleShape == RippleShape.CIRCLE
+                    ? CIRCLE_RIPPLE_ANIMATION_DURATION : ROUNDED_BOX_RIPPLE_ANIMATION_DURATION)
+                    - SCRIM_FADE_DURATION);
+            AnimatorSet animatorSetScrim = new AnimatorSet();
+            animatorSetScrim.playTogether(scrimFadeInAnimator, scrimFadeOutAnimator);
+            animatorSetScrim.start();
+        }
 
         mRippleView = findViewById(R.id.wireless_charging_ripple);
         mRippleView.setupShader(rippleShape);
@@ -147,7 +154,26 @@
         if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
             mRippleView.setDuration(ROUNDED_BOX_RIPPLE_ANIMATION_DURATION);
             mRippleView.setSparkleStrength(0.22f);
-            mRippleView.setColor(color, 28);
+            mRippleView.setColor(color, 102); // 40% of opacity.
+            mRippleView.setBaseRingFadeParams(
+                    /* fadeInStart = */ 0f,
+                    /* fadeInEnd = */ 0f,
+                    /* fadeOutStart = */ 0.2f,
+                    /* fadeOutEnd= */ 0.47f
+            );
+            mRippleView.setSparkleRingFadeParams(
+                    /* fadeInStart = */ 0f,
+                    /* fadeInEnd = */ 0f,
+                    /* fadeOutStart = */ 0.2f,
+                    /* fadeOutEnd= */ 1f
+            );
+            mRippleView.setCenterFillFadeParams(
+                    /* fadeInStart = */ 0f,
+                    /* fadeInEnd = */ 0f,
+                    /* fadeOutStart = */ 0f,
+                    /* fadeOutEnd= */ 0.2f
+            );
+            mRippleView.setBlur(6.5f, 2.5f);
         } else {
             mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
             mRippleView.setColor(color, RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA);
@@ -246,9 +272,7 @@
             int height = getMeasuredHeight();
             mRippleView.setCenter(width * 0.5f, height * 0.5f);
             if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
-                // Those magic numbers are introduced for visual polish. This aspect ratio maps with
-                // the tablet's docking station.
-                mRippleView.setMaxSize(width * 1.36f, height * 1.46f);
+                updateRippleSizeAtProgressList(width, height);
             } else {
                 float maxSize = Math.max(width, height);
                 mRippleView.setMaxSize(maxSize, maxSize);
@@ -257,4 +281,36 @@
 
         super.onLayout(changed, left, top, right, bottom);
     }
+
+    private void updateRippleSizeAtProgressList(float width, float height) {
+        if (mSizeAtProgressArray == null) {
+            float maxSize = Math.max(width, height);
+            mSizeAtProgressArray = new RippleShader.SizeAtProgress[] {
+                    // Those magic numbers are introduced for visual polish. It starts from a pill
+                    // shape and expand to a full circle.
+                    new RippleShader.SizeAtProgress(0f, 0f, 0f),
+                    new RippleShader.SizeAtProgress(0.3f, width * 0.4f, height * 0.4f),
+                    new RippleShader.SizeAtProgress(1f, maxSize, maxSize)
+            };
+        } else {
+            // Same multipliers, just need to recompute with the new width and height.
+            RippleShader.SizeAtProgress first = mSizeAtProgressArray[0];
+            first.setT(0f);
+            first.setWidth(0f);
+            first.setHeight(0f);
+
+            RippleShader.SizeAtProgress second = mSizeAtProgressArray[1];
+            second.setT(0.3f);
+            second.setWidth(width * 0.4f);
+            second.setHeight(height * 0.4f);
+
+            float maxSize = Math.max(width, height);
+            RippleShader.SizeAtProgress last = mSizeAtProgressArray[2];
+            last.setT(1f);
+            last.setWidth(maxSize);
+            last.setHeight(maxSize);
+        }
+
+        mRippleView.setSizeAtProgresses(mSizeAtProgressArray);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index e8e1f2e..f6b7133 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -176,7 +176,8 @@
     private @Classifier.InteractionType int mPriorInteractionType = Classifier.GENERIC;
 
     @Inject
-    public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider,
+    public BrightLineFalsingManager(
+            FalsingDataProvider falsingDataProvider,
             MetricsLogger metricsLogger,
             @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers,
             SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier,
@@ -399,7 +400,9 @@
                 || mDataProvider.isJustUnlockedWithFace()
                 || mDataProvider.isDocked()
                 || mAccessibilityManager.isTouchExplorationEnabled()
-                || mDataProvider.isA11yAction();
+                || mDataProvider.isA11yAction()
+                || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED)
+                    && mDataProvider.isUnfolded());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index 09ebeea..bc0f995 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.classifier;
 
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.util.DisplayMetrics;
 import android.view.MotionEvent;
 import android.view.MotionEvent.PointerCoords;
@@ -42,6 +43,7 @@
     private final int mWidthPixels;
     private final int mHeightPixels;
     private BatteryController mBatteryController;
+    private final FoldStateListener mFoldStateListener;
     private final DockManager mDockManager;
     private final float mXdpi;
     private final float mYdpi;
@@ -65,12 +67,14 @@
     public FalsingDataProvider(
             DisplayMetrics displayMetrics,
             BatteryController batteryController,
+            FoldStateListener foldStateListener,
             DockManager dockManager) {
         mXdpi = displayMetrics.xdpi;
         mYdpi = displayMetrics.ydpi;
         mWidthPixels = displayMetrics.widthPixels;
         mHeightPixels = displayMetrics.heightPixels;
         mBatteryController = batteryController;
+        mFoldStateListener = foldStateListener;
         mDockManager = dockManager;
 
         FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi());
@@ -376,6 +380,10 @@
         return mBatteryController.isWirelessCharging() || mDockManager.isDocked();
     }
 
+    public boolean isUnfolded() {
+        return Boolean.FALSE.equals(mFoldStateListener.getFolded());
+    }
+
     /** Implement to be alerted abotu the beginning and ending of falsing tracking. */
     public interface SessionListener {
         /** Called when the lock screen is shown and falsing-tracking begins. */
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 805a20a..edda8752 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -18,10 +18,10 @@
 
 import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
 
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
 
 import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
 
@@ -29,7 +29,6 @@
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.os.SystemProperties;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -38,8 +37,6 @@
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.util.DeviceConfigProxy;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -59,42 +56,31 @@
             "com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
 
     private final Context mContext;
-    private final DeviceConfigProxy mDeviceConfig;
     private final Provider<ClipboardOverlayController> mOverlayProvider;
-    private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
     private final ClipboardToast mClipboardToast;
     private final ClipboardManager mClipboardManager;
-    private final UiEventLogger mUiEventLogger;
     private final FeatureFlags mFeatureFlags;
-    private boolean mUsingNewOverlay;
+    private final UiEventLogger mUiEventLogger;
     private ClipboardOverlay mClipboardOverlay;
 
     @Inject
-    public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
+    public ClipboardListener(Context context,
             Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
-            ClipboardOverlayControllerLegacyFactory overlayFactory,
             ClipboardToast clipboardToast,
             ClipboardManager clipboardManager,
-            UiEventLogger uiEventLogger,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            UiEventLogger uiEventLogger) {
         mContext = context;
-        mDeviceConfig = deviceConfigProxy;
         mOverlayProvider = clipboardOverlayControllerProvider;
-        mOverlayFactory = overlayFactory;
         mClipboardToast = clipboardToast;
         mClipboardManager = clipboardManager;
-        mUiEventLogger = uiEventLogger;
         mFeatureFlags = featureFlags;
-
-        mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
+        mUiEventLogger = uiEventLogger;
     }
 
     @Override
     public void start() {
-        if (mDeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, true)) {
-            mClipboardManager.addPrimaryClipChangedListener(this);
-        }
+        mClipboardManager.addPrimaryClipChangedListener(this);
     }
 
     @Override
@@ -111,8 +97,9 @@
             return;
         }
 
-        if (!isUserSetupComplete()) {
-            // just show a toast, user should not access intents from this state
+        if (!isUserSetupComplete() // user should not access intents from this state
+                || clipData == null // shouldn't happen, but just in case
+                || clipData.getItemCount() == 0) {
             if (shouldShowToast(clipData)) {
                 mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
                 mClipboardToast.showCopiedToast();
@@ -120,19 +107,17 @@
             return;
         }
 
-        boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
-        if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
-            mUsingNewOverlay = enabled;
-            if (enabled) {
-                mClipboardOverlay = mOverlayProvider.get();
-            } else {
-                mClipboardOverlay = mOverlayFactory.create(mContext);
-            }
+        if (mClipboardOverlay == null) {
+            mClipboardOverlay = mOverlayProvider.get();
             mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
         } else {
             mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
         }
-        mClipboardOverlay.setClipData(clipData, clipSource);
+        if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+            mClipboardOverlay.setClipData(clipData, clipSource);
+        } else {
+            mClipboardOverlay.setClipDataLegacy(clipData, clipSource);
+        }
         mClipboardOverlay.setOnSessionCompleteListener(() -> {
             // Session is complete, free memory until it's needed again.
             mClipboardOverlay = null;
@@ -175,6 +160,8 @@
     }
 
     interface ClipboardOverlay {
+        void setClipDataLegacy(ClipData clipData, String clipSource);
+
         void setClipData(ClipData clipData, String clipSource);
 
         void setOnSessionCompleteListener(Runnable runnable);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt
new file mode 100644
index 0000000..789833c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.clipboardoverlay
+
+import android.content.ClipData
+import android.content.ClipDescription.EXTRA_IS_SENSITIVE
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.text.TextUtils
+import android.util.Log
+import android.util.Size
+import android.view.textclassifier.TextLinks
+import com.android.systemui.R
+import java.io.IOException
+
+data class ClipboardModel(
+    val clipData: ClipData,
+    val source: String,
+    val type: Type,
+    val text: CharSequence?,
+    val textLinks: TextLinks?,
+    val uri: Uri?,
+    val isSensitive: Boolean,
+    val isRemote: Boolean,
+) {
+    private var _bitmap: Bitmap? = null
+
+    fun dataMatches(other: ClipboardModel?): Boolean {
+        if (other == null) {
+            return false
+        }
+        return source == other.source &&
+            type == other.type &&
+            text == other.text &&
+            uri == other.uri &&
+            isSensitive == other.isSensitive
+    }
+
+    fun loadThumbnail(context: Context): Bitmap? {
+        if (_bitmap == null && type == Type.IMAGE && uri != null) {
+            try {
+                val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
+                _bitmap = context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null)
+            } catch (e: IOException) {
+                Log.e(TAG, "Thumbnail loading failed!", e)
+            }
+        }
+        return _bitmap
+    }
+
+    internal companion object {
+        private val TAG: String = "ClipboardModel"
+
+        @JvmStatic
+        fun fromClipData(
+            context: Context,
+            utils: ClipboardOverlayUtils,
+            clipData: ClipData,
+            source: String
+        ): ClipboardModel {
+            val sensitive = clipData.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false
+            val item = clipData.getItemAt(0)!!
+            val type = getType(context, item)
+            val remote = utils.isRemoteCopy(context, clipData, source)
+            return ClipboardModel(
+                clipData,
+                source,
+                type,
+                item.text,
+                item.textLinks,
+                item.uri,
+                sensitive,
+                remote
+            )
+        }
+
+        private fun getType(context: Context, item: ClipData.Item): Type {
+            return if (!TextUtils.isEmpty(item.text)) {
+                Type.TEXT
+            } else if (item.uri != null) {
+                if (context.contentResolver.getType(item.uri)?.startsWith("image") == true) {
+                    Type.IMAGE
+                } else {
+                    Type.URI
+                }
+            } else {
+                Type.OTHER
+            }
+        }
+    }
+
+    enum class Type {
+        TEXT,
+        IMAGE,
+        URI,
+        OTHER
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index f97d6af..c214f53 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -17,11 +17,8 @@
 package com.android.systemui.clipboardoverlay;
 
 import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
@@ -32,10 +29,9 @@
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
 import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
 
-import static java.util.Objects.requireNonNull;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.RemoteAction;
@@ -48,7 +44,6 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
-import android.hardware.display.DisplayManager;
 import android.hardware.input.InputManager;
 import android.net.Uri;
 import android.os.Looper;
@@ -56,14 +51,15 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Size;
-import android.view.Display;
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.InputMonitor;
 import android.view.MotionEvent;
+import android.view.WindowInsets;
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -95,7 +91,6 @@
     private final Context mContext;
     private final ClipboardLogger mClipboardLogger;
     private final BroadcastDispatcher mBroadcastDispatcher;
-    private final DisplayManager mDisplayManager;
     private final ClipboardOverlayWindow mWindow;
     private final TimeoutHandler mTimeoutHandler;
     private final ClipboardOverlayUtils mClipboardUtils;
@@ -107,7 +102,6 @@
     private Runnable mOnSessionCompleteListener;
     private Runnable mOnRemoteCopyTapped;
     private Runnable mOnShareTapped;
-    private Runnable mOnEditTapped;
     private Runnable mOnPreviewTapped;
 
     private InputMonitor mInputMonitor;
@@ -121,6 +115,9 @@
 
     private Runnable mOnUiUpdate;
 
+    private boolean mIsMinimized;
+    private ClipboardModel mClipboardModel;
+
     private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
             new ClipboardOverlayView.ClipboardOverlayCallbacks() {
                 @Override
@@ -156,13 +153,6 @@
                 }
 
                 @Override
-                public void onEditButtonTapped() {
-                    if (mOnEditTapped != null) {
-                        mOnEditTapped.run();
-                    }
-                }
-
-                @Override
                 public void onRemoteCopyButtonTapped() {
                     if (mOnRemoteCopyTapped != null) {
                         mOnRemoteCopyTapped.run();
@@ -174,6 +164,13 @@
                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
                     animateOut();
                 }
+
+                @Override
+                public void onMinimizedViewTapped() {
+                    if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+                        animateFromMinimized();
+                    }
+                }
             };
 
     @Inject
@@ -187,30 +184,27 @@
             ClipboardOverlayUtils clipboardUtils,
             @Background Executor bgExecutor,
             UiEventLogger uiEventLogger) {
+        mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
-        mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
-        final Context displayContext = context.createDisplayContext(getDefaultDisplay());
-        mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
 
         mClipboardLogger = new ClipboardLogger(uiEventLogger);
 
         mView = clipboardOverlayView;
         mWindow = clipboardOverlayWindow;
-        mWindow.init(mView::setInsets, () -> {
+        mWindow.init(this::onInsetsChanged, () -> {
             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
             hideImmediate();
         });
 
+        mFeatureFlags = featureFlags;
         mTimeoutHandler = timeoutHandler;
         mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
 
-        mFeatureFlags = featureFlags;
         mClipboardUtils = clipboardUtils;
         mBgExecutor = bgExecutor;
 
         mView.setCallbacks(mClipboardCallbacks);
 
-
         mWindow.withWindowAttached(() -> {
             mWindow.setContentView(mView);
             mView.setInsets(mWindow.getWindowInsets(),
@@ -255,8 +249,136 @@
         broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
     }
 
+    @VisibleForTesting
+    void onInsetsChanged(WindowInsets insets, int orientation) {
+        mView.setInsets(insets, orientation);
+        if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+            if (shouldShowMinimized(insets) && !mIsMinimized) {
+                mIsMinimized = true;
+                mView.setMinimized(true);
+            }
+        }
+    }
+
     @Override // ClipboardListener.ClipboardOverlay
-    public void setClipData(ClipData clipData, String clipSource) {
+    public void setClipData(ClipData data, String source) {
+        ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
+        if (mExitAnimator != null && mExitAnimator.isRunning()) {
+            mExitAnimator.cancel();
+        }
+        boolean shouldAnimate = !model.dataMatches(mClipboardModel);
+        mClipboardModel = model;
+        mClipboardLogger.setClipSource(mClipboardModel.getSource());
+        if (shouldAnimate) {
+            reset();
+            mClipboardLogger.setClipSource(mClipboardModel.getSource());
+            if (shouldShowMinimized(mWindow.getWindowInsets())) {
+                mIsMinimized = true;
+                mView.setMinimized(true);
+            } else {
+                setExpandedView();
+            }
+            animateIn();
+            mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
+        } else if (!mIsMinimized) {
+            setExpandedView();
+        }
+        if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && mClipboardModel.isRemote()) {
+            mTimeoutHandler.cancelTimeout();
+            mOnUiUpdate = null;
+        } else {
+            mOnUiUpdate = mTimeoutHandler::resetTimeout;
+            mOnUiUpdate.run();
+        }
+    }
+
+    private void setExpandedView() {
+        final ClipboardModel model = mClipboardModel;
+        mView.setMinimized(false);
+        switch (model.getType()) {
+            case TEXT:
+                if ((mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && model.isRemote())
+                        || DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
+                    if (model.getTextLinks() != null) {
+                        classifyText(model);
+                    }
+                }
+                if (model.isSensitive()) {
+                    mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
+                } else {
+                    mView.showTextPreview(model.getText(), false);
+                }
+                mView.setEditAccessibilityAction(true);
+                mOnPreviewTapped = this::editText;
+                break;
+            case IMAGE:
+                if (model.isSensitive() || model.loadThumbnail(mContext) != null) {
+                    mView.showImagePreview(
+                            model.isSensitive() ? null : model.loadThumbnail(mContext));
+                    mView.setEditAccessibilityAction(true);
+                    mOnPreviewTapped = () -> editImage(model.getUri());
+                } else {
+                    // image loading failed
+                    mView.showDefaultTextPreview();
+                }
+                break;
+            case URI:
+            case OTHER:
+                mView.showDefaultTextPreview();
+                break;
+        }
+        if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)) {
+            if (!model.isRemote()) {
+                maybeShowRemoteCopy(model.getClipData());
+            }
+        } else {
+            maybeShowRemoteCopy(model.getClipData());
+        }
+        if (model.getType() != ClipboardModel.Type.OTHER) {
+            mOnShareTapped = () -> shareContent(model.getClipData());
+            mView.showShareChip();
+        }
+    }
+
+    private boolean shouldShowMinimized(WindowInsets insets) {
+        return insets.getInsets(WindowInsets.Type.ime()).bottom > 0;
+    }
+
+    private void animateFromMinimized() {
+        mIsMinimized = false;
+        setExpandedView();
+        animateIn();
+    }
+
+    private String getAccessibilityAnnouncement(ClipboardModel.Type type) {
+        if (type == ClipboardModel.Type.TEXT) {
+            return mContext.getString(R.string.clipboard_text_copied);
+        } else if (type == ClipboardModel.Type.IMAGE) {
+            return mContext.getString(R.string.clipboard_image_copied);
+        } else {
+            return mContext.getString(R.string.clipboard_content_copied);
+        }
+    }
+
+    private void classifyText(ClipboardModel model) {
+        mBgExecutor.execute(() -> {
+            Optional<RemoteAction> remoteAction = mClipboardUtils.getAction(
+                            model.getText(), model.getTextLinks(), model.getSource());
+            if (model.equals(mClipboardModel)) {
+                remoteAction.ifPresent(action -> {
+                    mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
+                    mView.setActionChip(action, () -> {
+                        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+                        animateOut();
+                    });
+                });
+            }
+        });
+    }
+
+    @Override // ClipboardListener.ClipboardOverlay
+    public void setClipDataLegacy(ClipData clipData, String clipSource) {
         if (mExitAnimator != null && mExitAnimator.isRunning()) {
             mExitAnimator.cancel();
         }
@@ -289,10 +411,10 @@
             accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
         } else if (clipData.getItemAt(0).getUri() != null) {
             if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
-                mOnShareTapped = () -> shareContent(clipData);
-                mView.showShareChip();
                 accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
             }
+            mOnShareTapped = () -> shareContent(clipData);
+            mView.showShareChip();
         } else {
             mView.showDefaultTextPreview();
         }
@@ -392,11 +514,6 @@
         mView.showTextPreview(text, hidden);
         mView.setEditAccessibilityAction(true);
         mOnPreviewTapped = this::editText;
-        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
-            mOnEditTapped = this::editText;
-            mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description));
-        }
     }
 
     private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
@@ -427,10 +544,6 @@
         } else {
             mView.showDefaultTextPreview();
         }
-        if (isEditableImage && DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
-            mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description));
-        }
         return isEditableImage;
     }
 
@@ -506,17 +619,12 @@
     private void reset() {
         mOnRemoteCopyTapped = null;
         mOnShareTapped = null;
-        mOnEditTapped = null;
         mOnPreviewTapped = null;
         mView.reset();
         mTimeoutHandler.cancelTimeout();
         mClipboardLogger.reset();
     }
 
-    private Display getDefaultDisplay() {
-        return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
-    }
-
     static class ClipboardLogger {
         private final UiEventLogger mUiEventLogger;
         private String mClipSource;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
deleted file mode 100644
index 3a040829..0000000
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
+++ /dev/null
@@ -1,963 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.clipboardoverlay;
-
-import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
-
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
-
-import static java.util.Objects.requireNonNull;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.MainThread;
-import android.app.ICompatCameraControlCallback;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.Icon;
-import android.hardware.display.DisplayManager;
-import android.hardware.input.InputManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Looper;
-import android.provider.DeviceConfig;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.MathUtils;
-import android.util.Size;
-import android.util.TypedValue;
-import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.Gravity;
-import android.view.InputEvent;
-import android.view.InputEventReceiver;
-import android.view.InputMonitor;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.LinearInterpolator;
-import android.view.animation.PathInterpolator;
-import android.view.textclassifier.TextClassification;
-import android.view.textclassifier.TextClassificationManager;
-import android.view.textclassifier.TextClassifier;
-import android.view.textclassifier.TextLinks;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.PhoneWindow;
-import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.screenshot.DraggableConstraintLayout;
-import com.android.systemui.screenshot.FloatingWindowUtil;
-import com.android.systemui.screenshot.OverlayActionChip;
-import com.android.systemui.screenshot.TimeoutHandler;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * Controls state and UI for the overlay that appears when something is added to the clipboard
- */
-public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay {
-    private static final String TAG = "ClipboardOverlayCtrlr";
-    private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY";
-
-    /** Constants for screenshot/copy deconflicting */
-    public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT";
-    public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF";
-    public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
-
-    private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard";
-
-    private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
-    private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
-    private static final int FONT_SEARCH_STEP_PX = 4;
-
-    private final Context mContext;
-    private final ClipboardLogger mClipboardLogger;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final DisplayManager mDisplayManager;
-    private final DisplayMetrics mDisplayMetrics;
-    private final WindowManager mWindowManager;
-    private final WindowManager.LayoutParams mWindowLayoutParams;
-    private final PhoneWindow mWindow;
-    private final TimeoutHandler mTimeoutHandler;
-    private final AccessibilityManager mAccessibilityManager;
-    private final TextClassifier mTextClassifier;
-
-    private final DraggableConstraintLayout mView;
-    private final View mClipboardPreview;
-    private final ImageView mImagePreview;
-    private final TextView mTextPreview;
-    private final TextView mHiddenPreview;
-    private final View mPreviewBorder;
-    private final OverlayActionChip mEditChip;
-    private final OverlayActionChip mShareChip;
-    private final OverlayActionChip mRemoteCopyChip;
-    private final View mActionContainerBackground;
-    private final View mDismissButton;
-    private final LinearLayout mActionContainer;
-    private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
-
-    private Runnable mOnSessionCompleteListener;
-
-    private InputMonitor mInputMonitor;
-    private InputEventReceiver mInputEventReceiver;
-
-    private BroadcastReceiver mCloseDialogsReceiver;
-    private BroadcastReceiver mScreenshotReceiver;
-
-    private boolean mBlockAttach = false;
-    private Animator mExitAnimator;
-    private Animator mEnterAnimator;
-    private final int mOrientation;
-    private boolean mKeyboardVisible;
-
-
-    public ClipboardOverlayControllerLegacy(Context context,
-            BroadcastDispatcher broadcastDispatcher,
-            BroadcastSender broadcastSender,
-            TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
-        mBroadcastDispatcher = broadcastDispatcher;
-        mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
-        final Context displayContext = context.createDisplayContext(getDefaultDisplay());
-        mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
-
-        mClipboardLogger = new ClipboardLogger(uiEventLogger);
-
-        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
-        mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
-                .getTextClassifier();
-
-        mWindowManager = mContext.getSystemService(WindowManager.class);
-
-        mDisplayMetrics = new DisplayMetrics();
-        mContext.getDisplay().getRealMetrics(mDisplayMetrics);
-
-        mTimeoutHandler = timeoutHandler;
-        mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
-
-        // Setup the window that we are going to use
-        mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
-        mWindowLayoutParams.setTitle("ClipboardOverlay");
-
-        mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
-        mWindow.setWindowManager(mWindowManager, null, null);
-
-        setWindowFocusable(false);
-
-        mView = (DraggableConstraintLayout)
-                LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null);
-        mActionContainerBackground =
-                requireNonNull(mView.findViewById(R.id.actions_container_background));
-        mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
-        mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
-        mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
-        mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
-        mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview));
-        mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
-        mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
-        mShareChip = requireNonNull(mView.findViewById(R.id.share_chip));
-        mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
-        mEditChip.setAlpha(1);
-        mShareChip.setAlpha(1);
-        mRemoteCopyChip.setAlpha(1);
-        mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
-
-        mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
-        mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
-            @Override
-            public void onInteraction() {
-                mTimeoutHandler.resetTimeout();
-            }
-
-            @Override
-            public void onSwipeDismissInitiated(Animator animator) {
-                mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
-                mExitAnimator = animator;
-            }
-
-            @Override
-            public void onDismissComplete() {
-                hideImmediate();
-            }
-        });
-
-        mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
-            int availableHeight = mTextPreview.getHeight()
-                    - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
-            mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
-            return true;
-        });
-
-        mDismissButton.setOnClickListener(view -> {
-            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
-            animateOut();
-        });
-
-        mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
-        mRemoteCopyChip.setIcon(
-                Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
-        mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
-        mOrientation = mContext.getResources().getConfiguration().orientation;
-
-        attachWindow();
-        withWindowAttached(() -> {
-            mWindow.setContentView(mView);
-            WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
-            mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
-            updateInsets(insets);
-            mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
-                    new ViewTreeObserver.OnGlobalLayoutListener() {
-                        @Override
-                        public void onGlobalLayout() {
-                            WindowInsets insets =
-                                    mWindowManager.getCurrentWindowMetrics().getWindowInsets();
-                            boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
-                            if (keyboardVisible != mKeyboardVisible) {
-                                mKeyboardVisible = keyboardVisible;
-                                updateInsets(insets);
-                            }
-                        }
-                    });
-            mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
-                    new ViewRootImpl.ActivityConfigCallback() {
-                        @Override
-                        public void onConfigurationChanged(Configuration overrideConfig,
-                                int newDisplayId) {
-                            if (mContext.getResources().getConfiguration().orientation
-                                    != mOrientation) {
-                                mClipboardLogger.logSessionComplete(
-                                        CLIPBOARD_OVERLAY_DISMISSED_OTHER);
-                                hideImmediate();
-                            }
-                        }
-
-                        @Override
-                        public void requestCompatCameraControl(
-                                boolean showControl, boolean transformationApplied,
-                                ICompatCameraControlCallback callback) {
-                            Log.w(TAG, "unexpected requestCompatCameraControl call");
-                        }
-                    });
-        });
-
-        mTimeoutHandler.setOnTimeoutRunnable(() -> {
-            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
-            animateOut();
-        });
-
-        mCloseDialogsReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
-                    mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
-                    animateOut();
-                }
-            }
-        };
-
-        mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
-                new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
-        mScreenshotReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (SCREENSHOT_ACTION.equals(intent.getAction())) {
-                    mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
-                    animateOut();
-                }
-            }
-        };
-
-        mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
-                new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
-                SELF_PERMISSION);
-        monitorOutsideTouches();
-
-        Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
-        // Set package name so the system knows it's safe
-        copyIntent.setPackage(mContext.getPackageName());
-        broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
-    }
-
-    @Override // ClipboardListener.ClipboardOverlay
-    public void setClipData(ClipData clipData, String clipSource) {
-        if (mExitAnimator != null && mExitAnimator.isRunning()) {
-            mExitAnimator.cancel();
-        }
-        reset();
-        String accessibilityAnnouncement;
-
-        boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
-                && clipData.getDescription().getExtras()
-                .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
-        if (clipData == null || clipData.getItemCount() == 0) {
-            showTextPreview(
-                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
-                    mTextPreview);
-            accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
-        } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
-            ClipData.Item item = clipData.getItemAt(0);
-            if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                    CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
-                if (item.getTextLinks() != null) {
-                    AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource));
-                }
-            }
-            if (isSensitive) {
-                showEditableText(
-                        mContext.getResources().getString(R.string.clipboard_asterisks), true);
-            } else {
-                showEditableText(item.getText(), false);
-            }
-            showShareChip(clipData);
-            accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
-        } else if (clipData.getItemAt(0).getUri() != null) {
-            if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
-                showShareChip(clipData);
-                accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
-            } else {
-                accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
-            }
-        } else {
-            showTextPreview(
-                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
-                    mTextPreview);
-            accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
-        }
-        Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
-        // Only show remote copy if it's available.
-        PackageManager packageManager = mContext.getPackageManager();
-        if (packageManager.resolveActivity(
-                remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
-            mRemoteCopyChip.setContentDescription(
-                    mContext.getString(R.string.clipboard_send_nearby_description));
-            mRemoteCopyChip.setVisibility(View.VISIBLE);
-            mRemoteCopyChip.setOnClickListener((v) -> {
-                mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
-                mContext.startActivity(remoteCopyIntent);
-                animateOut();
-            });
-            mActionContainerBackground.setVisibility(View.VISIBLE);
-        } else {
-            mRemoteCopyChip.setVisibility(View.GONE);
-        }
-        withWindowAttached(() -> {
-            if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
-                mView.post(this::animateIn);
-            }
-            mView.announceForAccessibility(accessibilityAnnouncement);
-        });
-        mTimeoutHandler.resetTimeout();
-    }
-
-    @Override // ClipboardListener.ClipboardOverlay
-    public void setOnSessionCompleteListener(Runnable runnable) {
-        mOnSessionCompleteListener = runnable;
-    }
-
-    private void classifyText(ClipData.Item item, String source) {
-        ArrayList<RemoteAction> actions = new ArrayList<>();
-        for (TextLinks.TextLink link : item.getTextLinks().getLinks()) {
-            TextClassification classification = mTextClassifier.classifyText(
-                    item.getText(), link.getStart(), link.getEnd(), null);
-            actions.addAll(classification.getActions());
-        }
-        mView.post(() -> {
-            resetActionChips();
-            if (actions.size() > 0) {
-                mActionContainerBackground.setVisibility(View.VISIBLE);
-                for (RemoteAction action : actions) {
-                    Intent targetIntent = action.getActionIntent().getIntent();
-                    ComponentName component = targetIntent.getComponent();
-                    if (component != null && !TextUtils.equals(source,
-                            component.getPackageName())) {
-                        OverlayActionChip chip = constructActionChip(action);
-                        mActionContainer.addView(chip);
-                        mActionChips.add(chip);
-                        break; // only show at most one action chip
-                    }
-                }
-            }
-        });
-    }
-
-    private void showShareChip(ClipData clip) {
-        mShareChip.setVisibility(View.VISIBLE);
-        mActionContainerBackground.setVisibility(View.VISIBLE);
-        mShareChip.setOnClickListener((v) -> shareContent(clip));
-    }
-
-    private OverlayActionChip constructActionChip(RemoteAction action) {
-        OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
-                R.layout.overlay_action_chip, mActionContainer, false);
-        chip.setText(action.getTitle());
-        chip.setContentDescription(action.getTitle());
-        chip.setIcon(action.getIcon(), false);
-        chip.setPendingIntent(action.getActionIntent(), () -> {
-            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
-            animateOut();
-        });
-        chip.setAlpha(1);
-        return chip;
-    }
-
-    private void monitorOutsideTouches() {
-        InputManager inputManager = mContext.getSystemService(InputManager.class);
-        mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
-        mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
-                Looper.getMainLooper()) {
-            @Override
-            public void onInputEvent(InputEvent event) {
-                if (event instanceof MotionEvent) {
-                    MotionEvent motionEvent = (MotionEvent) event;
-                    if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
-                        Region touchRegion = new Region();
-
-                        final Rect tmpRect = new Rect();
-                        mPreviewBorder.getBoundsOnScreen(tmpRect);
-                        tmpRect.inset(
-                                (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
-                                (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
-                                        -SWIPE_PADDING_DP));
-                        touchRegion.op(tmpRect, Region.Op.UNION);
-                        mActionContainerBackground.getBoundsOnScreen(tmpRect);
-                        tmpRect.inset(
-                                (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
-                                (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
-                                        -SWIPE_PADDING_DP));
-                        touchRegion.op(tmpRect, Region.Op.UNION);
-                        mDismissButton.getBoundsOnScreen(tmpRect);
-                        touchRegion.op(tmpRect, Region.Op.UNION);
-                        if (!touchRegion.contains(
-                                (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
-                            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
-                            animateOut();
-                        }
-                    }
-                }
-                finishInputEvent(event, true /* handled */);
-            }
-        };
-    }
-
-    private void editImage(Uri uri) {
-        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
-        mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
-        animateOut();
-    }
-
-    private void editText() {
-        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
-        mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
-        animateOut();
-    }
-
-    private void shareContent(ClipData clip) {
-        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
-        mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
-        animateOut();
-    }
-
-    private void showSinglePreview(View v) {
-        mTextPreview.setVisibility(View.GONE);
-        mImagePreview.setVisibility(View.GONE);
-        mHiddenPreview.setVisibility(View.GONE);
-        v.setVisibility(View.VISIBLE);
-    }
-
-    private void showTextPreview(CharSequence text, TextView textView) {
-        showSinglePreview(textView);
-        final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length()));
-        textView.setText(truncatedText);
-        updateTextSize(truncatedText, textView);
-
-        textView.addOnLayoutChangeListener(
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    if (right - left != oldRight - oldLeft) {
-                        updateTextSize(truncatedText, textView);
-                    }
-                });
-        mEditChip.setVisibility(View.GONE);
-    }
-
-    private void updateTextSize(CharSequence text, TextView textView) {
-        Paint paint = new Paint(textView.getPaint());
-        Resources res = textView.getResources();
-        float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
-        float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
-        if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
-            // If the text is a single word and would fit within the TextView at the min font size,
-            // find the biggest font size that will fit.
-            float fontSizePx = minFontSize;
-            while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
-                    && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
-                fontSizePx += FONT_SEARCH_STEP_PX;
-            }
-            // Need to turn off autosizing, otherwise setTextSize is a no-op.
-            textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
-            // It's possible to hit the max font size and not fill the width, so centering
-            // horizontally looks better in this case.
-            textView.setGravity(Gravity.CENTER);
-            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
-        } else {
-            // Otherwise just stick with autosize.
-            textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
-                    (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
-            textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
-        }
-    }
-
-    private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
-            float fontSizePx) {
-        paint.setTextSize(fontSizePx);
-        float size = paint.measureText(text.toString());
-        float availableWidth = textView.getWidth() - textView.getPaddingLeft()
-                - textView.getPaddingRight();
-        return size < availableWidth;
-    }
-
-    private static boolean isOneWord(CharSequence text) {
-        return text.toString().split("\\s+", 2).length == 1;
-    }
-
-    private void showEditableText(CharSequence text, boolean hidden) {
-        TextView textView = hidden ? mHiddenPreview : mTextPreview;
-        showTextPreview(text, textView);
-        View.OnClickListener listener = v -> editText();
-        setAccessibilityActionToEdit(textView);
-        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
-            mEditChip.setVisibility(View.VISIBLE);
-            mActionContainerBackground.setVisibility(View.VISIBLE);
-            mEditChip.setContentDescription(
-                    mContext.getString(R.string.clipboard_edit_text_description));
-            mEditChip.setOnClickListener(listener);
-        }
-        textView.setOnClickListener(listener);
-    }
-
-    private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
-        View.OnClickListener listener = v -> editImage(uri);
-        ContentResolver resolver = mContext.getContentResolver();
-        String mimeType = resolver.getType(uri);
-        boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
-        if (isSensitive) {
-            mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
-            showSinglePreview(mHiddenPreview);
-            if (isEditableImage) {
-                mHiddenPreview.setOnClickListener(listener);
-                setAccessibilityActionToEdit(mHiddenPreview);
-            }
-        } else if (isEditableImage) { // if the MIMEtype is image, try to load
-            try {
-                int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
-                // The width of the view is capped, height maintains aspect ratio, so allow it to be
-                // taller if needed.
-                Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
-                showSinglePreview(mImagePreview);
-                mImagePreview.setImageBitmap(thumbnail);
-                mImagePreview.setOnClickListener(listener);
-                setAccessibilityActionToEdit(mImagePreview);
-            } catch (IOException e) {
-                Log.e(TAG, "Thumbnail loading failed", e);
-                showTextPreview(
-                        mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
-                        mTextPreview);
-                isEditableImage = false;
-            }
-        } else {
-            showTextPreview(
-                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
-                    mTextPreview);
-        }
-        if (isEditableImage && DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
-            mEditChip.setVisibility(View.VISIBLE);
-            mActionContainerBackground.setVisibility(View.VISIBLE);
-            mEditChip.setOnClickListener(listener);
-            mEditChip.setContentDescription(
-                    mContext.getString(R.string.clipboard_edit_image_description));
-        }
-        return isEditableImage;
-    }
-
-    private void setAccessibilityActionToEdit(View view) {
-        ViewCompat.replaceAccessibilityAction(view,
-                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
-                mContext.getString(R.string.clipboard_edit), null);
-    }
-
-    private void animateIn() {
-        if (mAccessibilityManager.isEnabled()) {
-            mDismissButton.setVisibility(View.VISIBLE);
-        }
-        mEnterAnimator = getEnterAnimation();
-        mEnterAnimator.start();
-    }
-
-    private void animateOut() {
-        if (mExitAnimator != null && mExitAnimator.isRunning()) {
-            return;
-        }
-        Animator anim = getExitAnimation();
-        anim.addListener(new AnimatorListenerAdapter() {
-            private boolean mCancelled;
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                super.onAnimationCancel(animation);
-                mCancelled = true;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-                if (!mCancelled) {
-                    hideImmediate();
-                }
-            }
-        });
-        mExitAnimator = anim;
-        anim.start();
-    }
-
-    private Animator getEnterAnimation() {
-        TimeInterpolator linearInterpolator = new LinearInterpolator();
-        TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
-        AnimatorSet enterAnim = new AnimatorSet();
-
-        ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
-        rootAnim.setInterpolator(linearInterpolator);
-        rootAnim.setDuration(66);
-        rootAnim.addUpdateListener(animation -> {
-            mView.setAlpha(animation.getAnimatedFraction());
-        });
-
-        ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
-        scaleAnim.setInterpolator(scaleInterpolator);
-        scaleAnim.setDuration(333);
-        scaleAnim.addUpdateListener(animation -> {
-            float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
-            mClipboardPreview.setScaleX(previewScale);
-            mClipboardPreview.setScaleY(previewScale);
-            mPreviewBorder.setScaleX(previewScale);
-            mPreviewBorder.setScaleY(previewScale);
-
-            float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
-            mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
-            mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
-            float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
-            float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
-            mActionContainer.setScaleX(actionsScaleX);
-            mActionContainer.setScaleY(actionsScaleY);
-            mActionContainerBackground.setScaleX(actionsScaleX);
-            mActionContainerBackground.setScaleY(actionsScaleY);
-        });
-
-        ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
-        alphaAnim.setInterpolator(linearInterpolator);
-        alphaAnim.setDuration(283);
-        alphaAnim.addUpdateListener(animation -> {
-            float alpha = animation.getAnimatedFraction();
-            mClipboardPreview.setAlpha(alpha);
-            mPreviewBorder.setAlpha(alpha);
-            mDismissButton.setAlpha(alpha);
-            mActionContainer.setAlpha(alpha);
-        });
-
-        mActionContainer.setAlpha(0);
-        mPreviewBorder.setAlpha(0);
-        mClipboardPreview.setAlpha(0);
-        enterAnim.play(rootAnim).with(scaleAnim);
-        enterAnim.play(alphaAnim).after(50).after(rootAnim);
-
-        enterAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-                mView.setAlpha(1);
-                mTimeoutHandler.resetTimeout();
-            }
-        });
-        return enterAnim;
-    }
-
-    private Animator getExitAnimation() {
-        TimeInterpolator linearInterpolator = new LinearInterpolator();
-        TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
-        AnimatorSet exitAnim = new AnimatorSet();
-
-        ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
-        rootAnim.setInterpolator(linearInterpolator);
-        rootAnim.setDuration(100);
-        rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction()));
-
-        ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
-        scaleAnim.setInterpolator(scaleInterpolator);
-        scaleAnim.setDuration(250);
-        scaleAnim.addUpdateListener(animation -> {
-            float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
-            mClipboardPreview.setScaleX(previewScale);
-            mClipboardPreview.setScaleY(previewScale);
-            mPreviewBorder.setScaleX(previewScale);
-            mPreviewBorder.setScaleY(previewScale);
-
-            float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
-            mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
-            mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
-            float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
-            float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
-            mActionContainer.setScaleX(actionScaleX);
-            mActionContainer.setScaleY(actionScaleY);
-            mActionContainerBackground.setScaleX(actionScaleX);
-            mActionContainerBackground.setScaleY(actionScaleY);
-        });
-
-        ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
-        alphaAnim.setInterpolator(linearInterpolator);
-        alphaAnim.setDuration(166);
-        alphaAnim.addUpdateListener(animation -> {
-            float alpha = 1 - animation.getAnimatedFraction();
-            mClipboardPreview.setAlpha(alpha);
-            mPreviewBorder.setAlpha(alpha);
-            mDismissButton.setAlpha(alpha);
-            mActionContainer.setAlpha(alpha);
-        });
-
-        exitAnim.play(alphaAnim).with(scaleAnim);
-        exitAnim.play(rootAnim).after(150).after(alphaAnim);
-        return exitAnim;
-    }
-
-    private void hideImmediate() {
-        // Note this may be called multiple times if multiple dismissal events happen at the same
-        // time.
-        mTimeoutHandler.cancelTimeout();
-        final View decorView = mWindow.peekDecorView();
-        if (decorView != null && decorView.isAttachedToWindow()) {
-            mWindowManager.removeViewImmediate(decorView);
-        }
-        if (mCloseDialogsReceiver != null) {
-            mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
-            mCloseDialogsReceiver = null;
-        }
-        if (mScreenshotReceiver != null) {
-            mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver);
-            mScreenshotReceiver = null;
-        }
-        if (mInputEventReceiver != null) {
-            mInputEventReceiver.dispose();
-            mInputEventReceiver = null;
-        }
-        if (mInputMonitor != null) {
-            mInputMonitor.dispose();
-            mInputMonitor = null;
-        }
-        if (mOnSessionCompleteListener != null) {
-            mOnSessionCompleteListener.run();
-        }
-    }
-
-    private void resetActionChips() {
-        for (OverlayActionChip chip : mActionChips) {
-            mActionContainer.removeView(chip);
-        }
-        mActionChips.clear();
-    }
-
-    private void reset() {
-        mView.setTranslationX(0);
-        mView.setAlpha(0);
-        mActionContainerBackground.setVisibility(View.GONE);
-        mShareChip.setVisibility(View.GONE);
-        mEditChip.setVisibility(View.GONE);
-        mRemoteCopyChip.setVisibility(View.GONE);
-        resetActionChips();
-        mTimeoutHandler.cancelTimeout();
-        mClipboardLogger.reset();
-    }
-
-    @MainThread
-    private void attachWindow() {
-        View decorView = mWindow.getDecorView();
-        if (decorView.isAttachedToWindow() || mBlockAttach) {
-            return;
-        }
-        mBlockAttach = true;
-        mWindowManager.addView(decorView, mWindowLayoutParams);
-        decorView.requestApplyInsets();
-        mView.requestApplyInsets();
-        decorView.getViewTreeObserver().addOnWindowAttachListener(
-                new ViewTreeObserver.OnWindowAttachListener() {
-                    @Override
-                    public void onWindowAttached() {
-                        mBlockAttach = false;
-                    }
-
-                    @Override
-                    public void onWindowDetached() {
-                    }
-                }
-        );
-    }
-
-    private void withWindowAttached(Runnable action) {
-        View decorView = mWindow.getDecorView();
-        if (decorView.isAttachedToWindow()) {
-            action.run();
-        } else {
-            decorView.getViewTreeObserver().addOnWindowAttachListener(
-                    new ViewTreeObserver.OnWindowAttachListener() {
-                        @Override
-                        public void onWindowAttached() {
-                            mBlockAttach = false;
-                            decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
-                            action.run();
-                        }
-
-                        @Override
-                        public void onWindowDetached() {
-                        }
-                    });
-        }
-    }
-
-    private void updateInsets(WindowInsets insets) {
-        int orientation = mContext.getResources().getConfiguration().orientation;
-        FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
-        if (p == null) {
-            return;
-        }
-        DisplayCutout cutout = insets.getDisplayCutout();
-        Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
-        Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
-        if (cutout == null) {
-            p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
-        } else {
-            Insets waterfall = cutout.getWaterfallInsets();
-            if (orientation == ORIENTATION_PORTRAIT) {
-                p.setMargins(
-                        waterfall.left,
-                        Math.max(cutout.getSafeInsetTop(), waterfall.top),
-                        waterfall.right,
-                        Math.max(imeInsets.bottom,
-                                Math.max(cutout.getSafeInsetBottom(),
-                                        Math.max(navBarInsets.bottom, waterfall.bottom))));
-            } else {
-                p.setMargins(
-                        waterfall.left,
-                        waterfall.top,
-                        waterfall.right,
-                        Math.max(imeInsets.bottom,
-                                Math.max(navBarInsets.bottom, waterfall.bottom)));
-            }
-        }
-        mView.setLayoutParams(p);
-        mView.requestLayout();
-    }
-
-    private Display getDefaultDisplay() {
-        return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
-    }
-
-    /**
-     * Updates the window focusability.  If the window is already showing, then it updates the
-     * window immediately, otherwise the layout params will be applied when the window is next
-     * shown.
-     */
-    private void setWindowFocusable(boolean focusable) {
-        int flags = mWindowLayoutParams.flags;
-        if (focusable) {
-            mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-        } else {
-            mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-        }
-        if (mWindowLayoutParams.flags == flags) {
-            return;
-        }
-        final View decorView = mWindow.peekDecorView();
-        if (decorView != null && decorView.isAttachedToWindow()) {
-            mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
-        }
-    }
-
-    static class ClipboardLogger {
-        private final UiEventLogger mUiEventLogger;
-        private boolean mGuarded = false;
-
-        ClipboardLogger(UiEventLogger uiEventLogger) {
-            mUiEventLogger = uiEventLogger;
-        }
-
-        void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) {
-            if (!mGuarded) {
-                mGuarded = true;
-                mUiEventLogger.log(event);
-            }
-        }
-
-        void reset() {
-            mGuarded = false;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
deleted file mode 100644
index 0d989a7..0000000
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.clipboardoverlay;
-
-import android.content.Context;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.screenshot.TimeoutHandler;
-
-import javax.inject.Inject;
-
-/**
- * A factory that churns out ClipboardOverlayControllerLegacys on demand.
- */
-@SysUISingleton
-public class ClipboardOverlayControllerLegacyFactory {
-
-    private final UiEventLogger mUiEventLogger;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final BroadcastSender mBroadcastSender;
-
-    @Inject
-    public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher,
-            BroadcastSender broadcastSender, UiEventLogger uiEventLogger) {
-        this.mBroadcastDispatcher = broadcastDispatcher;
-        this.mBroadcastSender = broadcastSender;
-        this.mUiEventLogger = uiEventLogger;
-    }
-
-    /**
-     * One new ClipboardOverlayControllerLegacy, coming right up!
-     */
-    public ClipboardOverlayControllerLegacy create(Context context) {
-        return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender,
-                new TimeoutHandler(context), mUiEventLogger);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java
index 785e4a0..a85f8b9 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java
@@ -65,6 +65,23 @@
         return false;
     }
 
+    public Optional<RemoteAction> getAction(CharSequence text, TextLinks textLinks, String source) {
+        return getActions(text, textLinks).stream().filter(remoteAction -> {
+            ComponentName component = remoteAction.getActionIntent().getIntent().getComponent();
+            return component != null && !TextUtils.equals(source, component.getPackageName());
+        }).findFirst();
+    }
+
+    private ArrayList<RemoteAction> getActions(CharSequence text, TextLinks textLinks) {
+        ArrayList<RemoteAction> actions = new ArrayList<>();
+        for (TextLinks.TextLink link : textLinks.getLinks()) {
+            TextClassification classification = mTextClassifier.classifyText(
+                    text, link.getStart(), link.getEnd(), null);
+            actions.addAll(classification.getActions());
+        }
+        return actions;
+    }
+
     public Optional<RemoteAction> getAction(ClipData.Item item, String source) {
         return getActions(item).stream().filter(remoteAction -> {
             ComponentName component = remoteAction.getActionIntent().getIntent().getComponent();
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index 2d33157..f372bb4 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -18,8 +18,6 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static java.util.Objects.requireNonNull;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -72,11 +70,11 @@
 
         void onRemoteCopyButtonTapped();
 
-        void onEditButtonTapped();
-
         void onShareButtonTapped();
 
         void onPreviewTapped();
+
+        void onMinimizedViewTapped();
     }
 
     private static final String TAG = "ClipboardView";
@@ -92,8 +90,8 @@
     private ImageView mImagePreview;
     private TextView mTextPreview;
     private TextView mHiddenPreview;
+    private LinearLayout mMinimizedPreview;
     private View mPreviewBorder;
-    private OverlayActionChip mEditChip;
     private OverlayActionChip mShareChip;
     private OverlayActionChip mRemoteCopyChip;
     private View mActionContainerBackground;
@@ -117,26 +115,22 @@
 
     @Override
     protected void onFinishInflate() {
-        mActionContainerBackground =
-                requireNonNull(findViewById(R.id.actions_container_background));
-        mActionContainer = requireNonNull(findViewById(R.id.actions));
-        mClipboardPreview = requireNonNull(findViewById(R.id.clipboard_preview));
-        mImagePreview = requireNonNull(findViewById(R.id.image_preview));
-        mTextPreview = requireNonNull(findViewById(R.id.text_preview));
-        mHiddenPreview = requireNonNull(findViewById(R.id.hidden_preview));
-        mPreviewBorder = requireNonNull(findViewById(R.id.preview_border));
-        mEditChip = requireNonNull(findViewById(R.id.edit_chip));
-        mShareChip = requireNonNull(findViewById(R.id.share_chip));
-        mRemoteCopyChip = requireNonNull(findViewById(R.id.remote_copy_chip));
-        mDismissButton = requireNonNull(findViewById(R.id.dismiss_button));
+        mActionContainerBackground = requireViewById(R.id.actions_container_background);
+        mActionContainer = requireViewById(R.id.actions);
+        mClipboardPreview = requireViewById(R.id.clipboard_preview);
+        mPreviewBorder = requireViewById(R.id.preview_border);
+        mImagePreview = requireViewById(R.id.image_preview);
+        mTextPreview = requireViewById(R.id.text_preview);
+        mHiddenPreview = requireViewById(R.id.hidden_preview);
+        mMinimizedPreview = requireViewById(R.id.minimized_preview);
+        mShareChip = requireViewById(R.id.share_chip);
+        mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
+        mDismissButton = requireViewById(R.id.dismiss_button);
 
-        mEditChip.setAlpha(1);
         mShareChip.setAlpha(1);
         mRemoteCopyChip.setAlpha(1);
         mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
 
-        mEditChip.setIcon(
-                Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
         mRemoteCopyChip.setIcon(
                 Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
         mShareChip.setIcon(
@@ -158,11 +152,11 @@
     public void setCallbacks(SwipeDismissCallbacks callbacks) {
         super.setCallbacks(callbacks);
         ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
-        mEditChip.setOnClickListener(v -> clipboardCallbacks.onEditButtonTapped());
         mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
         mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
         mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
         mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
+        mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
     }
 
     void setEditAccessibilityAction(boolean editable) {
@@ -177,12 +171,28 @@
         }
     }
 
+    void setMinimized(boolean minimized) {
+        if (minimized) {
+            mMinimizedPreview.setVisibility(View.VISIBLE);
+            mClipboardPreview.setVisibility(View.GONE);
+            mPreviewBorder.setVisibility(View.GONE);
+            mActionContainer.setVisibility(View.GONE);
+            mActionContainerBackground.setVisibility(View.GONE);
+        } else {
+            mMinimizedPreview.setVisibility(View.GONE);
+            mClipboardPreview.setVisibility(View.VISIBLE);
+            mPreviewBorder.setVisibility(View.VISIBLE);
+            mActionContainer.setVisibility(View.VISIBLE);
+        }
+    }
+
     void setInsets(WindowInsets insets, int orientation) {
         FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams();
         if (p == null) {
             return;
         }
         Rect margins = computeMargins(insets, orientation);
+
         p.setMargins(margins.left, margins.top, margins.right, margins.bottom);
         setLayoutParams(p);
         requestLayout();
@@ -204,6 +214,12 @@
                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
         touchRegion.op(tmpRect, Region.Op.UNION);
 
+        mMinimizedPreview.getBoundsOnScreen(tmpRect);
+        tmpRect.inset(
+                (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+                (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
+        touchRegion.op(tmpRect, Region.Op.UNION);
+
         mDismissButton.getBoundsOnScreen(tmpRect);
         touchRegion.op(tmpRect, Region.Op.UNION);
 
@@ -235,7 +251,6 @@
                         updateTextSize(text, textView);
                     }
                 });
-        mEditChip.setVisibility(View.GONE);
     }
 
     void showImagePreview(@Nullable Bitmap thumbnail) {
@@ -248,12 +263,6 @@
         }
     }
 
-    void showEditChip(String contentDescription) {
-        mEditChip.setVisibility(View.VISIBLE);
-        mActionContainerBackground.setVisibility(View.VISIBLE);
-        mEditChip.setContentDescription(contentDescription);
-    }
-
     void showShareChip() {
         mShareChip.setVisibility(View.VISIBLE);
         mActionContainerBackground.setVisibility(View.VISIBLE);
@@ -265,7 +274,6 @@
         mActionContainerBackground.setVisibility(View.GONE);
         mDismissButton.setVisibility(View.GONE);
         mShareChip.setVisibility(View.GONE);
-        mEditChip.setVisibility(View.GONE);
         mRemoteCopyChip.setVisibility(View.GONE);
         setEditAccessibilityAction(false);
         resetActionChips();
@@ -298,6 +306,8 @@
         scaleAnim.setDuration(333);
         scaleAnim.addUpdateListener(animation -> {
             float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+            mMinimizedPreview.setScaleX(previewScale);
+            mMinimizedPreview.setScaleY(previewScale);
             mClipboardPreview.setScaleX(previewScale);
             mClipboardPreview.setScaleY(previewScale);
             mPreviewBorder.setScaleX(previewScale);
@@ -319,12 +329,14 @@
         alphaAnim.setDuration(283);
         alphaAnim.addUpdateListener(animation -> {
             float alpha = animation.getAnimatedFraction();
+            mMinimizedPreview.setAlpha(alpha);
             mClipboardPreview.setAlpha(alpha);
             mPreviewBorder.setAlpha(alpha);
             mDismissButton.setAlpha(alpha);
             mActionContainer.setAlpha(alpha);
         });
 
+        mMinimizedPreview.setAlpha(0);
         mActionContainer.setAlpha(0);
         mPreviewBorder.setAlpha(0);
         mClipboardPreview.setAlpha(0);
@@ -356,6 +368,8 @@
         scaleAnim.setDuration(250);
         scaleAnim.addUpdateListener(animation -> {
             float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+            mMinimizedPreview.setScaleX(previewScale);
+            mMinimizedPreview.setScaleY(previewScale);
             mClipboardPreview.setScaleX(previewScale);
             mClipboardPreview.setScaleY(previewScale);
             mPreviewBorder.setScaleX(previewScale);
@@ -377,6 +391,7 @@
         alphaAnim.setDuration(166);
         alphaAnim.addUpdateListener(animation -> {
             float alpha = 1 - animation.getAnimatedFraction();
+            mMinimizedPreview.setAlpha(alpha);
             mClipboardPreview.setAlpha(alpha);
             mPreviewBorder.setAlpha(alpha);
             mDismissButton.setAlpha(alpha);
@@ -399,6 +414,7 @@
         mTextPreview.setVisibility(View.GONE);
         mImagePreview.setVisibility(View.GONE);
         mHiddenPreview.setVisibility(View.GONE);
+        mMinimizedPreview.setVisibility(View.GONE);
         v.setVisibility(View.VISIBLE);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
index 2244813..09b2e44 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.clipboardoverlay.dagger;
 
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -28,6 +27,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.clipboardoverlay.ClipboardOverlayView;
+import com.android.systemui.settings.DisplayTracker;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -46,8 +46,9 @@
      */
     @Provides
     @OverlayWindowContext
-    static Context provideWindowContext(DisplayManager displayManager, Context context) {
-        Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
+    static Context provideWindowContext(DisplayManager displayManager,
+            DisplayTracker displayTracker, Context context) {
+        Display display = displayManager.getDisplay(displayTracker.getDefaultDisplayId());
         return context.createWindowContext(display, TYPE_SCREENSHOT, null);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt
new file mode 100644
index 0000000..b973667
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.common.coroutine
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Calls the specified function [block] and returns its encapsulated result if invocation was
+ * successful, catching any [Throwable] exception that was thrown from the block function execution
+ * and encapsulating it as a failure.
+ *
+ * Unlike [runCatching], [suspendRunCatching] does not break structured concurrency by rethrowing
+ * any [CancellationException].
+ *
+ * **Heads-up:** [TimeoutCancellationException] extends [CancellationException] but catching it does
+ * not breaks structured concurrency and therefore, will not be rethrown. Therefore, you can use
+ * [suspendRunCatching] with [withTimeout], and handle any timeout gracefully.
+ *
+ * @see <a href="https://github.com/Kotlin/kotlinx.coroutines/issues/1814">link</a>
+ */
+suspend inline fun <T> suspendRunCatching(crossinline block: suspend () -> T): Result<T> =
+    try {
+        Result.success(block())
+    } catch (e: Throwable) {
+        // Ensures the try-catch block will not break structured concurrency.
+        currentCoroutineContext().ensureActive()
+        Result.failure(e)
+    }
+
+/**
+ * Calls the specified function [block] and returns its encapsulated result if invocation was
+ * successful, catching any [Throwable] exception that was thrown from the block function execution
+ * and encapsulating it as a failure.
+ *
+ * Unlike [runCatching], [suspendRunCatching] does not break structured concurrency by rethrowing
+ * any [CancellationException].
+ *
+ * **Heads-up:** [TimeoutCancellationException] extends [CancellationException] but catching it does
+ * not breaks structured concurrency and therefore, will not be rethrown. Therefore, you can use
+ * [suspendRunCatching] with [withTimeout], and handle any timeout gracefully.
+ *
+ * @see <a href="https://github.com/Kotlin/kotlinx.coroutines/issues/1814">link</a>
+ */
+suspend inline fun <T, R> T.suspendRunCatching(crossinline block: suspend T.() -> R): Result<R> =
+    // Overload with a `this` receiver, matches with `kotlin.runCatching` functions.
+    // Qualified name needs to be used to avoid a recursive call.
+    com.android.systemui.common.coroutine.suspendRunCatching { block(this) }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableConstraintLayout.kt
similarity index 76%
copy from packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
copy to packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableConstraintLayout.kt
index 7bbfec7..9763665 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableConstraintLayout.kt
@@ -12,37 +12,37 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
 package com.android.systemui.common.ui.view
 
 import android.content.Context
 import android.util.AttributeSet
-import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
 
-class LaunchableImageView : ImageView, LaunchableView {
+/** A [ConstraintLayout] that also implements [LaunchableView]. */
+open class LaunchableConstraintLayout : ConstraintLayout, LaunchableView {
     private val delegate =
         LaunchableViewDelegate(
             this,
             superSetVisibility = { super.setVisibility(it) },
         )
 
-    constructor(context: Context?) : super(context)
-    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+    constructor(context: Context) : super(context)
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
     constructor(
-        context: Context?,
+        context: Context,
         attrs: AttributeSet?,
-        defStyleAttr: Int,
+        defStyleAttr: Int
     ) : super(context, attrs, defStyleAttr)
 
     constructor(
-        context: Context?,
+        context: Context,
         attrs: AttributeSet?,
         defStyleAttr: Int,
-        defStyleRes: Int,
+        defStyleRes: Int
     ) : super(context, attrs, defStyleAttr, defStyleRes)
 
     override fun setShouldBlockVisibilityChanges(block: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
new file mode 100644
index 0000000..2dd98dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.common.ui.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlinx.coroutines.DisposableHandle
+
+/**
+ * View designed to handle long-presses.
+ *
+ * The view will not handle any long pressed by default. To set it up, set up a listener and, when
+ * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
+ */
+class LongPressHandlingView(
+    context: Context,
+    attrs: AttributeSet?,
+) :
+    View(
+        context,
+        attrs,
+    ) {
+    interface Listener {
+        /** Notifies that a long-press has been detected by the given view. */
+        fun onLongPressDetected(
+            view: View,
+            x: Int,
+            y: Int,
+        )
+
+        /** Notifies that the gesture was too short for a long press, it is actually a click. */
+        fun onSingleTapDetected(view: View) = Unit
+    }
+
+    var listener: Listener? = null
+
+    private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
+        LongPressHandlingViewInteractionHandler(
+            postDelayed = { block, timeoutMs ->
+                val dispatchToken = Any()
+
+                handler.postDelayed(
+                    block,
+                    dispatchToken,
+                    timeoutMs,
+                )
+
+                DisposableHandle { handler.removeCallbacksAndMessages(dispatchToken) }
+            },
+            isAttachedToWindow = ::isAttachedToWindow,
+            onLongPressDetected = { x, y ->
+                listener?.onLongPressDetected(
+                    view = this,
+                    x = x,
+                    y = y,
+                )
+            },
+            onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) },
+        )
+    }
+
+    fun setLongPressHandlingEnabled(isEnabled: Boolean) {
+        interactionHandler.isLongPressHandlingEnabled = isEnabled
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        return interactionHandler.onTouchEvent(event?.toModel())
+    }
+}
+
+private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
+    return when (actionMasked) {
+        MotionEvent.ACTION_DOWN ->
+            LongPressHandlingViewInteractionHandler.MotionEventModel.Down(
+                x = x.toInt(),
+                y = y.toInt(),
+            )
+        MotionEvent.ACTION_MOVE ->
+            LongPressHandlingViewInteractionHandler.MotionEventModel.Move(
+                distanceMoved = distanceMoved(),
+            )
+        MotionEvent.ACTION_UP ->
+            LongPressHandlingViewInteractionHandler.MotionEventModel.Up(
+                distanceMoved = distanceMoved(),
+                gestureDuration = gestureDuration(),
+            )
+        MotionEvent.ACTION_CANCEL -> LongPressHandlingViewInteractionHandler.MotionEventModel.Cancel
+        else -> LongPressHandlingViewInteractionHandler.MotionEventModel.Other
+    }
+}
+
+private fun MotionEvent.distanceMoved(): Float {
+    return if (historySize > 0) {
+        sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
+    } else {
+        0f
+    }
+}
+
+private fun MotionEvent.gestureDuration(): Long {
+    return eventTime - downTime
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
new file mode 100644
index 0000000..c2d4d12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.common.ui.view
+
+import android.view.ViewConfiguration
+import kotlinx.coroutines.DisposableHandle
+
+/** Encapsulates logic to handle complex touch interactions with a [LongPressHandlingView]. */
+class LongPressHandlingViewInteractionHandler(
+    /**
+     * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
+     * allowing the delayed runnable to be canceled before it is run.
+     */
+    private val postDelayed: (block: Runnable, delayMs: Long) -> DisposableHandle,
+    /** Callback to be queried to check if the view is attached to its window. */
+    private val isAttachedToWindow: () -> Boolean,
+    /** Callback reporting the a long-press gesture was detected at the given coordinates. */
+    private val onLongPressDetected: (x: Int, y: Int) -> Unit,
+    /** Callback reporting the a single tap gesture was detected at the given coordinates. */
+    private val onSingleTapDetected: () -> Unit,
+) {
+    sealed class MotionEventModel {
+        object Other : MotionEventModel()
+
+        data class Down(
+            val x: Int,
+            val y: Int,
+        ) : MotionEventModel()
+
+        data class Move(
+            val distanceMoved: Float,
+        ) : MotionEventModel()
+
+        data class Up(
+            val distanceMoved: Float,
+            val gestureDuration: Long,
+        ) : MotionEventModel()
+
+        object Cancel : MotionEventModel()
+    }
+
+    var isLongPressHandlingEnabled: Boolean = false
+    var scheduledLongPressHandle: DisposableHandle? = null
+
+    fun onTouchEvent(event: MotionEventModel?): Boolean {
+        if (!isLongPressHandlingEnabled) {
+            return false
+        }
+
+        return when (event) {
+            is MotionEventModel.Down -> {
+                scheduleLongPress(event.x, event.y)
+                true
+            }
+            is MotionEventModel.Move -> {
+                if (event.distanceMoved > ViewConfiguration.getTouchSlop()) {
+                    cancelScheduledLongPress()
+                }
+                false
+            }
+            is MotionEventModel.Up -> {
+                cancelScheduledLongPress()
+                if (
+                    event.distanceMoved <= ViewConfiguration.getTouchSlop() &&
+                        event.gestureDuration < ViewConfiguration.getLongPressTimeout()
+                ) {
+                    dispatchSingleTap()
+                }
+                false
+            }
+            is MotionEventModel.Cancel -> {
+                cancelScheduledLongPress()
+                false
+            }
+            else -> false
+        }
+    }
+
+    private fun scheduleLongPress(
+        x: Int,
+        y: Int,
+    ) {
+        scheduledLongPressHandle =
+            postDelayed(
+                {
+                    dispatchLongPress(
+                        x = x,
+                        y = y,
+                    )
+                },
+                ViewConfiguration.getLongPressTimeout().toLong(),
+            )
+    }
+
+    private fun dispatchLongPress(
+        x: Int,
+        y: Int,
+    ) {
+        if (!isAttachedToWindow()) {
+            return
+        }
+
+        onLongPressDetected(x, y)
+    }
+
+    private fun cancelScheduledLongPress() {
+        scheduledLongPressHandle?.dispose()
+    }
+
+    private fun dispatchSingleTap() {
+        if (!isAttachedToWindow()) {
+            return
+        }
+
+        onSingleTapDetected()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
new file mode 100644
index 0000000..1833202
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
@@ -0,0 +1,202 @@
+/*
+ * 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.common.ui.view;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+import com.android.systemui.R;
+
+/**
+ * The layout contains a seekbar whose progress could be modified
+ * through the icons on two ends of the seekbar.
+ */
+public class SeekBarWithIconButtonsView extends LinearLayout {
+
+    private static final int DEFAULT_SEEKBAR_MAX = 6;
+    private static final int DEFAULT_SEEKBAR_PROGRESS = 0;
+
+    private ViewGroup mIconStartFrame;
+    private ViewGroup mIconEndFrame;
+    private ImageView mIconStart;
+    private ImageView mIconEnd;
+    private SeekBar mSeekbar;
+
+    private SeekBarChangeListener mSeekBarListener = new SeekBarChangeListener();
+
+    public SeekBarWithIconButtonsView(Context context) {
+        this(context, null);
+    }
+
+    public SeekBarWithIconButtonsView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SeekBarWithIconButtonsView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public SeekBarWithIconButtonsView(Context context,
+            AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        LayoutInflater.from(context).inflate(
+                R.layout.seekbar_with_icon_buttons, this, /* attachToRoot= */ true);
+
+        mIconStartFrame = findViewById(R.id.icon_start_frame);
+        mIconEndFrame = findViewById(R.id.icon_end_frame);
+        mIconStart = findViewById(R.id.icon_start);
+        mIconEnd = findViewById(R.id.icon_end);
+        mSeekbar = findViewById(R.id.seekbar);
+
+        if (attrs != null) {
+            TypedArray typedArray = context.obtainStyledAttributes(
+                    attrs,
+                    R.styleable.SeekBarWithIconButtonsView_Layout,
+                    defStyleAttr, defStyleRes
+            );
+            int max = typedArray.getInt(
+                    R.styleable.SeekBarWithIconButtonsView_Layout_max, DEFAULT_SEEKBAR_MAX);
+            int progress = typedArray.getInt(
+                    R.styleable.SeekBarWithIconButtonsView_Layout_progress,
+                    DEFAULT_SEEKBAR_PROGRESS);
+            mSeekbar.setMax(max);
+            setProgress(progress);
+
+            int iconStartFrameContentDescriptionId = typedArray.getResourceId(
+                    R.styleable.SeekBarWithIconButtonsView_Layout_iconStartContentDescription,
+                    /* defValue= */ 0);
+            int iconEndFrameContentDescriptionId = typedArray.getResourceId(
+                    R.styleable.SeekBarWithIconButtonsView_Layout_iconEndContentDescription,
+                    /* defValue= */ 0);
+            if (iconStartFrameContentDescriptionId != 0) {
+                final String contentDescription =
+                        context.getString(iconStartFrameContentDescriptionId);
+                mIconStartFrame.setContentDescription(contentDescription);
+            }
+            if (iconEndFrameContentDescriptionId != 0) {
+                final String contentDescription =
+                        context.getString(iconEndFrameContentDescriptionId);
+                mIconEndFrame.setContentDescription(contentDescription);
+            }
+
+            typedArray.recycle();
+        } else {
+            mSeekbar.setMax(DEFAULT_SEEKBAR_MAX);
+            setProgress(DEFAULT_SEEKBAR_PROGRESS);
+        }
+
+        mSeekbar.setOnSeekBarChangeListener(mSeekBarListener);
+
+        mIconStartFrame.setOnClickListener((view) -> {
+            final int progress = mSeekbar.getProgress();
+            if (progress > 0) {
+                mSeekbar.setProgress(progress - 1);
+                setIconViewAndFrameEnabled(mIconStart, mSeekbar.getProgress() > 0);
+            }
+        });
+
+        mIconEndFrame.setOnClickListener((view) -> {
+            final int progress = mSeekbar.getProgress();
+            if (progress < mSeekbar.getMax()) {
+                mSeekbar.setProgress(progress + 1);
+                setIconViewAndFrameEnabled(mIconEnd, mSeekbar.getProgress() < mSeekbar.getMax());
+            }
+        });
+    }
+
+    private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) {
+        iconView.setEnabled(enabled);
+        final ViewGroup iconFrame = (ViewGroup) iconView.getParent();
+        iconFrame.setEnabled(enabled);
+    }
+
+    /**
+     * Sets a onSeekbarChangeListener to the seekbar in the layout.
+     * We update the Start Icon and End Icon if needed when the seekbar progress is changed.
+     */
+    public void setOnSeekBarChangeListener(
+            @Nullable SeekBar.OnSeekBarChangeListener onSeekBarChangeListener) {
+        mSeekBarListener.setOnSeekBarChangeListener(onSeekBarChangeListener);
+    }
+
+    /**
+     * Start and End icons might need to be updated when there is a change in seekbar progress.
+     * Icon Start will need to be enabled when the seekbar progress is larger than 0.
+     * Icon End will need to be enabled when the seekbar progress is less than Max.
+     */
+    private void updateIconViewIfNeeded(int progress) {
+        setIconViewAndFrameEnabled(mIconStart, progress > 0);
+        setIconViewAndFrameEnabled(mIconEnd, progress < mSeekbar.getMax());
+    }
+
+    /**
+     * Sets max to the seekbar in the layout.
+     */
+    public void setMax(int max) {
+        mSeekbar.setMax(max);
+    }
+
+    /**
+     * Sets progress to the seekbar in the layout.
+     * If the progress is smaller than or equals to 0, the IconStart will be disabled. If the
+     * progress is larger than or equals to Max, the IconEnd will be disabled. The seekbar progress
+     * will be constrained in {@link SeekBar}.
+     */
+    public void setProgress(int progress) {
+        mSeekbar.setProgress(progress);
+        updateIconViewIfNeeded(progress);
+    }
+
+    private class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
+        private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = null;
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            if (mOnSeekBarChangeListener != null) {
+                mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
+            }
+            updateIconViewIfNeeded(progress);
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            if (mOnSeekBarChangeListener != null) {
+                mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
+            }
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            if (mOnSeekBarChangeListener != null) {
+                mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
+            }
+        }
+
+        void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener listener) {
+            mOnSeekBarChangeListener = listener;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index e5ec727..c0f8549 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -17,8 +17,12 @@
 
 package com.android.systemui.compose
 
+import android.content.Context
+import android.view.View
 import androidx.activity.ComponentActivity
+import androidx.lifecycle.LifecycleOwner
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 
 /**
  * A facade to interact with Compose, when it is available.
@@ -35,10 +39,22 @@
      */
     fun isComposeAvailable(): Boolean
 
+    /**
+     * Return the [ComposeInitializer] to make Compose usable in windows outside normal activities.
+     */
+    fun composeInitializer(): ComposeInitializer
+
     /** Bind the content of [activity] to [viewModel]. */
     fun setPeopleSpaceActivityContent(
         activity: ComponentActivity,
         viewModel: PeopleViewModel,
         onResult: (PeopleViewModel.Result) -> Unit,
     )
+
+    /** Create a [View] to represent [viewModel] on screen. */
+    fun createFooterActionsView(
+        context: Context,
+        viewModel: FooterActionsViewModel,
+        qsVisibilityLifecycleOwner: LifecycleOwner,
+    ): View
 }
diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt
new file mode 100644
index 0000000..90dc3a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.compose
+
+import android.view.View
+
+/**
+ * An initializer to use Compose outside of an Activity, e.g. inside a window added directly using
+ * [android.view.WindowManager.addView] (like the shade or status bar) or inside a dialog.
+ *
+ * Example:
+ * ```
+ *    windowManager.addView(MyWindowRootView(context), /* layoutParams */)
+ *
+ *    class MyWindowRootView(context: Context) : FrameLayout(context) {
+ *        override fun onAttachedToWindow() {
+ *            super.onAttachedToWindow()
+ *            ComposeInitializer.onAttachedToWindow(this)
+ *        }
+ *
+ *        override fun onDetachedFromWindow() {
+ *            super.onDetachedFromWindow()
+ *            ComposeInitializer.onDetachedFromWindow(this)
+ *        }
+ *    }
+ * ```
+ */
+interface ComposeInitializer {
+    /** Function to be called on your window root view's [View.onAttachedToWindow] function. */
+    fun onAttachedToWindow(root: View)
+
+    /** Function to be called on your window root view's [View.onDetachedFromWindow] function. */
+    fun onDetachedFromWindow(root: View)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index dbe301d..860149d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -28,12 +28,13 @@
 import android.content.pm.ServiceInfo
 import android.os.UserHandle
 import android.service.controls.ControlsProviderService
+import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
 import com.android.settingslib.applications.DefaultAppInfo
 import com.android.systemui.R
 import java.util.Objects
 
-class ControlsServiceInfo(
+open class ControlsServiceInfo(
     private val context: Context,
     val serviceInfo: ServiceInfo
 ) : DefaultAppInfo(
@@ -64,7 +65,7 @@
      * [R.array.config_controlsPreferredPackages] can declare activities for use as a panel.
      */
     var panelActivity: ComponentName? = null
-        private set
+        protected set
 
     private var resolved: Boolean = false
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt
index 0a6335e..b3c18fb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt
@@ -21,8 +21,10 @@
 import android.app.job.JobService
 import android.content.ComponentName
 import android.content.Context
+import android.os.PersistableBundle
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.backup.BackupHelper
+import com.android.systemui.settings.UserFileManagerImpl
 import java.io.File
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
@@ -33,14 +35,14 @@
  * This file is a copy of the `controls_favorites.xml` file restored from a back up. It is used to
  * keep track of controls that were restored but its corresponding app has not been installed yet.
  */
-class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
-    wrapper: ControlsFavoritePersistenceWrapper
-) {
+class AuxiliaryPersistenceWrapper
+@VisibleForTesting
+internal constructor(wrapper: ControlsFavoritePersistenceWrapper) {
 
     constructor(
         file: File,
         executor: Executor
-    ): this(ControlsFavoritePersistenceWrapper(file, executor))
+    ) : this(ControlsFavoritePersistenceWrapper(file, executor))
 
     companion object {
         const val AUXILIARY_FILE_NAME = "aux_controls_favorites.xml"
@@ -48,9 +50,7 @@
 
     private var persistenceWrapper: ControlsFavoritePersistenceWrapper = wrapper
 
-    /**
-     * Access the current list of favorites as tracked by the auxiliary file
-     */
+    /** Access the current list of favorites as tracked by the auxiliary file */
     var favorites: List<StructureInfo> = emptyList()
         private set
 
@@ -73,18 +73,19 @@
      * exist, it will be initialized to an empty list.
      */
     fun initialize() {
-        favorites = if (persistenceWrapper.fileExists) {
-            persistenceWrapper.readFavorites()
-        } else {
-            emptyList()
-        }
+        favorites =
+            if (persistenceWrapper.fileExists) {
+                persistenceWrapper.readFavorites()
+            } else {
+                emptyList()
+            }
     }
 
     /**
      * Gets the list of favorite controls as persisted in the auxiliary file for a given component.
      *
-     * When the favorites for that application are returned, they will be removed from the
-     * auxiliary file immediately, so they won't be retrieved again.
+     * When the favorites for that application are returned, they will be removed from the auxiliary
+     * file immediately, so they won't be retrieved again.
      * @param componentName the name of the service that provided the controls
      * @return a list of structures with favorites
      */
@@ -103,20 +104,20 @@
         }
     }
 
-    /**
-     * [JobService] to delete the auxiliary file after a week.
-     */
+    /** [JobService] to delete the auxiliary file after a week. */
     class DeletionJobService : JobService() {
         companion object {
-            @VisibleForTesting
-            internal val DELETE_FILE_JOB_ID = 1000
+            @VisibleForTesting internal val DELETE_FILE_JOB_ID = 1000
+            @VisibleForTesting internal val USER = "USER"
             private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
-            fun getJobForContext(context: Context): JobInfo {
+            fun getJobForContext(context: Context, targetUserId: Int): JobInfo {
                 val jobId = DELETE_FILE_JOB_ID + context.userId
                 val componentName = ComponentName(context, DeletionJobService::class.java)
+                val bundle = PersistableBundle().also { it.putInt(USER, targetUserId) }
                 return JobInfo.Builder(jobId, componentName)
                     .setMinimumLatency(WEEK_IN_MILLIS)
                     .setPersisted(true)
+                    .setExtras(bundle)
                     .build()
             }
         }
@@ -127,8 +128,14 @@
         }
 
         override fun onStartJob(params: JobParameters): Boolean {
+            val userId = params.getExtras()?.getInt(USER, 0) ?: 0
             synchronized(BackupHelper.controlsDataLock) {
-                baseContext.deleteFile(AUXILIARY_FILE_NAME)
+                val file =
+                    UserFileManagerImpl.createFile(
+                        userId = userId,
+                        fileName = AUXILIARY_FILE_NAME,
+                    )
+                baseContext.deleteFile(file.getPath())
             }
             return false
         }
@@ -137,4 +144,4 @@
             return true // reschedule and try again if the job was stopped without completing
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
index eed5531..9b2a224 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
@@ -51,13 +51,22 @@
     fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback)
 
     /**
-     * Request to bind to the given service.
+     * Request to bind to the given service. This should only be used for services using the full
+     * [ControlsProviderService] API, where SystemUI renders the devices' UI.
      *
      * @param component The [ComponentName] of the service to bind
      */
     fun bindService(component: ComponentName)
 
     /**
+     * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+     * the app to remain "warm", and reduce latency.
+     *
+     * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+     */
+    fun bindServiceForPanel(component: ComponentName)
+
+    /**
      * Send a subscribe message to retrieve status of a set of controls.
      *
      * @param structureInfo structure containing the controls to update
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 2f0fd99..3d6d335 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -170,6 +170,10 @@
         retrieveLifecycleManager(component).bindService()
     }
 
+    override fun bindServiceForPanel(component: ComponentName) {
+        retrieveLifecycleManager(component).bindServiceForPanel()
+    }
+
     override fun changeUser(newUser: UserHandle) {
         if (newUser == currentUser) return
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index 2f49c3f..3555d0a 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -166,6 +166,19 @@
     )
 
     /**
+     * Removes favorites for a given component
+     * @param componentName the name of the service that provides the [Control]
+     * @return true when favorites is scheduled for deletion
+     */
+    fun removeFavorites(componentName: ComponentName): Boolean
+
+    /**
+     * Checks if the favorites can be removed. You can't remove components from the preferred list.
+     * @param componentName the name of the service that provides the [Control]
+     */
+    fun canRemoveFavorites(componentName: ComponentName): Boolean
+
+    /**
      * Replaces the favorites for the given structure.
      *
      * Calling this method will eliminate the previous selection of favorites and replace it with a
@@ -188,6 +201,16 @@
     /** See [ControlsUiController.getPreferredSelectedItem]. */
     fun getPreferredSelection(): SelectedItem
 
+    fun setPreferredSelection(selectedItem: SelectedItem)
+
+    /**
+     * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+     * the app to remain "warm", and reduce latency.
+     *
+     * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+     */
+    fun bindComponentForPanel(componentName: ComponentName)
+
     /**
      * Interface for structure to pass data to [ControlsFavoritingActivity].
      */
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 80c5f66..8547903 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.controls.ControlStatus
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.controls.ui.SelectedItem
 import com.android.systemui.dagger.SysUISingleton
@@ -61,6 +62,7 @@
     private val listingController: ControlsListingController,
     private val userFileManager: UserFileManager,
     private val userTracker: UserTracker,
+    private val authorizedPanelsRepository: AuthorizedPanelsRepository,
     optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
     dumpManager: DumpManager,
 ) : Dumpable, ControlsController {
@@ -119,16 +121,13 @@
         userChanging = false
     }
 
-    private val userTrackerCallback = object : UserTracker.Callback {
-        override fun onUserChanged(newUser: Int, userContext: Context) {
-            userChanging = true
-            val newUserHandle = UserHandle.of(newUser)
-            if (currentUser == newUserHandle) {
-                userChanging = false
-                return
-            }
-            setValuesForUser(newUserHandle)
+    override fun changeUser(newUser: UserHandle) {
+        userChanging = true
+        if (currentUser == newUser) {
+            userChanging = false
+            return
         }
+        setValuesForUser(newUser)
     }
 
     @VisibleForTesting
@@ -229,7 +228,6 @@
         dumpManager.registerDumpable(javaClass.name, this)
         resetFavorites()
         userChanging = false
-        userTracker.addCallback(userTrackerCallback, executor)
         context.registerReceiver(
             restoreFinishedReceiver,
             IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
@@ -241,7 +239,6 @@
     }
 
     fun destroy() {
-        userTracker.removeCallback(userTrackerCallback)
         context.unregisterReceiver(restoreFinishedReceiver)
         listingController.removeCallback(listingCallback)
     }
@@ -249,6 +246,11 @@
     private fun resetFavorites() {
         Favorites.clear()
         Favorites.load(persistenceWrapper.readFavorites())
+        // After loading favorites, add the package names of any apps with favorites to the list
+        // of authorized panels. That way, if the user has previously favorited controls for an app,
+        // that panel will be authorized.
+        authorizedPanelsRepository.addAuthorizedPanels(
+                Favorites.getAllStructures().map { it.componentName.packageName }.toSet())
     }
 
     private fun confirmAvailability(): Boolean {
@@ -477,6 +479,10 @@
         bindingController.unsubscribe()
     }
 
+    override fun bindComponentForPanel(componentName: ComponentName) {
+        bindingController.bindServiceForPanel(componentName)
+    }
+
     override fun addFavorite(
         componentName: ComponentName,
         structureName: CharSequence,
@@ -485,11 +491,27 @@
         if (!confirmAvailability()) return
         executor.execute {
             if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
+                authorizedPanelsRepository.addAuthorizedPanels(setOf(componentName.packageName))
                 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
             }
         }
     }
 
+    override fun canRemoveFavorites(componentName: ComponentName): Boolean =
+            !authorizedPanelsRepository.getPreferredPackages().contains(componentName.packageName)
+
+    override fun removeFavorites(componentName: ComponentName): Boolean {
+        if (!confirmAvailability()) return false
+        if (!canRemoveFavorites(componentName)) return false
+
+        executor.execute {
+            Favorites.removeStructures(componentName)
+            authorizedPanelsRepository.removeAuthorizedPanels(setOf(componentName.packageName))
+            persistenceWrapper.storeFavorites(Favorites.getAllStructures())
+        }
+        return true
+    }
+
     override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
         if (!confirmAvailability()) return
         executor.execute {
@@ -551,6 +573,10 @@
         return uiController.getPreferredSelectedItem(getFavorites())
     }
 
+    override fun setPreferredSelection(selectedItem: SelectedItem) {
+        uiController.updatePreferences(selectedItem)
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("ControlsController state:")
         pw.println("  Changing users: $userChanging")
@@ -644,10 +670,11 @@
         return true
     }
 
-    fun removeStructures(componentName: ComponentName) {
+    fun removeStructures(componentName: ComponentName): Boolean {
         val newFavMap = favMap.toMutableMap()
-        newFavMap.remove(componentName)
+        val removed = newFavMap.remove(componentName) != null
         favMap = newFavMap
+        return removed
     }
 
     fun addFavorite(
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
index 5b38e5b..72c3a94 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -78,6 +78,10 @@
         private const val DEBUG = true
         private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
             Context.BIND_NOT_PERCEPTIBLE
+        // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI.
+        // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app
+        // once the Task is finished in the device controls panel.
+        private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE
     }
 
     private val intent = Intent().apply {
@@ -87,18 +91,19 @@
         })
     }
 
-    private fun bindService(bind: Boolean) {
+    private fun bindService(bind: Boolean, forPanel: Boolean = false) {
         executor.execute {
             requiresBound = bind
             if (bind) {
-                if (bindTryCount != MAX_BIND_RETRIES) {
+                if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) {
                     if (DEBUG) {
                         Log.d(TAG, "Binding service $intent")
                     }
                     bindTryCount++
                     try {
+                        val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
                         val bound = context
-                            .bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
+                                .bindServiceAsUser(intent, serviceConnection, flags, user)
                         if (!bound) {
                             context.unbindService(serviceConnection)
                         }
@@ -279,6 +284,10 @@
         bindService(true)
     }
 
+    fun bindServiceForPanel() {
+        bindService(bind = true, forPanel = true)
+    }
+
     /**
      * Request unbind from the service.
      */
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/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index 6d6410d..d949d11 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -34,6 +34,8 @@
 import com.android.systemui.controls.management.ControlsListingControllerImpl
 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
 import com.android.systemui.controls.management.ControlsRequestDialog
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.AuthorizedPanelsRepositoryImpl
 import com.android.systemui.controls.settings.ControlsSettingsDialogManager
 import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl
 import com.android.systemui.controls.ui.ControlActionCoordinator
@@ -42,12 +44,15 @@
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.controls.ui.ControlsUiControllerImpl
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.DeviceControlsTile
 import dagger.Binds
 import dagger.BindsOptionalOf
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
 
 /**
  * Module for injecting classes in `com.android.systemui.controls`-
@@ -104,6 +109,11 @@
         coordinator: ControlActionCoordinatorImpl
     ): ControlActionCoordinator
 
+    @Binds
+    abstract fun provideAuthorizedPanelsRepository(
+        repository: AuthorizedPanelsRepositoryImpl
+    ): AuthorizedPanelsRepository
+
     @BindsOptionalOf
     abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper
 
@@ -142,4 +152,9 @@
     @IntoMap
     @ClassKey(ControlsActivity::class)
     abstract fun provideControlsActivity(activity: ControlsActivity): Activity
+
+    @Binds
+    @IntoMap
+    @StringKey(DeviceControlsTile.TILE_SPEC)
+    abstract fun bindDeviceControlsTile(controlsTile: DeviceControlsTile): QSTileImpl<*>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt
new file mode 100644
index 0000000..3f20c26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.controls.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.controls.start.ControlsStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class StartControlsStartableModule {
+    @Binds
+    @IntoMap
+    @ClassKey(ControlsStartable::class)
+    abstract fun bindFeature(impl: ControlsStartable): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
index 753d5ad..3fe0f03 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
@@ -45,14 +45,15 @@
  * @param onAppSelected a callback to indicate that an app has been selected in the list.
  */
 class AppAdapter(
-    backgroundExecutor: Executor,
-    uiExecutor: Executor,
-    lifecycle: Lifecycle,
-    controlsListingController: ControlsListingController,
-    private val layoutInflater: LayoutInflater,
-    private val onAppSelected: (ComponentName?) -> Unit = {},
-    private val favoritesRenderer: FavoritesRenderer,
-    private val resources: Resources
+        backgroundExecutor: Executor,
+        uiExecutor: Executor,
+        lifecycle: Lifecycle,
+        controlsListingController: ControlsListingController,
+        private val layoutInflater: LayoutInflater,
+        private val onAppSelected: (ControlsServiceInfo) -> Unit = {},
+        private val favoritesRenderer: FavoritesRenderer,
+        private val resources: Resources,
+        private val authorizedPanels: Set<String> = emptySet(),
 ) : RecyclerView.Adapter<AppAdapter.Holder>() {
 
     private var listOfServices = emptyList<ControlsServiceInfo>()
@@ -64,8 +65,10 @@
                 val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
                     it.loadLabel() ?: ""
                 }
-                listOfServices = serviceInfos.filter { it.panelActivity == null }
-                        .sortedWith(localeComparator)
+                // No panel or the panel is not authorized
+                listOfServices = serviceInfos.filter {
+                    it.panelActivity == null || it.panelActivity?.packageName !in authorizedPanels
+                }.sortedWith(localeComparator)
                 uiExecutor.execute(::notifyDataSetChanged)
             }
         }
@@ -86,8 +89,8 @@
 
     override fun onBindViewHolder(holder: Holder, index: Int) {
         holder.bindData(listOfServices[index])
-        holder.itemView.setOnClickListener {
-            onAppSelected(ComponentName.unflattenFromString(listOfServices[index].key))
+        holder.view.setOnClickListener {
+            onAppSelected(listOfServices[index])
         }
     }
 
@@ -95,6 +98,8 @@
      * Holder for binding views in the [RecyclerView]-
      */
     class Holder(view: View, val favRenderer: FavoritesRenderer) : RecyclerView.ViewHolder(view) {
+        val view: View = itemView
+
         private val icon: ImageView = itemView.requireViewById(com.android.internal.R.id.icon)
         private val title: TextView = itemView.requireViewById(com.android.internal.R.id.title)
         private val favorites: TextView = itemView.requireViewById(R.id.favorites)
@@ -106,7 +111,11 @@
         fun bindData(data: ControlsServiceInfo) {
             icon.setImageDrawable(data.loadIcon())
             title.text = data.loadLabel()
-            val text = favRenderer.renderFavoritesForComponent(data.componentName)
+            val text = if (data.panelActivity == null) {
+                favRenderer.renderFavoritesForComponent(data.componentName)
+            } else {
+                null
+            }
             favorites.text = text
             favorites.visibility = if (text == null) View.GONE else View.VISIBLE
         }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
index 90bc5d0..3808e73 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.controls.management
 
 import android.app.ActivityOptions
+import android.app.Dialog
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
@@ -31,12 +32,15 @@
 import android.window.OnBackInvokedCallback
 import android.window.OnBackInvokedDispatcher
 import androidx.activity.ComponentActivity
+import androidx.annotation.VisibleForTesting
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.android.systemui.R
+import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
 import com.android.systemui.controls.ui.ControlsActivity
-import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.controls.ui.SelectedItem
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.settings.UserTracker
@@ -52,7 +56,8 @@
     private val listingController: ControlsListingController,
     private val controlsController: ControlsController,
     private val userTracker: UserTracker,
-    private val uiController: ControlsUiController
+    private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+    private val panelConfirmationDialogFactory: PanelConfirmationDialogFactory
 ) : ComponentActivity() {
 
     companion object {
@@ -72,6 +77,7 @@
             }
         }
     }
+    private var dialog: Dialog? = null
 
     private val mOnBackInvokedCallback = OnBackInvokedCallback {
         if (DEBUG) {
@@ -138,9 +144,11 @@
                 lifecycle,
                 listingController,
                 LayoutInflater.from(this),
-                ::launchFavoritingActivity,
+                ::onAppSelected,
                 FavoritesRenderer(resources, controlsController::countFavoritesForComponent),
-                resources).apply {
+                resources,
+                authorizedPanelsRepository.getAuthorizedPanels()
+        ).apply {
             registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
                 var hasAnimated = false
                 override fun onChanged() {
@@ -167,13 +175,35 @@
             Log.d(TAG, "Unregistered onBackInvokedCallback")
         }
         onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback)
+        dialog?.cancel()
+    }
+
+    fun onAppSelected(serviceInfo: ControlsServiceInfo) {
+        dialog?.cancel()
+        if (serviceInfo.panelActivity == null) {
+            launchFavoritingActivity(serviceInfo.componentName)
+        } else {
+            val appName = serviceInfo.loadLabel() ?: ""
+            dialog = panelConfirmationDialogFactory.createConfirmationDialog(this, appName) { ok ->
+                if (ok) {
+                    authorizedPanelsRepository.addAuthorizedPanels(
+                            setOf(serviceInfo.componentName.packageName)
+                    )
+                    val selected = SelectedItem.PanelItem(appName, componentName)
+                    controlsController.setPreferredSelection(selected)
+                    animateExitAndFinish()
+                    openControlsOrigin()
+                }
+                dialog = null
+            }.also { it.show() }
+        }
     }
 
     /**
      * Launch the [ControlsFavoritingActivity] for the specified component.
      * @param component a component name for a [ControlsProviderService]
      */
-    fun launchFavoritingActivity(component: ComponentName?) {
+    private fun launchFavoritingActivity(component: ComponentName?) {
         executor.execute {
             component?.let {
                 val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java)
@@ -194,7 +224,15 @@
         super.onDestroy()
     }
 
-    private fun animateExitAndFinish() {
+    private fun openControlsOrigin() {
+        startActivity(
+                Intent(applicationContext, ControlsActivity::class.java),
+                ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+        )
+    }
+
+    @VisibleForTesting
+    internal open fun animateExitAndFinish() {
         val rootView = requireViewById<ViewGroup>(R.id.controls_management_root)
         ControlsAnimations.exitAnimation(
                 rootView,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
new file mode 100644
index 0000000..6f87aa9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.controls.management
+
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * Factory to create dialogs for consenting to show app panels for specific apps.
+ *
+ * [internalDialogFactory] is for facilitating testing.
+ */
+class PanelConfirmationDialogFactory(
+    private val internalDialogFactory: (Context) -> SystemUIDialog
+) {
+    @Inject constructor() : this({ SystemUIDialog(it) })
+
+    /**
+     * Creates a dialog to show to the user. [response] will be true if an only if the user responds
+     * affirmatively.
+     */
+    fun createConfirmationDialog(
+        context: Context,
+        appName: CharSequence,
+        response: Consumer<Boolean>
+    ): Dialog {
+        val listener =
+            DialogInterface.OnClickListener { _, which ->
+                response.accept(which == DialogInterface.BUTTON_POSITIVE)
+            }
+        return internalDialogFactory(context).apply {
+            setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName))
+            setMessage(this.context.getString(R.string.controls_panel_authorization, appName))
+            setCanceledOnTouchOutside(true)
+            setOnCancelListener { response.accept(false) }
+            setPositiveButton(R.string.controls_dialog_ok, listener)
+            setNeutralButton(R.string.cancel, listener)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt
new file mode 100644
index 0000000..ae9c37a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.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.controls.panels
+
+/**
+ * Repository for keeping track of which packages the panel has authorized to show control panels
+ * (embedded activity).
+ */
+interface AuthorizedPanelsRepository {
+
+    /** A set of package names that the user has previously authorized to show panels. */
+    fun getAuthorizedPanels(): Set<String>
+
+    /** Preferred applications to query controls suggestions from */
+    fun getPreferredPackages(): Set<String>
+
+    /** Adds [packageNames] to the set of packages that the user has authorized to show panels. */
+    fun addAuthorizedPanels(packageNames: Set<String>)
+
+    /**
+     * Removes [packageNames] from the set of packages that the user has authorized to show panels.
+     */
+    fun removeAuthorizedPanels(packageNames: Set<String>)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
new file mode 100644
index 0000000..e51e832
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
@@ -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.controls.panels
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.android.systemui.R
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import javax.inject.Inject
+
+class AuthorizedPanelsRepositoryImpl
+@Inject
+constructor(
+    private val context: Context,
+    private val userFileManager: UserFileManager,
+    private val userTracker: UserTracker
+) : AuthorizedPanelsRepository {
+
+    override fun getAuthorizedPanels(): Set<String> {
+        return getAuthorizedPanelsInternal(instantiateSharedPrefs())
+    }
+
+    override fun getPreferredPackages(): Set<String> =
+        context.resources.getStringArray(R.array.config_controlsPreferredPackages).toSet()
+
+    override fun addAuthorizedPanels(packageNames: Set<String>) {
+        addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames)
+    }
+
+    override fun removeAuthorizedPanels(packageNames: Set<String>) {
+        with(instantiateSharedPrefs()) {
+            val currentSet = getAuthorizedPanelsInternal(this)
+            edit().putStringSet(KEY, currentSet - packageNames).apply()
+        }
+    }
+
+    private fun getAuthorizedPanelsInternal(sharedPreferences: SharedPreferences): Set<String> {
+        return sharedPreferences.getStringSet(KEY, emptySet())!!
+    }
+
+    private fun addAuthorizedPanelsInternal(
+        sharedPreferences: SharedPreferences,
+        packageNames: Set<String>
+    ) {
+        val currentSet = getAuthorizedPanelsInternal(sharedPreferences)
+        sharedPreferences.edit().putStringSet(KEY, currentSet + packageNames).apply()
+    }
+
+    private fun instantiateSharedPrefs(): SharedPreferences {
+        val sharedPref =
+            userFileManager.getSharedPreferences(
+                DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+                Context.MODE_PRIVATE,
+                userTracker.userId,
+            )
+
+        // If we've never run this (i.e., the key doesn't exist), add the default packages
+        if (sharedPref.getStringSet(KEY, null) == null) {
+            sharedPref.edit().putStringSet(KEY, getPreferredPackages()).apply()
+        }
+        return sharedPref
+    }
+
+    companion object {
+        private const val KEY = "authorized_panels"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
new file mode 100644
index 0000000..9d99253
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.controls.start
+
+import android.content.Context
+import android.content.res.Resources
+import android.os.UserHandle
+import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.SelectedItem
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Started with SystemUI to perform early operations for device controls subsystem (only if enabled)
+ *
+ * In particular, it will perform the following:
+ * * If there is no preferred selection for provider and at least one of the preferred packages 
+ * provides a panel, it will select the first one that does.
+ * * If the preferred selection provides a panel, it will bind to that service (to reduce latency on
+ * displaying the panel).
+ *
+ * It will also perform those operations on user change.
+ */
+@SysUISingleton
+class ControlsStartable
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    @Background private val executor: Executor,
+    private val controlsComponent: ControlsComponent,
+    private val userTracker: UserTracker
+) : CoreStartable {
+
+    // These two controllers can only be accessed after `start` method once we've checked if the
+    // feature is enabled
+    private val controlsController: ControlsController
+        get() = controlsComponent.getControlsController().get()
+
+    private val controlsListingController: ControlsListingController
+        get() = controlsComponent.getControlsListingController().get()
+
+    private val userTrackerCallback =
+        object : UserTracker.Callback {
+            override fun onUserChanged(newUser: Int, userContext: Context) {
+                controlsController.changeUser(UserHandle.of(newUser))
+                startForUser()
+            }
+        }
+
+    override fun start() {
+        if (!controlsComponent.isEnabled()) {
+            // Controls is disabled, we don't need this anymore
+            return
+        }
+        startForUser()
+        userTracker.addCallback(userTrackerCallback, executor)
+    }
+
+    private fun startForUser() {
+        selectDefaultPanelIfNecessary()
+        bindToPanel()
+    }
+
+    private fun selectDefaultPanelIfNecessary() {
+        val currentSelection = controlsController.getPreferredSelection()
+        if (currentSelection == SelectedItem.EMPTY_SELECTION) {
+            val availableServices = controlsListingController.getCurrentServices()
+            val panels = availableServices.filter { it.panelActivity != null }
+            resources
+                .getStringArray(R.array.config_controlsPreferredPackages)
+                // Looking for the first element in the string array such that there is one package
+                // that has a panel. It will return null if there are no packages in the array,
+                // or if no packages in the array have a panel associated with it.
+                .firstNotNullOfOrNull { name ->
+                    panels.firstOrNull { it.componentName.packageName == name }
+                }
+                ?.let { info ->
+                    controlsController.setPreferredSelection(
+                        SelectedItem.PanelItem(info.loadLabel(), info.componentName)
+                    )
+                }
+        }
+    }
+
+    private fun bindToPanel() {
+        val currentSelection = controlsController.getPreferredSelection()
+        val panels =
+            controlsListingController.getCurrentServices().filter { it.panelActivity != null }
+        if (
+            currentSelection is SelectedItem.PanelItem &&
+                panels.firstOrNull { it.componentName == currentSelection.componentName } != null
+        ) {
+            controlsController.bindComponentForPanel(currentSelection.componentName)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index d8d8c0e..bf0a692 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -68,7 +68,7 @@
 
         getLifecycle().addObserver(
             ControlsAnimations.observerForAnimations(
-                requireViewById<ViewGroup>(R.id.control_detail_root),
+                requireViewById(R.id.control_detail_root),
                 window,
                 intent,
                 !featureFlags.isEnabled(Flags.USE_APP_PANELS)
@@ -95,7 +95,7 @@
     override fun onStart() {
         super.onStart()
 
-        parent = requireViewById<ViewGroup>(R.id.global_actions_controls)
+        parent = requireViewById(R.id.control_detail_root)
         parent.alpha = 0f
         if (featureFlags.isEnabled(Flags.USE_APP_PANELS) && !keyguardStateController.isUnlocked) {
             controlsSettingsDialogManager.maybeShowDialog(this) {
@@ -134,7 +134,8 @@
         super.onStop()
         mExitToDream = false
 
-        uiController.hide()
+        // parent is set in onStart, so the field is initialized when we get here
+        uiController.hide(parent)
         controlsSettingsDialogManager.closeDialog()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
new file mode 100644
index 0000000..d6cfb79
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.controls.ui
+
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.function.Consumer
+import javax.inject.Inject
+
+class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) {
+
+    @Inject constructor() : this({ SystemUIDialog(it) })
+
+    fun createRemoveAppDialog(
+        context: Context,
+        appName: CharSequence,
+        response: Consumer<Boolean>
+    ): Dialog {
+        val listener =
+            DialogInterface.OnClickListener { _, which ->
+                response.accept(which == DialogInterface.BUTTON_POSITIVE)
+            }
+        return internalDialogFactory(context).apply {
+            setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName))
+            setCanceledOnTouchOutside(true)
+            setOnCancelListener { response.accept(false) }
+            setPositiveButton(R.string.controls_dialog_remove, listener)
+            setNeutralButton(R.string.cancel, listener)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index f5c5905..58673bb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -31,7 +31,13 @@
     }
 
     fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context)
-    fun hide()
+
+    /**
+     * Hide the controls content if it's attached to this parent.
+     */
+    fun hide(parent: ViewGroup)
+
+    val isShowing: Boolean
 
     /**
      * Returns the preferred activity to start, depending on if the user has favorited any
@@ -58,6 +64,8 @@
      * This element will be the one that appears when the user first opens the controls activity.
      */
     fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem
+
+    fun updatePreferences(selectedItem: SelectedItem)
 }
 
 sealed class SelectedItem {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 1e3e5cd..c61dad6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -21,12 +21,15 @@
 import android.animation.ObjectAnimator
 import android.app.Activity
 import android.app.ActivityOptions
+import android.app.Dialog
 import android.app.PendingIntent
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
+import android.os.Trace
 import android.service.controls.Control
 import android.service.controls.ControlsProviderService
 import android.util.Log
@@ -38,6 +41,7 @@
 import android.view.animation.DecelerateInterpolator
 import android.widget.AdapterView
 import android.widget.ArrayAdapter
+import android.widget.BaseAdapter
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
@@ -49,7 +53,6 @@
 import com.android.systemui.R
 import com.android.systemui.controls.ControlsMetricsLogger
 import com.android.systemui.controls.ControlsServiceInfo
-import com.android.systemui.controls.settings.ControlsSettingsRepository
 import com.android.systemui.controls.CustomIconCache
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.controller.StructureInfo
@@ -60,10 +63,14 @@
 import com.android.systemui.controls.management.ControlsFavoritingActivity
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.settings.ControlsSettingsRepository
 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.globalactions.GlobalActionsPopupMenu
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserFileManager
@@ -77,7 +84,7 @@
 import dagger.Lazy
 import java.io.PrintWriter
 import java.text.Collator
-import java.util.Optional
+import java.util.*
 import java.util.function.Consumer
 import javax.inject.Inject
 
@@ -87,6 +94,7 @@
 class ControlsUiControllerImpl @Inject constructor (
         val controlsController: Lazy<ControlsController>,
         val context: Context,
+        private val packageManager: PackageManager,
         @Main val uiExecutor: DelayableExecutor,
         @Background val bgExecutor: DelayableExecutor,
         val controlsListingController: Lazy<ControlsListingController>,
@@ -99,6 +107,9 @@
         private val userTracker: UserTracker,
         private val taskViewFactory: Optional<TaskViewFactory>,
         private val controlsSettingsRepository: ControlsSettingsRepository,
+        private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+        private val featureFlags: FeatureFlags,
+        private val dialogsFactory: ControlsDialogsFactory,
         dumpManager: DumpManager
 ) : ControlsUiController, Dumpable {
 
@@ -108,6 +119,12 @@
         private const val PREF_IS_PANEL = "controls_is_panel"
 
         private const val FADE_IN_MILLIS = 200L
+
+        private const val OPEN_APP_ID = 0L
+        private const val ADD_CONTROLS_ID = 1L
+        private const val ADD_APP_ID = 2L
+        private const val EDIT_CONTROLS_ID = 3L
+        private const val REMOVE_APP_ID = 4L
     }
 
     private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION
@@ -135,6 +152,10 @@
         it.getTitle()
     }
 
+    private var openAppIntent: Intent? = null
+    private var overflowMenuAdapter: BaseAdapter? = null
+    private var removeAppDialog: Dialog? = null
+
     private val onSeedingComplete = Consumer<Boolean> {
         accepted ->
             if (accepted) {
@@ -151,6 +172,9 @@
     private lateinit var activityContext: Context
     private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
 
+    override val isShowing: Boolean
+        get() = !hidden
+
     init {
         dumpManager.registerDumpable(javaClass.name, this)
     }
@@ -160,6 +184,7 @@
     ): ControlsListingController.ControlsListingCallback {
         return object : ControlsListingController.ControlsListingCallback {
             override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
                 val lastItems = serviceInfos.map {
                     val uid = it.serviceInfo.applicationInfo.uid
 
@@ -169,7 +194,11 @@
                             it.loadIcon(),
                             it.componentName,
                             uid,
-                            it.panelActivity
+                            if (it.componentName.packageName in authorizedPanels) {
+                                it.panelActivity
+                            } else {
+                                null
+                            }
                     )
                 }
                 uiExecutor.execute {
@@ -203,9 +232,12 @@
         activityContext: Context
     ) {
         Log.d(ControlsUiController.TAG, "show()")
+        Trace.instant(Trace.TRACE_TAG_APP, "ControlsUiControllerImpl#show")
         this.parent = parent
         this.onDismiss = onDismiss
         this.activityContext = activityContext
+        this.openAppIntent = null
+        this.overflowMenuAdapter = null
         hidden = false
         retainCache = false
 
@@ -232,6 +264,8 @@
                     ControlKey(selected.structure.componentName, it.ci.controlId)
                 }
                 controlsController.get().subscribeToFavorites(selected.structure)
+            } else {
+                controlsController.get().bindComponentForPanel(selected.componentName)
             }
             listingCallback = createCallback(::showControlsView)
         }
@@ -294,6 +328,37 @@
         startTargetedActivity(si, ControlsEditingActivity::class.java)
     }
 
+    private fun startDefaultActivity() {
+        openAppIntent?.let {
+            startActivity(it, animateExtra = false)
+        }
+    }
+
+    @VisibleForTesting
+    internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) {
+        removeAppDialog?.cancel()
+        removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) {
+            if (!controlsController.get().removeFavorites(componentName)) {
+                return@createRemoveAppDialog
+            }
+            if (
+                    sharedPreferences.getString(PREF_COMPONENT, "") ==
+                    componentName.flattenToString()
+            ) {
+                sharedPreferences
+                        .edit()
+                        .remove(PREF_COMPONENT)
+                        .remove(PREF_STRUCTURE_OR_APP_NAME)
+                        .remove(PREF_IS_PANEL)
+                        .commit()
+            }
+
+            allStructures = controlsController.get().getFavorites()
+            selectedItem = getPreferredSelectedItem(allStructures)
+            reload(parent)
+        }.apply { show() }
+    }
+
     private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
         val i = Intent(activityContext, klazz)
         putIntentExtras(i, si)
@@ -317,9 +382,11 @@
         startActivity(i)
     }
 
-    private fun startActivity(intent: Intent) {
+    private fun startActivity(intent: Intent, animateExtra: Boolean = true) {
         // Force animations when transitioning from a dialog to an activity
-        intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+        if (animateExtra) {
+            intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+        }
 
         if (keyguardStateController.isShowing()) {
             activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */)
@@ -371,8 +438,34 @@
             Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem")
         }
 
+        bgExecutor.execute {
+            val intent = Intent(Intent.ACTION_MAIN)
+                    .addCategory(Intent.CATEGORY_LAUNCHER)
+                    .setPackage(selectionItem.componentName.packageName)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
+                            Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+            val intents = packageManager
+                    .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L))
+            intents.firstOrNull { it.activityInfo.exported }?.let { resolved ->
+                intent.setPackage(null)
+                intent.setComponent(resolved.activityInfo.componentName)
+                openAppIntent = intent
+                parent.post {
+                    // This will call show on the PopupWindow in the same thread, so make sure this
+                    // happens in the view thread.
+                    overflowMenuAdapter?.notifyDataSetChanged()
+                }
+            }
+        }
         createDropDown(panelsAndStructures, selectionItem)
-        createMenu()
+
+        val currentApps = panelsAndStructures.map { it.componentName }.toSet()
+        val allApps = controlsListingController.get()
+                .getCurrentServices().map { it.componentName }.toSet()
+        createMenu(
+                selectionItem = selectionItem,
+                extraApps = (allApps - currentApps).isNotEmpty(),
+        )
     }
 
     private fun createPanelView(componentName: ComponentName) {
@@ -411,22 +504,48 @@
         }
     }
 
-    private fun createMenu() {
+    private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) {
         val isPanel = selectedItem is SelectedItem.PanelItem
         val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure
                 ?: EMPTY_STRUCTURE
+        val newFlows = featureFlags.isEnabled(Flags.CONTROLS_MANAGEMENT_NEW_FLOWS)
 
-        val items = if (isPanel) {
-            arrayOf(
-                    context.resources.getString(R.string.controls_menu_add),
-            )
-        } else {
-            arrayOf(
-                    context.resources.getString(R.string.controls_menu_add),
-                    context.resources.getString(R.string.controls_menu_edit)
-            )
+        val items = buildList {
+            add(OverflowMenuAdapter.MenuItem(
+                    context.getText(R.string.controls_open_app),
+                    OPEN_APP_ID
+            ))
+            if (newFlows || isPanel) {
+                if (extraApps) {
+                    add(OverflowMenuAdapter.MenuItem(
+                            context.getText(R.string.controls_menu_add_another_app),
+                            ADD_APP_ID
+                    ))
+                }
+                if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED) &&
+                        controlsController.get().canRemoveFavorites(selectedItem.componentName)) {
+                    add(OverflowMenuAdapter.MenuItem(
+                            context.getText(R.string.controls_menu_remove),
+                            REMOVE_APP_ID,
+                    ))
+                }
+            } else {
+                add(OverflowMenuAdapter.MenuItem(
+                        context.getText(R.string.controls_menu_add),
+                        ADD_CONTROLS_ID
+                ))
+            }
+            if (!isPanel) {
+                add(OverflowMenuAdapter.MenuItem(
+                        context.getText(R.string.controls_menu_edit),
+                        EDIT_CONTROLS_ID
+                ))
+            }
         }
-        var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
+
+        val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position ->
+                getItemId(position) != OPEN_APP_ID || openAppIntent != null
+        }
 
         val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
         anchor.setOnClickListener(object : View.OnClickListener {
@@ -444,25 +563,24 @@
                             pos: Int,
                             id: Long
                         ) {
-                            when (pos) {
-                                // 0: Add Control
-                                0 -> {
-                                    if (isPanel) {
-                                        startProviderSelectorActivity()
-                                    } else {
-                                        startFavoritingActivity(selectedStructure)
-                                    }
-                                }
-                                // 1: Edit controls
-                                1 -> startEditingActivity(selectedStructure)
+                            when (id) {
+                                OPEN_APP_ID -> startDefaultActivity()
+                                ADD_APP_ID -> startProviderSelectorActivity()
+                                ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure)
+                                EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure)
+                                REMOVE_APP_ID -> startRemovingApp(
+                                        selectedStructure.componentName, selectionItem.appName
+                                )
                             }
                             dismiss()
                         }
                     })
                     show()
+                    listView?.post { listView?.requestAccessibilityFocus() }
                 }
             }
         })
+        overflowMenuAdapter = adapter
     }
 
     private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
@@ -470,8 +588,12 @@
             RenderInfo.registerComponentIcon(it.componentName, it.icon)
         }
 
-        var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
-            addAll(items)
+        val adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
+            add(selected)
+            addAll(items
+                    .filter { it !== selected }
+                    .sortedBy { it.appName.toString() }
+            )
         }
 
         val iconSize = context.resources
@@ -524,6 +646,7 @@
                         }
                     })
                     show()
+                    listView?.post { listView?.requestAccessibilityFocus() }
                 }
             }
         })
@@ -608,12 +731,12 @@
         }
     }
 
-    private fun updatePreferences(si: SelectedItem) {
+    override fun updatePreferences(selectedItem: SelectedItem) {
         sharedPreferences.edit()
-                .putString(PREF_COMPONENT, si.componentName.flattenToString())
-                .putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString())
-                .putBoolean(PREF_IS_PANEL, si is SelectedItem.PanelItem)
-                .commit()
+                .putString(PREF_COMPONENT, selectedItem.componentName.flattenToString())
+                .putString(PREF_STRUCTURE_OR_APP_NAME, selectedItem.name.toString())
+                .putBoolean(PREF_IS_PANEL, selectedItem is SelectedItem.PanelItem)
+                .apply()
     }
 
     private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean {
@@ -651,23 +774,30 @@
             it.value.dismiss()
         }
         controlActionCoordinator.closeDialogs()
+        removeAppDialog?.cancel()
     }
 
-    override fun hide() {
-        hidden = true
+    override fun hide(parent: ViewGroup) {
+        // We need to check for the parent because it's possible that  we have started showing in a
+        // different activity. In that case, make sure to only clear things associated with the
+        // passed parent
+        if (parent == this.parent) {
+            Log.d(ControlsUiController.TAG, "hide()")
+            hidden = true
 
-        closeDialogs(true)
-        controlsController.get().unsubscribe()
-        taskViewController?.dismiss()
-        taskViewController = null
+            closeDialogs(true)
+            controlsController.get().unsubscribe()
+            taskViewController?.dismiss()
+            taskViewController = null
 
+            controlsById.clear()
+            controlViewsById.clear()
+
+            controlsListingController.get().removeCallback(listingCallback)
+
+            if (!retainCache) RenderInfo.clearCache()
+        }
         parent.removeAllViews()
-        controlsById.clear()
-        controlViewsById.clear()
-
-        controlsListingController.get().removeCallback(listingCallback)
-
-        if (!retainCache) RenderInfo.clearCache()
     }
 
     override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/OverflowMenuAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/OverflowMenuAdapter.kt
new file mode 100644
index 0000000..6b84e36
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/OverflowMenuAdapter.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.controls.ui
+
+import android.content.Context
+import android.widget.ArrayAdapter
+import androidx.annotation.LayoutRes
+
+open class OverflowMenuAdapter(
+    context: Context,
+    @LayoutRes layoutId: Int,
+    itemsWithIds: List<MenuItem>,
+    private val isEnabledInternal: OverflowMenuAdapter.(Int) -> Boolean
+) : ArrayAdapter<CharSequence>(context, layoutId, itemsWithIds.map(MenuItem::text)) {
+
+    private val ids = itemsWithIds.map(MenuItem::id)
+
+    override fun getItemId(position: Int): Long {
+        return ids[position]
+    }
+
+    override fun isEnabled(position: Int): Boolean {
+        return isEnabledInternal(position)
+    }
+
+    data class MenuItem(val text: CharSequence, val id: Long)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
index f5764c2..78e87ca 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
@@ -27,6 +27,7 @@
 import android.graphics.Color
 import android.graphics.drawable.ShapeDrawable
 import android.graphics.drawable.shapes.RoundRectShape
+import android.os.Trace
 import com.android.systemui.R
 import com.android.systemui.util.boundsOnScreen
 import com.android.wm.shell.TaskView
@@ -70,7 +71,7 @@
                 taskView.post {
                     val roundedCorner =
                         activityContext.resources.getDimensionPixelSize(
-                            R.dimen.notification_corner_radius
+                            R.dimen.controls_panel_corner_radius
                         )
                     val radii = FloatArray(8) { roundedCorner.toFloat() }
                     taskView.background =
@@ -84,6 +85,7 @@
                         options,
                         taskView.boundsOnScreen
                     )
+                    Trace.instant(Trace.TRACE_TAG_APP, "PanelTaskViewController - startActivity")
                 }
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 4bb5d04..d1c34a8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -31,6 +31,7 @@
 import android.app.UiModeManager;
 import android.app.WallpaperManager;
 import android.app.admin.DevicePolicyManager;
+import android.app.ambientcontext.AmbientContextManager;
 import android.app.job.JobScheduler;
 import android.app.role.RoleManager;
 import android.app.smartspace.SmartspaceManager;
@@ -79,6 +80,7 @@
 import android.safetycenter.SafetyCenterManager;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
+import android.service.vr.IVrManager;
 import android.telecom.TelecomManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
@@ -146,6 +148,13 @@
         return Optional.ofNullable(context.getSystemService(SystemUpdateManager.class));
     }
 
+    @Provides
+    @Nullable
+    @Singleton
+    static AmbientContextManager provideAmbientContextManager(Context context) {
+        return context.getSystemService(AmbientContextManager.class);
+    }
+
     /** */
     @Provides
     public AmbientDisplayConfiguration provideAmbientDisplayConfiguration(Context context) {
@@ -261,6 +270,13 @@
     @Provides
     @Singleton
     @Nullable
+    static IVrManager provideIVrManager() {
+        return IVrManager.Stub.asInterface(ServiceManager.getService(Context.VR_SERVICE));
+    }
+
+    @Provides
+    @Singleton
+    @Nullable
     static FaceManager provideFaceManager(Context context) {
         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) {
             return context.getSystemService(FaceManager.class);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index fd690df..378b7bb 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -27,6 +27,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardViewController;
+import com.android.systemui.battery.BatterySaverModule;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
@@ -40,6 +41,7 @@
 import com.android.systemui.qs.tileimpl.QSFactoryImpl;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsImplementation;
+import com.android.systemui.rotationlock.RotationLockModule;
 import com.android.systemui.screenshot.ReferenceScreenshotModule;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeController;
@@ -50,6 +52,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule;
+import com.android.systemui.statusbar.events.StatusBarEventsModule;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.phone.DozeServiceHost;
@@ -92,11 +95,14 @@
  */
 @Module(includes = {
         AospPolicyModule.class,
+        BatterySaverModule.class,
         GestureModule.class,
         MediaModule.class,
         PowerModule.class,
         QSModule.class,
         ReferenceScreenshotModule.class,
+        RotationLockModule.class,
+        StatusBarEventsModule.class,
         StartCentralSurfacesModule.class,
         VolumeModule.class
 })
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 68f4dbe..625a028 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -35,6 +35,8 @@
 import com.android.systemui.unfold.FoldStateLoggingProvider;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.unfold.UnfoldLatencyTracker;
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
+import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
 import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider;
 import com.android.wm.shell.TaskViewFactory;
 import com.android.wm.shell.back.BackAnimation;
@@ -137,6 +139,10 @@
         getUnfoldLatencyTracker().init();
         getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
         getFoldStateLogger().ifPresent(FoldStateLogger::init);
+        getUnfoldTransitionProgressProvider().ifPresent((progressProvider) ->
+                getUnfoldTransitionProgressForwarder().ifPresent((forwarder) ->
+                        progressProvider.addCallback(forwarder)
+                ));
     }
 
     /**
@@ -164,6 +170,18 @@
     UnfoldLatencyTracker getUnfoldLatencyTracker();
 
     /**
+     * Creates a UnfoldTransitionProgressProvider.
+     */
+    @SysUISingleton
+    Optional<UnfoldTransitionProgressProvider> getUnfoldTransitionProgressProvider();
+
+    /**
+     * Creates a UnfoldTransitionProgressForwarder.
+     */
+    @SysUISingleton
+    Optional<UnfoldTransitionProgressForwarder> getUnfoldTransitionProgressForwarder();
+
+    /**
      * Creates a FoldStateLoggingProvider.
      */
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemPropertiesFlagsModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemPropertiesFlagsModule.kt
new file mode 100644
index 0000000..c6f833b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemPropertiesFlagsModule.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.dagger
+
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags
+import dagger.Module
+import dagger.Provides
+
+/** A module which provides access to the default [SystemUiSystemPropertiesFlags.FlagResolver] */
+@Module
+object SystemPropertiesFlagsModule {
+    /** provide the default FlagResolver. */
+    @Provides
+    fun provideFlagResolver(): SystemUiSystemPropertiesFlags.FlagResolver =
+        SystemUiSystemPropertiesFlags.getResolver()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 8012dea..947888b 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -26,11 +26,16 @@
 import com.android.systemui.accessibility.WindowMagnification
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
+import com.android.systemui.dreams.DreamMonitor
 import com.android.systemui.globalactions.GlobalActionsComponent
+import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.data.quickaffordance.MuteQuickAffordanceCoreStartable
 import com.android.systemui.log.SessionTracker
+import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
 import com.android.systemui.media.RingtonePlayer
 import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
 import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
@@ -51,7 +56,6 @@
 import com.android.systemui.toast.ToastUI
 import com.android.systemui.usb.StorageNotification
 import com.android.systemui.util.NotificationChannels
-import com.android.systemui.util.leak.GarbageMonitor
 import com.android.systemui.volume.VolumeUI
 import com.android.systemui.wmshell.WMShell
 import dagger.Binds
@@ -62,7 +66,10 @@
 /**
  * Collection of {@link CoreStartable}s that should be run on AOSP.
  */
-@Module(includes = [MultiUserUtilsModule::class])
+@Module(includes = [
+    MultiUserUtilsModule::class,
+    StartControlsStartableModule::class
+])
 abstract class SystemUICoreStartableModule {
     /** Inject into AuthController.  */
     @Binds
@@ -100,12 +107,6 @@
     @ClassKey(FsiChromeViewBinder::class)
     abstract fun bindFsiChromeWindowBinder(sysui: FsiChromeViewBinder): CoreStartable
 
-    /** Inject into GarbageMonitor.Service.  */
-    @Binds
-    @IntoMap
-    @ClassKey(GarbageMonitor::class)
-    abstract fun bindGarbageMonitorService(sysui: GarbageMonitor.Service): CoreStartable
-
     /** Inject into GlobalActionsComponent.  */
     @Binds
     @IntoMap
@@ -217,6 +218,12 @@
     @ClassKey(ToastUI::class)
     abstract fun bindToastUI(service: ToastUI): CoreStartable
 
+    /** Inject into MediaOutputSwitcherDialogUI.  */
+    @Binds
+    @IntoMap
+    @ClassKey(MediaOutputSwitcherDialogUI::class)
+    abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable
+
     /** Inject into VolumeUI.  */
     @Binds
     @IntoMap
@@ -279,4 +286,23 @@
     @IntoMap
     @ClassKey(StylusUsiPowerStartable::class)
     abstract fun bindStylusUsiPowerStartable(sysui: StylusUsiPowerStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(PhysicalKeyboardCoreStartable::class)
+    abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable
+
+    /** Inject into MuteQuickAffordanceCoreStartable*/
+    @Binds
+    @IntoMap
+    @ClassKey(MuteQuickAffordanceCoreStartable::class)
+    abstract fun bindMuteQuickAffordanceCoreStartable(
+            sysui: MuteQuickAffordanceCoreStartable
+    ): CoreStartable
+
+    /**Inject into DreamMonitor */
+    @Binds
+    @IntoMap
+    @ClassKey(DreamMonitor::class)
+    abstract fun bindDreamMonitor(sysui: DreamMonitor): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index b8e6673..f0ee443 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -28,9 +28,11 @@
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.BootCompleteCache;
 import com.android.systemui.BootCompleteCacheImpl;
+import com.android.systemui.accessibility.AccessibilityModule;
 import com.android.systemui.appops.dagger.AppOpsModule;
 import com.android.systemui.assist.AssistModule;
 import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
 import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
 import com.android.systemui.biometrics.dagger.BiometricsModule;
 import com.android.systemui.biometrics.dagger.UdfpsModule;
@@ -45,6 +47,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.FlagsModule;
 import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyguard.data.BouncerViewModule;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
@@ -53,20 +56,25 @@
 import com.android.systemui.navigationbar.NavigationBarComponent;
 import com.android.systemui.notetask.NoteTaskModule;
 import com.android.systemui.people.PeopleModule;
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
 import com.android.systemui.privacy.PrivacyModule;
+import com.android.systemui.qrcodescanner.dagger.QRCodeScannerModule;
 import com.android.systemui.qs.FgsManagerController;
 import com.android.systemui.qs.FgsManagerControllerImpl;
 import com.android.systemui.qs.footer.dagger.FooterActionsModule;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.screenrecord.ScreenRecordModule;
 import com.android.systemui.screenshot.dagger.ScreenshotModule;
 import com.android.systemui.security.data.repository.SecurityRepositoryModule;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.dagger.MultiUserUtilsModule;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.smartspace.dagger.SmartspaceModule;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.connectivity.ConnectivityModule;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
@@ -82,6 +90,7 @@
 import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.PolicyModule;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
 import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
@@ -94,6 +103,7 @@
 import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
 import com.android.systemui.util.dagger.UtilModule;
 import com.android.systemui.util.kotlin.CoroutinesModule;
+import com.android.systemui.util.leak.GarbageMonitorModule;
 import com.android.systemui.util.sensors.SensorModule;
 import com.android.systemui.util.settings.SettingsUtilModule;
 import com.android.systemui.util.time.SystemClock;
@@ -105,6 +115,8 @@
 import java.util.Optional;
 import java.util.concurrent.Executor;
 
+import javax.inject.Named;
+
 import dagger.Binds;
 import dagger.BindsOptionalOf;
 import dagger.Module;
@@ -121,6 +133,7 @@
  * may not appreciate that.
  */
 @Module(includes = {
+            AccessibilityModule.class,
             AppOpsModule.class,
             AssistModule.class,
             BiometricsModule.class,
@@ -128,24 +141,31 @@
             ClipboardOverlayModule.class,
             ClockInfoModule.class,
             ClockRegistryModule.class,
+            ConnectivityModule.class,
             CoroutinesModule.class,
             DreamModule.class,
             ControlsModule.class,
             DemoModeModule.class,
             FalsingModule.class,
             FlagsModule.class,
+            SystemPropertiesFlagsModule.class,
             FooterActionsModule.class,
+            GarbageMonitorModule.class,
+            KeyboardModule.class,
             LogModule.class,
             MediaProjectionModule.class,
             MotionToolModule.class,
             PeopleHubModule.class,
             PeopleModule.class,
             PluginModule.class,
+            PolicyModule.class,
             PrivacyModule.class,
+            QRCodeScannerModule.class,
             ScreenshotModule.class,
             SensorModule.class,
             MultiUserUtilsModule.class,
             SecurityRepositoryModule.class,
+            ScreenRecordModule.class,
             SettingsUtilModule.class,
             SmartRepliesInflationModule.class,
             SmartspaceModule.class,
@@ -194,8 +214,8 @@
 
     @SysUISingleton
     @Provides
-    static SysUiState provideSysUiState(DumpManager dumpManager) {
-        final SysUiState state = new SysUiState();
+    static SysUiState provideSysUiState(DisplayTracker displayTracker, DumpManager dumpManager) {
+        final SysUiState state = new SysUiState(displayTracker);
         dumpManager.registerDumpable(state);
         return state;
     }
@@ -210,6 +230,17 @@
     abstract BcSmartspaceDataPlugin optionalBcSmartspaceDataPlugin();
 
     @BindsOptionalOf
+    abstract BcSmartspaceConfigPlugin optionalBcSmartspaceConfigPlugin();
+
+    @BindsOptionalOf
+    @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN)
+    abstract BcSmartspaceDataPlugin optionalDateSmartspaceConfigPlugin();
+
+    @BindsOptionalOf
+    @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
+    abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceConfigPlugin();
+
+    @BindsOptionalOf
     abstract Recents optionalRecents();
 
     @BindsOptionalOf
@@ -221,6 +252,14 @@
     @BindsOptionalOf
     abstract AlternateUdfpsTouchProvider optionalUdfpsTouchProvider();
 
+    @BindsOptionalOf
+    abstract FingerprintInteractiveToAuthProvider optionalFingerprintInteractiveToAuthProvider();
+
+    @BindsOptionalOf
+    //TODO(b/269430792 remove full qualifier. Full qualifier is used to avoid merge conflict.)
+    abstract com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+    optionalSystemStatusAnimationScheduler();
+
     @SysUISingleton
     @Binds
     abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 976afd4..88c0c50 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.ScreenDecorationsLogger
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -45,6 +46,7 @@
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Main private val mainExecutor: Executor,
+    private val logger: ScreenDecorationsLogger,
 ) : DecorProviderFactory() {
     private val display = context.display
     private val displayInfo = DisplayInfo()
@@ -82,7 +84,8 @@
                                         authController,
                                         statusBarStateController,
                                         keyguardUpdateMonitor,
-                                        mainExecutor
+                                        mainExecutor,
+                                        logger,
                                 )
                         )
                     }
@@ -104,7 +107,8 @@
     private val authController: AuthController,
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val mainExecutor: Executor
+    private val mainExecutor: Executor,
+    private val logger: ScreenDecorationsLogger,
 ) : BoundDecorProvider() {
     override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
 
@@ -136,7 +140,8 @@
                 alignedBound,
                 statusBarStateController,
                 keyguardUpdateMonitor,
-                mainExecutor
+                mainExecutor,
+                logger,
         )
         view.id = viewId
         view.setColor(tintColor)
@@ -155,8 +160,9 @@
         layoutParams.let { lp ->
             lp.width = ViewGroup.LayoutParams.MATCH_PARENT
             lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+            logger.faceSensorLocation(authController.faceSensorLocation)
             authController.faceSensorLocation?.y?.let { faceAuthSensorHeight ->
-                val faceScanningHeight = (faceAuthSensorHeight * 2).toInt()
+                val faceScanningHeight = (faceAuthSensorHeight * 2)
                 when (rotation) {
                     Surface.ROTATION_0, Surface.ROTATION_180 ->
                         lp.height = faceScanningHeight
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
index 2a3d67f..c331164 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
@@ -17,12 +17,12 @@
 package com.android.systemui.doze;
 
 import android.hardware.display.AmbientDisplayConfiguration;
-import android.os.UserHandle;
 import android.util.Log;
 
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.DozeMachine.State;
 import com.android.systemui.doze.dagger.DozeScope;
+import com.android.systemui.settings.UserTracker;
 
 import java.io.PrintWriter;
 
@@ -40,14 +40,17 @@
     private final AmbientDisplayConfiguration mConfig;
     private DozeMachine mMachine;
     private final DockManager mDockManager;
+    private final UserTracker mUserTracker;
     private final DockEventListener mDockEventListener;
 
     private int mDockState = DockManager.STATE_NONE;
 
     @Inject
-    DozeDockHandler(AmbientDisplayConfiguration config, DockManager dockManager) {
+    DozeDockHandler(AmbientDisplayConfiguration config, DockManager dockManager,
+            UserTracker userTracker) {
         mConfig = config;
         mDockManager = dockManager;
+        mUserTracker = userTracker;
         mDockEventListener = new DockEventListener();
     }
 
@@ -100,7 +103,7 @@
                     nextState = State.DOZE_AOD_DOCKED;
                     break;
                 case DockManager.STATE_NONE:
-                    nextState = mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) ? State.DOZE_AOD
+                    nextState = mConfig.alwaysOnEnabled(mUserTracker.getUserId()) ? State.DOZE_AOD
                             : State.DOZE;
                     break;
                 case DockManager.STATE_DOCKED_HIDE:
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index 96c35d4..fc3263f 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -23,7 +23,6 @@
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Trace;
-import android.os.UserHandle;
 import android.util.Log;
 import android.view.Display;
 
@@ -33,6 +32,7 @@
 import com.android.systemui.doze.dagger.WrappedService;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.wakelock.WakeLock;
@@ -149,6 +149,7 @@
     private final DozeHost mDozeHost;
     private final DockManager mDockManager;
     private final Part[] mParts;
+    private final UserTracker mUserTracker;
 
     private final ArrayList<State> mQueuedRequests = new ArrayList<>();
     private State mState = State.UNINITIALIZED;
@@ -161,7 +162,7 @@
             AmbientDisplayConfiguration ambientDisplayConfig,
             WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
             DozeLog dozeLog, DockManager dockManager,
-            DozeHost dozeHost, Part[] parts) {
+            DozeHost dozeHost, Part[] parts, UserTracker userTracker) {
         mDozeService = service;
         mAmbientDisplayConfig = ambientDisplayConfig;
         mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -170,6 +171,7 @@
         mDockManager = dockManager;
         mDozeHost = dozeHost;
         mParts = parts;
+        mUserTracker = userTracker;
         for (Part part : parts) {
             part.setDozeMachine(this);
         }
@@ -429,7 +431,7 @@
                     nextState = State.FINISH;
                 } else if (mDockManager.isDocked()) {
                     nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED;
-                } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+                } else if (mAmbientDisplayConfig.alwaysOnEnabled(mUserTracker.getUserId())) {
                     nextState = State.DOZE_AOD;
                 } else {
                     nextState = State.DOZE;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 937884c..4cade77 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
+import com.android.systemui.util.settings.SystemSettings;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -78,6 +79,7 @@
     private final DozeParameters mDozeParameters;
     private final DevicePostureController mDevicePostureController;
     private final DozeLog mDozeLog;
+    private final SystemSettings mSystemSettings;
     private final int[] mSensorToBrightness;
     private final int[] mSensorToScrimOpacity;
     private final int mScreenBrightnessDim;
@@ -110,7 +112,8 @@
             WakefulnessLifecycle wakefulnessLifecycle,
             DozeParameters dozeParameters,
             DevicePostureController devicePostureController,
-            DozeLog dozeLog) {
+            DozeLog dozeLog,
+            SystemSettings systemSettings) {
         mContext = context;
         mDozeService = service;
         mSensorManager = sensorManager;
@@ -122,6 +125,7 @@
         mDozeHost = host;
         mHandler = handler;
         mDozeLog = dozeLog;
+        mSystemSettings = systemSettings;
 
         mScreenBrightnessMinimumDimAmountFloat = context.getResources().getFloat(
                 R.dimen.config_screenBrightnessMinimumDimAmountFloat);
@@ -257,7 +261,7 @@
     }
     //TODO: brightnessfloat change usages to float.
     private int clampToUserSetting(int brightness) {
-        int userSetting = Settings.System.getIntForUser(mContext.getContentResolver(),
+        int userSetting = mSystemSettings.getIntForUser(
                 Settings.System.SCREEN_BRIGHTNESS, Integer.MAX_VALUE,
                 UserHandle.USER_CURRENT);
         return Math.min(brightness, userSetting);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 37183e8..e3c568a9 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -116,6 +116,7 @@
     private boolean mListening;
     private boolean mListeningTouchScreenSensors;
     private boolean mListeningProxSensors;
+    private boolean mListeningAodOnlySensors;
     private boolean mUdfpsEnrolled;
 
     @DevicePostureController.DevicePostureInt
@@ -184,7 +185,8 @@
                         dozeParameters.getPulseOnSigMotion(),
                         DozeLog.PULSE_REASON_SENSOR_SIGMOTION,
                         false /* touchCoords */,
-                        false /* touchscreen */),
+                        false /* touchscreen */
+                ),
                 new TriggerSensor(
                         mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
                         Settings.Secure.DOZE_PICK_UP_GESTURE,
@@ -195,14 +197,17 @@
                         false /* touchscreen */,
                         false /* ignoresSetting */,
                         false /* requires prox */,
-                        true /* immediatelyReRegister */),
+                        true /* immediatelyReRegister */,
+                        false /* requiresAod */
+                ),
                 new TriggerSensor(
                         findSensor(config.doubleTapSensorType()),
                         Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
                         true /* configured */,
                         DozeLog.REASON_SENSOR_DOUBLE_TAP,
                         dozeParameters.doubleTapReportsTouchCoordinates(),
-                        true /* touchscreen */),
+                        true /* touchscreen */
+                ),
                 new TriggerSensor(
                         findSensors(config.tapSensorTypeMapping()),
                         Settings.Secure.DOZE_TAP_SCREEN_GESTURE,
@@ -214,7 +219,9 @@
                         false /* ignoresSetting */,
                         dozeParameters.singleTapUsesProx(mDevicePosture) /* requiresProx */,
                         true /* immediatelyReRegister */,
-                        mDevicePosture),
+                        mDevicePosture,
+                        false
+                ),
                 new TriggerSensor(
                         findSensor(config.longPressSensorType()),
                         Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
@@ -225,7 +232,9 @@
                         true /* touchscreen */,
                         false /* ignoresSetting */,
                         dozeParameters.longPressUsesProx() /* requiresProx */,
-                        true /* immediatelyReRegister */),
+                        true /* immediatelyReRegister */,
+                        false /* requiresAod */
+                ),
                 new TriggerSensor(
                         findSensor(config.udfpsLongPressSensorType()),
                         "doze_pulse_on_auth",
@@ -236,15 +245,18 @@
                         true /* touchscreen */,
                         false /* ignoresSetting */,
                         dozeParameters.longPressUsesProx(),
-                        false /* immediatelyReRegister */),
+                        false /* immediatelyReRegister */,
+                        true /* requiresAod */
+                ),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
                         Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE,
                         mConfig.wakeScreenGestureAvailable()
-                          && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT),
+                          && mConfig.alwaysOnEnabled(mUserTracker.getUserId()),
                         DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
                         false /* reports touch coordinates */,
-                        false /* touchscreen */),
+                        false /* touchscreen */
+                ),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_LOCK_SCREEN),
                         Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE,
@@ -252,7 +264,8 @@
                         DozeLog.PULSE_REASON_SENSOR_WAKE_REACH,
                         false /* reports touch coordinates */,
                         false /* touchscreen */,
-                        mConfig.getWakeLockScreenDebounce()),
+                        mConfig.getWakeLockScreenDebounce()
+                ),
                 new TriggerSensor(
                         findSensor(config.quickPickupSensorType()),
                         Settings.Secure.DOZE_QUICK_PICKUP_GESTURE,
@@ -263,7 +276,9 @@
                         false /* requiresTouchscreen */,
                         false /* ignoresSetting */,
                         false /* requiresProx */,
-                        true /* immediatelyReRegister */),
+                        true /* immediatelyReRegister */,
+                        false /* requiresAod */
+                ),
         };
         setProxListening(false);  // Don't immediately start listening when we register.
         mProximitySensor.register(
@@ -278,7 +293,7 @@
 
     private boolean udfpsLongPressConfigured() {
         return mUdfpsEnrolled
-                && (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) || mScreenOffUdfpsEnabled);
+                && (mConfig.alwaysOnEnabled(mUserTracker.getUserId()) || mScreenOffUdfpsEnabled);
     }
 
     private boolean quickPickUpConfigured() {
@@ -357,29 +372,36 @@
     /**
      * If sensors should be registered and sending signals.
      */
-    public void setListening(boolean listen, boolean includeTouchScreenSensors) {
-        if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors) {
+    public void setListening(boolean listen, boolean includeTouchScreenSensors,
+            boolean includeAodOnlySensors) {
+        if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors
+                && mListeningAodOnlySensors == includeAodOnlySensors) {
             return;
         }
         mListening = listen;
         mListeningTouchScreenSensors = includeTouchScreenSensors;
+        mListeningAodOnlySensors = includeAodOnlySensors;
         updateListening();
     }
 
     /**
      * If sensors should be registered and sending signals.
      */
-    public void setListening(boolean listen, boolean includeTouchScreenSensors,
-            boolean lowPowerStateOrOff) {
+    public void setListeningWithPowerState(boolean listen, boolean includeTouchScreenSensors,
+            boolean includeAodRequiringSensors, boolean lowPowerStateOrOff) {
         final boolean shouldRegisterProxSensors =
                 !mSelectivelyRegisterProxSensors || lowPowerStateOrOff;
-        if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors
-                && mListeningProxSensors == shouldRegisterProxSensors) {
+        if (mListening == listen
+                && mListeningTouchScreenSensors == includeTouchScreenSensors
+                && mListeningProxSensors == shouldRegisterProxSensors
+                && mListeningAodOnlySensors == includeAodRequiringSensors
+        ) {
             return;
         }
         mListening = listen;
         mListeningTouchScreenSensors = includeTouchScreenSensors;
         mListeningProxSensors = shouldRegisterProxSensors;
+        mListeningAodOnlySensors = includeAodRequiringSensors;
         updateListening();
     }
 
@@ -391,7 +413,8 @@
         for (TriggerSensor s : mTriggerSensors) {
             boolean listen = mListening
                     && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors)
-                    && (!s.mRequiresProx || mListeningProxSensors);
+                    && (!s.mRequiresProx || mListeningProxSensors)
+                    && (!s.mRequiresAod || mListeningAodOnlySensors);
             s.setListening(listen);
             if (listen) {
                 anyListening = true;
@@ -499,6 +522,9 @@
         private final boolean mRequiresTouchscreen;
         private final boolean mRequiresProx;
 
+        // Whether the sensor should only register if the device is in AOD
+        private final boolean mRequiresAod;
+
         // Whether to immediately re-register this sensor after the sensor is triggered.
         // If false, the sensor registration will be updated on the next AOD state transition.
         private final boolean mImmediatelyReRegister;
@@ -527,7 +553,8 @@
                     requiresTouchscreen,
                     false /* ignoresSetting */,
                     false /* requiresProx */,
-                    true /* immediatelyReRegister */
+                    true /* immediatelyReRegister */,
+                    false
             );
         }
 
@@ -541,7 +568,8 @@
                 boolean requiresTouchscreen,
                 boolean ignoresSetting,
                 boolean requiresProx,
-                boolean immediatelyReRegister
+                boolean immediatelyReRegister,
+                boolean requiresAod
         ) {
             this(
                     new Sensor[]{ sensor },
@@ -554,7 +582,8 @@
                     ignoresSetting,
                     requiresProx,
                     immediatelyReRegister,
-                    DevicePostureController.DEVICE_POSTURE_UNKNOWN
+                    DevicePostureController.DEVICE_POSTURE_UNKNOWN,
+                    requiresAod
             );
         }
 
@@ -569,7 +598,8 @@
                 boolean ignoresSetting,
                 boolean requiresProx,
                 boolean immediatelyReRegister,
-                @DevicePostureController.DevicePostureInt int posture
+                @DevicePostureController.DevicePostureInt int posture,
+                boolean requiresAod
         ) {
             mSensors = sensors;
             mSetting = setting;
@@ -580,6 +610,7 @@
             mRequiresTouchscreen = requiresTouchscreen;
             mIgnoresSetting = ignoresSetting;
             mRequiresProx = requiresProx;
+            mRequiresAod = requiresAod;
             mPosture = posture;
             mImmediatelyReRegister = immediatelyReRegister;
         }
@@ -663,13 +694,13 @@
         }
 
         protected boolean enabledBySetting() {
-            if (!mConfig.enabled(UserHandle.USER_CURRENT)) {
+            if (!mConfig.enabled(mUserTracker.getUserId())) {
                 return false;
             } else if (TextUtils.isEmpty(mSetting)) {
                 return true;
             }
             return mSecureSettings.getIntForUser(mSetting, mSettingDefault ? 1 : 0,
-                    UserHandle.USER_CURRENT) != 0;
+                    mUserTracker.getUserId()) != 0;
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index e6d9865..de0bdd3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -20,10 +20,10 @@
 
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.PowerManager;
-import android.os.UserHandle;
 import android.text.TextUtils;
 
 import com.android.systemui.doze.dagger.DozeScope;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
 import java.io.PrintWriter;
@@ -57,6 +57,7 @@
     private final AmbientDisplayConfiguration mConfig;
     private final DozeLog mDozeLog;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
+    private final UserTracker mUserTracker;
 
     private boolean mIsCarModeEnabled = false;
 
@@ -65,11 +66,13 @@
             DozeHost dozeHost,
             AmbientDisplayConfiguration config,
             DozeLog dozeLog,
-            Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
+            Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+            UserTracker userTracker) {
         mDozeHost = dozeHost;
         mConfig = config;
         mDozeLog = dozeLog;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
+        mUserTracker = userTracker;
     }
 
     @Override
@@ -148,7 +151,7 @@
 
     private void handleCarModeExited() {
         mDozeLog.traceCarModeEnded();
-        mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+        mMachine.requestState(mConfig.alwaysOnEnabled(mUserTracker.getUserId())
                 ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
     }
 
@@ -166,7 +169,7 @@
             if (mDozeHost.isPowerSaveActive()) {
                 nextState = DozeMachine.State.DOZE;
             } else if (mMachine.getState() == DozeMachine.State.DOZE
-                    && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+                    && mConfig.alwaysOnEnabled(mUserTracker.getUserId())) {
                 nextState = DozeMachine.State.DOZE_AOD;
             }
 
@@ -181,7 +184,7 @@
             // handles suppression changes, while DozeMachine#transitionPolicy handles gating
             // transitions to DOZE_AOD
             final DozeMachine.State nextState;
-            if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) && !suppressed) {
+            if (mConfig.alwaysOnEnabled(mUserTracker.getUserId()) && !suppressed) {
                 nextState = DozeMachine.State.DOZE_AOD;
             } else {
                 nextState = DozeMachine.State.DOZE;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index b95c3f3..b709608 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -111,6 +111,7 @@
     private boolean mWantProxSensor;
     private boolean mWantTouchScreenSensors;
     private boolean mWantSensors;
+    private boolean mInAod;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -460,12 +461,19 @@
                 mDozeSensors.requestTemporaryDisable();
                 break;
             case DOZE:
-            case DOZE_AOD:
                 mAodInterruptRunnable = null;
-                mWantProxSensor = newState != DozeMachine.State.DOZE;
+                mWantProxSensor = false;
                 mWantSensors = true;
                 mWantTouchScreenSensors = true;
-                if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) {
+                mInAod = false;
+                break;
+            case DOZE_AOD:
+                mAodInterruptRunnable = null;
+                mWantProxSensor = true;
+                mWantSensors = true;
+                mWantTouchScreenSensors = true;
+                mInAod = true;
+                if (!sWakeDisplaySensorState) {
                     onWakeScreen(false, newState, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE);
                 }
                 break;
@@ -491,7 +499,7 @@
                 break;
             default:
         }
-        mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors);
+        mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, mInAod);
     }
 
     private void registerCallbacks() {
@@ -510,11 +518,12 @@
 
     private void stopListeningToAllTriggers() {
         unregisterCallbacks();
-        mDozeSensors.setListening(false, false);
+        mDozeSensors.setListening(false, false, false);
         mDozeSensors.setProxListening(false);
         mWantSensors = false;
         mWantProxSensor = false;
         mWantTouchScreenSensors = false;
+        mInAod = false;
     }
 
     @Override
@@ -523,7 +532,8 @@
         final boolean lowPowerStateOrOff = state == Display.STATE_DOZE
                 || state == Display.STATE_DOZE_SUSPEND || state == Display.STATE_OFF;
         mDozeSensors.setProxListening(mWantProxSensor && lowPowerStateOrOff);
-        mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, lowPowerStateOrOff);
+        mDozeSensors.setListeningWithPowerState(mWantSensors, mWantTouchScreenSensors,
+                mInAod, lowPowerStateOrOff);
 
         if (mAodInterruptRunnable != null && state == Display.STATE_ON) {
             mAodInterruptRunnable.run();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamMonitor.java
new file mode 100644
index 0000000..055cd52
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamMonitor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.dreams;
+
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_PRETEXT_MONITOR;
+
+import android.util.Log;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.dreams.callbacks.DreamStatusBarStateCallback;
+import com.android.systemui.dreams.conditions.DreamCondition;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * A {@link CoreStartable} to retain a monitor for tracking dreaming.
+ */
+public class DreamMonitor extends ConditionalCoreStartable {
+    private static final String TAG = "DreamMonitor";
+
+    // We retain a reference to the monitor so it is not garbage-collected.
+    private final Monitor mConditionMonitor;
+    private final DreamCondition mDreamCondition;
+    private final DreamStatusBarStateCallback mCallback;
+
+
+    @Inject
+    public DreamMonitor(Monitor monitor, DreamCondition dreamCondition,
+            @Named(DREAM_PRETEXT_MONITOR) Monitor pretextMonitor,
+            DreamStatusBarStateCallback callback) {
+        super(pretextMonitor);
+        mConditionMonitor = monitor;
+        mDreamCondition = dreamCondition;
+        mCallback = callback;
+
+    }
+
+    @Override
+    protected void onStart() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "started");
+        }
+
+        mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(mCallback)
+                .addCondition(mDreamCondition)
+                .build());
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index c882f8a..c3bd5d9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -182,6 +182,18 @@
             }
     }
 
+    /**
+     * Ends the dream content and dream overlay animations, if they're currently running.
+     * @see [AnimatorSet.end]
+     */
+    fun endAnimations() {
+        mAnimator =
+            mAnimator?.let {
+                it.end()
+                null
+            }
+    }
+
     private fun blurAnimator(
         view: View,
         fromBlurRadius: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 3106173..50cfb6a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -37,10 +37,11 @@
 import com.android.systemui.dreams.complication.ComplicationHostViewController;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.dreams.dagger.DreamOverlayModule;
+import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.BlurUtils;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
@@ -55,7 +56,6 @@
 @DreamOverlayComponent.DreamOverlayScope
 public class DreamOverlayContainerViewController extends ViewController<DreamOverlayContainerView> {
     private final DreamOverlayStatusBarViewController mStatusBarViewController;
-    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final BlurUtils mBlurUtils;
     private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
     private final DreamOverlayStateController mStateController;
@@ -84,9 +84,26 @@
     private long mJitterStartTimeMillis;
 
     private boolean mBouncerAnimating;
+    private boolean mWakingUpFromSwipe;
 
-    private final KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback =
-            new KeyguardBouncer.PrimaryBouncerExpansionCallback() {
+    private final BouncerlessScrimController mBouncerlessScrimController;
+
+    private final BouncerlessScrimController.Callback mBouncerlessExpansionCallback =
+            new BouncerlessScrimController.Callback() {
+        @Override
+        public void onExpansion(ShadeExpansionChangeEvent event) {
+            updateTransitionState(event.getFraction());
+        }
+
+        @Override
+        public void onWakeup() {
+            mWakingUpFromSwipe = true;
+        }
+    };
+
+    private final PrimaryBouncerExpansionCallback
+            mBouncerExpansionCallback =
+            new PrimaryBouncerExpansionCallback() {
 
                 @Override
                 public void onStartingToShow() {
@@ -125,13 +142,29 @@
                 }
             };
 
+    /**
+     * If true, overlay entry animations should be skipped once.
+     *
+     * This is turned on when exiting low light and should be turned off once the entry animations
+     * are skipped once.
+     */
+    private boolean mSkipEntryAnimations;
+
+    private final DreamOverlayStateController.Callback
+            mDreamOverlayStateCallback =
+            new DreamOverlayStateController.Callback() {
+                @Override
+                public void onExitLowLight() {
+                    mSkipEntryAnimations = true;
+                }
+            };
+
     @Inject
     public DreamOverlayContainerViewController(
             DreamOverlayContainerView containerView,
             ComplicationHostViewController complicationHostViewController,
             @Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView,
             DreamOverlayStatusBarViewController statusBarViewController,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             BlurUtils blurUtils,
             @Main Handler handler,
             @Main Resources resources,
@@ -141,15 +174,18 @@
             @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter,
             PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
             DreamOverlayAnimationsController animationsController,
-            DreamOverlayStateController stateController) {
+            DreamOverlayStateController stateController,
+            BouncerlessScrimController bouncerlessScrimController) {
         super(containerView);
         mDreamOverlayContentView = contentView;
         mStatusBarViewController = statusBarViewController;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mBlurUtils = blurUtils;
         mDreamOverlayAnimationsController = animationsController;
         mStateController = stateController;
 
+        mBouncerlessScrimController = bouncerlessScrimController;
+        mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
+
         mComplicationHostViewController = complicationHostViewController;
         mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
                 R.dimen.dream_overlay_y_offset);
@@ -168,6 +204,7 @@
 
     @Override
     protected void onInit() {
+        mStateController.addCallback(mDreamOverlayStateCallback);
         mStatusBarViewController.init();
         mComplicationHostViewController.init();
         mDreamOverlayAnimationsController.init(mView);
@@ -175,27 +212,27 @@
 
     @Override
     protected void onViewAttached() {
+        mWakingUpFromSwipe = false;
         mJitterStartTimeMillis = System.currentTimeMillis();
         mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
-        final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getPrimaryBouncer();
-        if (bouncer != null) {
-            bouncer.addBouncerExpansionCallback(mBouncerExpansionCallback);
-        }
         mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
 
         // Start dream entry animations. Skip animations for low light clock.
         if (!mStateController.isLowLightActive()) {
             mDreamOverlayAnimationsController.startEntryAnimations();
+
+            if (mSkipEntryAnimations) {
+                // If we're transitioning from the low light dream back to the user dream, skip the
+                // overlay animations and show immediately.
+                mDreamOverlayAnimationsController.endAnimations();
+                mSkipEntryAnimations = false;
+            }
         }
     }
 
     @Override
     protected void onViewDetached() {
         mHandler.removeCallbacks(this::updateBurnInOffsets);
-        final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getPrimaryBouncer();
-        if (bouncer != null) {
-            bouncer.removeBouncerExpansionCallback(mBouncerExpansionCallback);
-        }
         mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
 
         mDreamOverlayAnimationsController.cancelAnimations();
@@ -264,6 +301,13 @@
      */
     public void wakeUp(@NonNull Runnable onAnimationEnd,
             @NonNull DelayableExecutor callbackExecutor) {
+        // When swiping causes wakeup, do not run any animations as the dream should exit as soon
+        // as possible.
+        if (mWakingUpFromSwipe) {
+            onAnimationEnd.run();
+            return;
+        }
+
         mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
index 87c5f51..a2dcdf5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
@@ -17,6 +17,7 @@
 package com.android.systemui.dreams;
 
 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_SERVICE_COMPONENT;
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_PRETEXT_MONITOR;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -33,8 +34,9 @@
 import android.service.dreams.IDreamManager;
 import android.util.Log;
 
-import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -43,7 +45,7 @@
  * {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be
  * the designated dream overlay component.
  */
-public class DreamOverlayRegistrant implements CoreStartable {
+public class DreamOverlayRegistrant extends ConditionalCoreStartable {
     private static final String TAG = "DreamOverlayRegistrant";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private final IDreamManager mDreamManager;
@@ -102,7 +104,9 @@
 
     @Inject
     public DreamOverlayRegistrant(Context context, @Main Resources resources,
-            @Named(DREAM_OVERLAY_SERVICE_COMPONENT) ComponentName dreamOverlayServiceComponent) {
+            @Named(DREAM_OVERLAY_SERVICE_COMPONENT) ComponentName dreamOverlayServiceComponent,
+            @Named(DREAM_PRETEXT_MONITOR) Monitor monitor) {
+        super(monitor);
         mContext = context;
         mResources = resources;
         mDreamManager = IDreamManager.Stub.asInterface(
@@ -111,7 +115,7 @@
     }
 
     @Override
-    public void start() {
+    protected void onStart() {
         final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_CHANGED);
         filter.addDataScheme("package");
         filter.addDataSchemeSpecificPart(mOverlayServiceComponent.getPackageName(),
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index a9a9cae..854323f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.dreams;
 
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.drawable.ColorDrawable;
@@ -70,6 +72,7 @@
     private final ComponentName mLowLightDreamComponent;
     private final UiEventLogger mUiEventLogger;
     private final WindowManager mWindowManager;
+    private final String mWindowTitle;
 
     // A reference to the {@link Window} used to hold the dream overlay.
     private Window mWindow;
@@ -134,7 +137,8 @@
             UiEventLogger uiEventLogger,
             @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
                     ComponentName lowLightDreamComponent,
-            DreamOverlayCallbackController dreamOverlayCallbackController) {
+            DreamOverlayCallbackController dreamOverlayCallbackController,
+            @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
         mContext = context;
         mExecutor = executor;
         mWindowManager = windowManager;
@@ -144,6 +148,7 @@
         mStateController = stateController;
         mUiEventLogger = uiEventLogger;
         mDreamOverlayCallbackController = dreamOverlayCallbackController;
+        mWindowTitle = windowTitle;
 
         final ViewModelStore viewModelStore = new ViewModelStore();
         final Complication.Host host =
@@ -195,7 +200,14 @@
             mDreamOverlayTouchMonitor.init();
 
             mStateController.setShouldShowComplications(shouldShowComplications());
-            addOverlayWindowLocked(layoutParams);
+
+            // If we are not able to add the overlay window, reset the overlay.
+            if (!addOverlayWindowLocked(layoutParams)) {
+                resetCurrentDreamOverlayLocked();
+                return;
+            }
+
+
             setCurrentStateLocked(Lifecycle.State.RESUMED);
             mStateController.setOverlayActive(true);
             final ComponentName dreamComponent = getDreamComponent();
@@ -208,6 +220,13 @@
         });
     }
 
+    @Override
+    public void onEndDream() {
+        mExecutor.execute(() -> {
+            resetCurrentDreamOverlayLocked();
+        });
+    }
+
     private Lifecycle.State getCurrentStateLocked() {
         return mLifecycleRegistry.getCurrentState();
     }
@@ -234,8 +253,10 @@
      * @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting
      *                     into the dream window.
      */
-    private void addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) {
+    private boolean addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) {
         mWindow = new PhoneWindow(mContext);
+        // Default to SystemUI name for TalkBack.
+        mWindow.setTitle(mWindowTitle);
         mWindow.setAttributes(layoutParams);
         mWindow.setWindowManager(null, layoutParams.token, "DreamOverlay", true);
 
@@ -257,9 +278,22 @@
         // risk an IllegalStateException in some cases when setting the container view as the
         // window's content view and the container view hasn't been properly removed previously).
         removeContainerViewFromParentLocked();
+
         mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView());
 
-        mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
+        // It is possible that a dream's window (and the dream as a whole) is no longer valid by
+        // the time the overlay service processes the dream. This can happen for example if
+        // another dream is started immediately after the existing dream begins. In this case, the
+        // overlay service should identify the situation through the thrown exception and tear down
+        // the overlay.
+        try {
+            mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
+            return true;
+        } catch (WindowManager.BadTokenException exception) {
+            Log.e(TAG, "Dream activity window invalid: " + layoutParams.packageName,
+                    exception);
+            return false;
+        }
     }
 
     private void removeContainerViewFromParentLocked() {
@@ -291,6 +325,7 @@
         mDreamOverlayContainerViewController = null;
         mDreamOverlayTouchMonitor = null;
 
+        mWindow = null;
         mStarted = false;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index ccfdd096..2c7ecb1 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -83,6 +83,12 @@
          */
         default void onAvailableComplicationTypesChanged() {
         }
+
+        /**
+         * Called when the low light dream is exiting and transitioning back to the user dream.
+         */
+        default void onExitLowLight() {
+        }
     }
 
     private final Executor mExecutor;
@@ -278,6 +284,10 @@
      * @param active {@code true} if low light mode is active, {@code false} otherwise.
      */
     public void setLowLightActive(boolean active) {
+        if (isLowLightActive() && !active) {
+            // Notify that we're exiting low light only on the transition from active to not active.
+            mCallbacks.forEach(Callback::onExitLowLight);
+        }
         modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_LOW_LIGHT_ACTIVE);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index f244cb0..96bce4c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
@@ -26,6 +27,9 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.systemui.R;
+import com.android.systemui.shared.shadow.DoubleShadowIconDrawable;
+import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo;
+import com.android.systemui.statusbar.AlphaOptimizedImageView;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -60,8 +64,15 @@
     public static final int STATUS_ICON_PRIORITY_MODE_ON = 6;
 
     private final Map<Integer, View> mStatusIcons = new HashMap<>();
+    private Context mContext;
     private ViewGroup mSystemStatusViewGroup;
     private ViewGroup mExtraSystemStatusViewGroup;
+    private ShadowInfo mKeyShadowInfo;
+    private ShadowInfo mAmbientShadowInfo;
+    private int mDrawableSize;
+    private int mDrawableInsetSize;
+    private static final float KEY_SHADOW_ALPHA = 0.35f;
+    private static final float AMBIENT_SHADOW_ALPHA = 0.4f;
 
     public DreamOverlayStatusBarView(Context context) {
         this(context, null);
@@ -73,6 +84,7 @@
 
     public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) {
         this(context, attrs, defStyleAttr, 0);
+        mContext = context;
     }
 
     public DreamOverlayStatusBarView(
@@ -80,14 +92,36 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
+        mKeyShadowInfo = createShadowInfo(
+            R.dimen.dream_overlay_status_bar_key_text_shadow_radius,
+            R.dimen.dream_overlay_status_bar_key_text_shadow_dx,
+            R.dimen.dream_overlay_status_bar_key_text_shadow_dy,
+            KEY_SHADOW_ALPHA
+        );
+
+        mAmbientShadowInfo = createShadowInfo(
+            R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius,
+            R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx,
+            R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy,
+            AMBIENT_SHADOW_ALPHA
+        );
+
+        mDrawableSize = mContext
+                        .getResources()
+                        .getDimensionPixelSize(R.dimen.dream_overlay_status_bar_icon_size);
+        mDrawableInsetSize = mContext
+                             .getResources()
+                             .getDimensionPixelSize(R.dimen.dream_overlay_icon_inset_dimen);
+
         mStatusIcons.put(STATUS_ICON_WIFI_UNAVAILABLE,
-                fetchStatusIconForResId(R.id.dream_overlay_wifi_status));
+                addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_wifi_status)));
         mStatusIcons.put(STATUS_ICON_ALARM_SET,
-                fetchStatusIconForResId(R.id.dream_overlay_alarm_set));
+                addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_alarm_set)));
         mStatusIcons.put(STATUS_ICON_CAMERA_DISABLED,
                 fetchStatusIconForResId(R.id.dream_overlay_camera_off));
         mStatusIcons.put(STATUS_ICON_MIC_DISABLED,
@@ -97,7 +131,7 @@
         mStatusIcons.put(STATUS_ICON_NOTIFICATIONS,
                 fetchStatusIconForResId(R.id.dream_overlay_notification_indicator));
         mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON,
-                fetchStatusIconForResId(R.id.dream_overlay_priority_mode));
+                addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode)));
 
         mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status);
         mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items);
@@ -137,4 +171,34 @@
         }
         return false;
     }
+
+    private View addDoubleShadow(View icon) {
+        if (icon instanceof AlphaOptimizedImageView) {
+            AlphaOptimizedImageView i = (AlphaOptimizedImageView) icon;
+            Drawable drawableIcon = i.getDrawable();
+            i.setImageDrawable(new DoubleShadowIconDrawable(
+                    mKeyShadowInfo,
+                    mAmbientShadowInfo,
+                    drawableIcon,
+                    mDrawableSize,
+                    mDrawableInsetSize
+            ));
+        }
+        return icon;
+    }
+
+    private ShadowInfo createShadowInfo(int blurId, int offsetXId, int offsetYId, float alpha) {
+        return new ShadowInfo(
+            fetchDimensionForResId(blurId),
+            fetchDimensionForResId(offsetXId),
+            fetchDimensionForResId(offsetYId),
+            alpha
+        );
+    }
+
+    private Float fetchDimensionForResId(int resId) {
+        return mContext
+               .getResources()
+               .getDimension(resId);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index f1bb156..7394e236 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -25,7 +25,6 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.format.DateFormat;
 import android.util.PluralsMessageFormatter;
@@ -37,6 +36,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
@@ -72,6 +72,7 @@
             mDreamOverlayNotificationCountProvider;
     private final ZenModeController mZenModeController;
     private final DreamOverlayStateController mDreamOverlayStateController;
+    private final UserTracker mUserTracker;
     private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final DreamOverlayStatusBarItemsProvider mStatusBarItemsProvider;
     private final Executor mMainExecutor;
@@ -154,7 +155,8 @@
             ZenModeController zenModeController,
             StatusBarWindowStateController statusBarWindowStateController,
             DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
-            DreamOverlayStateController dreamOverlayStateController) {
+            DreamOverlayStateController dreamOverlayStateController,
+            UserTracker userTracker) {
         super(view);
         mResources = resources;
         mMainExecutor = mainExecutor;
@@ -169,6 +171,7 @@
         mStatusBarItemsProvider = statusBarItemsProvider;
         mZenModeController = zenModeController;
         mDreamOverlayStateController = dreamOverlayStateController;
+        mUserTracker = userTracker;
 
         // Register to receive show/hide updates for the system status bar. Our custom status bar
         // needs to hide when the system status bar is showing to ovoid overlapping status bars.
@@ -254,12 +257,13 @@
                         mConnectivityManager.getActiveNetwork());
         final boolean available = capabilities != null
                 && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
-        showIcon(DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, !available);
+        showIcon(DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, !available,
+                R.string.wifi_unavailable_dream_overlay_content_description);
     }
 
     private void updateAlarmStatusIcon() {
         final AlarmManager.AlarmClockInfo alarm =
-                mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+                mAlarmManager.getNextAlarmClock(mUserTracker.getUserId());
         final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
         showIcon(
                 DreamOverlayStatusBarView.STATUS_ICON_ALARM_SET,
@@ -291,13 +295,16 @@
         @DreamOverlayStatusBarView.StatusIconType int iconType = Resources.ID_NULL;
         showIcon(
                 DreamOverlayStatusBarView.STATUS_ICON_CAMERA_DISABLED,
-                !micBlocked && cameraBlocked);
+                !micBlocked && cameraBlocked,
+                R.string.camera_blocked_dream_overlay_content_description);
         showIcon(
                 DreamOverlayStatusBarView.STATUS_ICON_MIC_DISABLED,
-                micBlocked && !cameraBlocked);
+                micBlocked && !cameraBlocked,
+                R.string.microphone_blocked_dream_overlay_content_description);
         showIcon(
                 DreamOverlayStatusBarView.STATUS_ICON_MIC_CAMERA_DISABLED,
-                micBlocked && cameraBlocked);
+                micBlocked && cameraBlocked,
+                R.string.camera_and_microphone_blocked_dream_overlay_content_description);
     }
 
     private String buildNotificationsContentDescription(int notificationCount) {
@@ -310,11 +317,13 @@
     private void updatePriorityModeStatusIcon() {
         showIcon(
                 DreamOverlayStatusBarView.STATUS_ICON_PRIORITY_MODE_ON,
-                mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF);
+                mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF,
+                R.string.priority_mode_dream_overlay_content_description);
     }
 
-    private void showIcon(@DreamOverlayStatusBarView.StatusIconType int iconType, boolean show) {
-        showIcon(iconType, show, null);
+    private void showIcon(@DreamOverlayStatusBarView.StatusIconType int iconType, boolean show,
+            int contentDescriptionResId) {
+        showIcon(iconType, show, mResources.getString(contentDescriptionResId));
     }
 
     private void showIcon(
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/callbacks/DreamStatusBarStateCallback.java b/packages/SystemUI/src/com/android/systemui/dreams/callbacks/DreamStatusBarStateCallback.java
new file mode 100644
index 0000000..c8c9470
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/callbacks/DreamStatusBarStateCallback.java
@@ -0,0 +1,46 @@
+/*
+ * 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.dreams.callbacks;
+
+import android.util.Log;
+
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+
+import javax.inject.Inject;
+
+/**
+ * A callback that informs {@link SysuiStatusBarStateController} when the dream state has changed.
+ */
+public class DreamStatusBarStateCallback implements Monitor.Callback {
+    private static final String TAG = "DreamStatusBarCallback";
+
+    private final SysuiStatusBarStateController mStateController;
+
+    @Inject
+    public DreamStatusBarStateCallback(SysuiStatusBarStateController statusBarStateController) {
+        mStateController = statusBarStateController;
+    }
+
+    @Override
+    public void onConditionsChanged(boolean allConditionsMet) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "onConditionChanged:" + allConditionsMet);
+        }
+
+        mStateController.setIsDreaming(allConditionsMet);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
index ee2f1af..244212b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
@@ -16,27 +16,31 @@
 
 package com.android.systemui.dreams.complication;
 
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_PRETEXT_MONITOR;
+
 import android.database.ContentObserver;
 import android.os.UserHandle;
 import android.provider.Settings;
 
 import com.android.settingslib.dream.DreamBackend;
-import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
 import com.android.systemui.util.settings.SecureSettings;
 
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * {@link ComplicationTypesUpdater} observes the state of available complication types set by the
  * user, and pushes updates to {@link DreamOverlayStateController}.
  */
 @SysUISingleton
-public class ComplicationTypesUpdater implements CoreStartable {
+public class ComplicationTypesUpdater extends ConditionalCoreStartable {
     private final DreamBackend mDreamBackend;
     private final Executor mExecutor;
     private final SecureSettings mSecureSettings;
@@ -48,7 +52,9 @@
             DreamBackend dreamBackend,
             @Main Executor executor,
             SecureSettings secureSettings,
-            DreamOverlayStateController dreamOverlayStateController) {
+            DreamOverlayStateController dreamOverlayStateController,
+            @Named(DREAM_PRETEXT_MONITOR) Monitor monitor) {
+        super(monitor);
         mDreamBackend = dreamBackend;
         mExecutor = executor;
         mSecureSettings = secureSettings;
@@ -56,7 +62,7 @@
     }
 
     @Override
-    public void start() {
+    public void onStart() {
         final ContentObserver settingsObserver = new ContentObserver(null /*handler*/) {
             @Override
             public void onChange(boolean selfChange) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
index 77e1fc9..bb1e6e2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
@@ -18,11 +18,14 @@
 
 import static com.android.systemui.dreams.complication.dagger.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW;
 import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS;
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_PRETEXT_MONITOR;
 
 import android.view.View;
 
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -60,7 +63,7 @@
      * {@link CoreStartable} responsible for registering {@link DreamClockTimeComplication} with
      * SystemUI.
      */
-    public static class Registrant implements CoreStartable {
+    public static class Registrant extends ConditionalCoreStartable {
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final DreamClockTimeComplication mComplication;
 
@@ -70,13 +73,15 @@
         @Inject
         public Registrant(
                 DreamOverlayStateController dreamOverlayStateController,
-                DreamClockTimeComplication dreamClockTimeComplication) {
+                DreamClockTimeComplication dreamClockTimeComplication,
+                @Named(DREAM_PRETEXT_MONITOR) Monitor monitor) {
+            super(monitor);
             mDreamOverlayStateController = dreamOverlayStateController;
             mComplication = dreamClockTimeComplication;
         }
 
         @Override
-        public void start() {
+        public void onStart() {
             mDreamOverlayStateController.addComplication(mComplication);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 1065b94..7f395d8 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -21,6 +21,7 @@
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.UNAVAILABLE;
 import static com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_VIEW;
 import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS;
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_PRETEXT_MONITOR;
 
 import android.content.Context;
 import android.content.Intent;
@@ -42,7 +43,9 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.shared.condition.Monitor;
 import com.android.systemui.util.ViewController;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
 
 import java.util.List;
 
@@ -75,7 +78,7 @@
     /**
      * {@link CoreStartable} for registering the complication with SystemUI on startup.
      */
-    public static class Registrant implements CoreStartable {
+    public static class Registrant extends ConditionalCoreStartable {
         private final DreamHomeControlsComplication mComplication;
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final ControlsComponent mControlsComponent;
@@ -105,14 +108,16 @@
         @Inject
         public Registrant(DreamHomeControlsComplication complication,
                 DreamOverlayStateController dreamOverlayStateController,
-                ControlsComponent controlsComponent) {
+                ControlsComponent controlsComponent,
+                @Named(DREAM_PRETEXT_MONITOR) Monitor monitor) {
+            super(monitor);
             mComplication = complication;
             mControlsComponent = controlsComponent;
             mDreamOverlayStateController = dreamOverlayStateController;
         }
 
         @Override
-        public void start() {
+        public void onStart() {
             mControlsComponent.getControlsListingController().ifPresent(
                     c -> c.addCallback(mControlsCallback));
             mDreamOverlayStateController.addCallback(mOverlayStateCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
index c3aaf0c..ff1f312 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
@@ -17,6 +17,7 @@
 package com.android.systemui.dreams.complication;
 
 import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_SMARTSPACE_LAYOUT_PARAMS;
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_PRETEXT_MONITOR;
 
 import android.content.Context;
 import android.os.Parcelable;
@@ -27,7 +28,11 @@
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
 
 import java.util.List;
 
@@ -61,10 +66,11 @@
      * {@link CoreStartable} responsbile for registering {@link SmartSpaceComplication} with
      * SystemUI.
      */
-    public static class Registrant implements CoreStartable {
+    public static class Registrant extends ConditionalCoreStartable {
         private final DreamSmartspaceController mSmartSpaceController;
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final SmartSpaceComplication mComplication;
+        private final FeatureFlags mFeatureFlags;
 
         private final BcSmartspaceDataPlugin.SmartspaceTargetListener mSmartspaceListener =
                 new BcSmartspaceDataPlugin.SmartspaceTargetListener() {
@@ -81,14 +87,22 @@
         public Registrant(
                 DreamOverlayStateController dreamOverlayStateController,
                 SmartSpaceComplication smartSpaceComplication,
-                DreamSmartspaceController smartSpaceController) {
+                DreamSmartspaceController smartSpaceController,
+                @Named(DREAM_PRETEXT_MONITOR) Monitor monitor,
+                FeatureFlags featureFlags) {
+            super(monitor);
             mDreamOverlayStateController = dreamOverlayStateController;
             mComplication = smartSpaceComplication;
             mSmartSpaceController = smartSpaceController;
+            mFeatureFlags = featureFlags;
         }
 
         @Override
-        public void start() {
+        public void onStart() {
+            if (mFeatureFlags.isEnabled(Flags.HIDE_SMARTSPACE_ON_DREAM_OVERLAY)) {
+                return;
+            }
+
             mDreamOverlayStateController.addCallback(new DreamOverlayStateController.Callback() {
                 @Override
                 public void onStateChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java
new file mode 100644
index 0000000..2befce7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java
@@ -0,0 +1,73 @@
+/*
+ * 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.dreams.conditions;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.text.TextUtils;
+
+import com.android.systemui.shared.condition.Condition;
+
+import javax.inject.Inject;
+
+/**
+ * {@link DreamCondition} provides a signal when a dream begins and ends.
+ */
+public class DreamCondition extends Condition {
+    private final Context mContext;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            processIntent(intent);
+        }
+    };
+
+    @Inject
+    public DreamCondition(Context context) {
+        mContext = context;
+    }
+
+    private void processIntent(Intent intent) {
+        // In the case of a non-existent sticky broadcast, ignore when there is no intent.
+        if (intent == null) {
+            return;
+        }
+        if (TextUtils.equals(intent.getAction(), Intent.ACTION_DREAMING_STARTED)) {
+            updateCondition(true);
+        } else if (TextUtils.equals(intent.getAction(), Intent.ACTION_DREAMING_STOPPED)) {
+            updateCondition(false);
+        } else {
+            throw new IllegalStateException("unexpected intent:" + intent);
+        }
+    }
+
+    @Override
+    protected void start() {
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_DREAMING_STARTED);
+        filter.addAction(Intent.ACTION_DREAMING_STOPPED);
+        final Intent stickyIntent = mContext.registerReceiver(mReceiver, filter);
+        processIntent(stickyIntent);
+    }
+
+    @Override
+    protected void stop() {
+        mContext.unregisterReceiver(mReceiver);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index e7b29bb..93a5754 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -23,18 +23,27 @@
 
 import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.settingslib.dream.DreamBackend;
+import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
 import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule;
+import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule;
+import com.android.systemui.process.condition.SystemProcessCondition;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.shared.condition.Monitor;
 
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
 
 import javax.inject.Named;
 
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.IntoSet;
 
 /**
  * Dagger Module providing Dream-related functionality.
@@ -42,6 +51,7 @@
 @Module(includes = {
             RegisteredComplicationsModule.class,
             LowLightDreamModule.class,
+            ScrimModule.class
         },
         subcomponents = {
             DreamOverlayComponent.class,
@@ -52,6 +62,10 @@
     String DREAM_OVERLAY_ENABLED = "dream_overlay_enabled";
 
     String DREAM_SUPPORTED = "dream_supported";
+    String DREAM_PRETEXT_CONDITIONS = "dream_pretext_conditions";
+    String DREAM_PRETEXT_MONITOR = "dream_prtext_monitor";
+    String DREAM_OVERLAY_WINDOW_TITLE = "dream_overlay_window_title";
+
 
     /**
      * Provides the dream component
@@ -110,4 +124,26 @@
     static boolean providesDreamSupported(@Main Resources resources) {
         return resources.getBoolean(com.android.internal.R.bool.config_dreamsSupported);
     }
+
+    /** */
+    @Binds
+    @IntoSet
+    @Named(DREAM_PRETEXT_CONDITIONS)
+    Condition bindSystemProcessCondition(SystemProcessCondition condition);
+
+    /** */
+    @Provides
+    @Named(DREAM_PRETEXT_MONITOR)
+    static Monitor providesDockerPretextMonitor(
+            @Main Executor executor,
+            @Named(DREAM_PRETEXT_CONDITIONS) Set<Condition> pretextConditions) {
+        return new Monitor(executor, pretextConditions);
+    }
+
+    /** */
+    @Provides
+    @Named(DREAM_OVERLAY_WINDOW_TITLE)
+    static String providesDreamOverlayWindowTitle(@Main Resources resources) {
+        return resources.getString(R.string.app_label);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
index 92cdcf9..73c2289 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -36,11 +36,12 @@
 
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.dreams.touch.scrim.ScrimController;
+import com.android.systemui.dreams.touch.scrim.ScrimManager;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import java.util.Optional;
@@ -78,7 +79,8 @@
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final float mBouncerZoneScreenPercentage;
 
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final ScrimManager mScrimManager;
+    private ScrimController mCurrentScrimController;
     private float mCurrentExpansion;
     private final Optional<CentralSurfaces> mCentralSurfaces;
 
@@ -90,6 +92,7 @@
     private final DisplayMetrics mDisplayMetrics;
 
     private Boolean mCapture;
+    private Boolean mExpanded;
 
     private boolean mBouncerInitiallyShowing;
 
@@ -101,6 +104,17 @@
 
     private final UiEventLogger mUiEventLogger;
 
+    private final ScrimManager.Callback mScrimManagerCallback = new ScrimManager.Callback() {
+        @Override
+        public void onScrimControllerChanged(ScrimController controller) {
+            if (mCurrentScrimController != null) {
+                mCurrentScrimController.reset();
+            }
+
+            mCurrentScrimController = controller;
+        }
+    };
+
     private final GestureDetector.OnGestureListener mOnGestureListener =
             new GestureDetector.SimpleOnGestureListener() {
                 @Override
@@ -115,8 +129,10 @@
                                 .orElse(false);
 
                         if (mCapture) {
+                            // reset expanding
+                            mExpanded = false;
                             // Since the user is dragging the bouncer up, set scrimmed to false.
-                            mStatusBarKeyguardViewManager.showPrimaryBouncer(false);
+                            mCurrentScrimController.show();
                         }
                     }
 
@@ -157,10 +173,10 @@
         ShadeExpansionChangeEvent event =
                 new ShadeExpansionChangeEvent(
                         /* fraction= */ mCurrentExpansion,
-                        /* expanded= */ false,
+                        /* expanded= */ mExpanded,
                         /* tracking= */ true,
                         /* dragDownPxAmount= */ dragDownAmount);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(event);
+        mCurrentScrimController.expand(event);
     }
 
 
@@ -187,7 +203,7 @@
     @Inject
     public BouncerSwipeTouchHandler(
             DisplayMetrics displayMetrics,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            ScrimManager scrimManager,
             Optional<CentralSurfaces> centralSurfaces,
             NotificationShadeWindowController notificationShadeWindowController,
             ValueAnimatorCreator valueAnimatorCreator,
@@ -200,7 +216,7 @@
             UiEventLogger uiEventLogger) {
         mDisplayMetrics = displayMetrics;
         mCentralSurfaces = centralSurfaces;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mScrimManager = scrimManager;
         mNotificationShadeWindowController = notificationShadeWindowController;
         mBouncerZoneScreenPercentage = swipeRegionPercentage;
         mFlingAnimationUtils = flingAnimationUtils;
@@ -234,9 +250,12 @@
         mTouchSession = session;
         mVelocityTracker.clear();
         mNotificationShadeWindowController.setForcePluginOpen(true, this);
+        mScrimManager.addCallback(mScrimManagerCallback);
+        mCurrentScrimController = mScrimManager.getCurrentController();
 
         session.registerCallback(() -> {
             mVelocityTracker.recycle();
+            mScrimManager.removeCallback(mScrimManagerCallback);
             mCapture = null;
             mNotificationShadeWindowController.setForcePluginOpen(false, this);
         });
@@ -273,18 +292,21 @@
                 final float velocityVector =
                         (float) Math.hypot(horizontalVelocity, verticalVelocity);
 
-                final float expansion = flingRevealsOverlay(verticalVelocity, velocityVector)
-                        ? KeyguardBouncer.EXPANSION_HIDDEN : KeyguardBouncer.EXPANSION_VISIBLE;
+                mExpanded = !flingRevealsOverlay(verticalVelocity, velocityVector);
+                final float expansion = mExpanded
+                        ? KeyguardBouncerConstants.EXPANSION_VISIBLE
+                        : KeyguardBouncerConstants.EXPANSION_HIDDEN;
 
                 // Log the swiping up to show Bouncer event.
-                if (!mBouncerInitiallyShowing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
+                if (!mBouncerInitiallyShowing
+                        && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
                     mUiEventLogger.log(DreamEvent.DREAM_SWIPED);
                 }
 
                 flingToExpansion(verticalVelocity, expansion);
 
-                if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
-                    mStatusBarKeyguardViewManager.reset(false);
+                if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
+                    mCurrentScrimController.reset();
                 }
                 break;
             default:
@@ -302,7 +324,8 @@
                     float dragDownAmount = expansionFraction * expansionHeight;
                     setPanelExpansion(expansionFraction, dragDownAmount);
                 });
-        if (!mBouncerInitiallyShowing && targetExpansion == KeyguardBouncer.EXPANSION_VISIBLE) {
+        if (!mBouncerInitiallyShowing
+                && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
             animator.addListener(
                     new AnimatorListenerAdapter() {
                         @Override
@@ -335,7 +358,7 @@
         final float targetHeight = viewHeight * expansion;
         final float expansionHeight = targetHeight - currentHeight;
         final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight);
-        if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
+        if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
             // Hides the bouncer, i.e., fully expands the space above the bouncer.
             mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity,
                     viewHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
index 695b59a..b8b459e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
@@ -226,6 +226,15 @@
             return;
         }
 
+        // When we stop monitoring touches, we must ensure that all active touch sessions and
+        // descendants informed of the removal so any cleanup for active tracking can proceed.
+        mExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> {
+            while (touchSession != null) {
+                touchSession.onRemoved();
+                touchSession = touchSession.getPredecessor();
+            }
+        }));
+
         mCurrentInputSession.dispose();
         mCurrentInputSession = null;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
index 4382757..e1d0339 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
@@ -21,10 +21,10 @@
 
 import android.os.Looper;
 import android.view.Choreographer;
-import android.view.Display;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
@@ -55,8 +55,9 @@
     public InputSession(@Named(INPUT_SESSION_NAME) String sessionName,
             InputChannelCompat.InputEventListener inputEventListener,
             GestureDetector.OnGestureListener gestureListener,
+            DisplayTracker displayTracker,
             @Named(PILFER_ON_GESTURE_CONSUME) boolean pilferOnGestureConsume) {
-        mInputMonitor = new InputMonitorCompat(sessionName, Display.DEFAULT_DISPLAY);
+        mInputMonitor = new InputMonitorCompat(sessionName, displayTracker.getDefaultDisplayId());
         mGestureDetector = new GestureDetector(gestureListener);
 
         mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(),
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerScrimController.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerScrimController.java
new file mode 100644
index 0000000..776b7bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerScrimController.java
@@ -0,0 +1,49 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+
+import javax.inject.Inject;
+
+/**
+ * Implementation for handling swipe movements on the overlay when the keyguard is present.
+ */
+public class BouncerScrimController implements ScrimController {
+    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+
+    @Inject
+    BouncerScrimController(StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+    }
+
+    @Override
+    public void show() {
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(false);
+    }
+
+    @Override
+    public void expand(ShadeExpansionChangeEvent event) {
+        mStatusBarKeyguardViewManager.onPanelExpansionChanged(event);
+    }
+
+    @Override
+    public void reset() {
+        mStatusBarKeyguardViewManager.reset(false);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimController.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimController.java
new file mode 100644
index 0000000..01e4d04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimController.java
@@ -0,0 +1,91 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import android.os.PowerManager;
+import android.os.SystemClock;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.unfold.util.CallbackController;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/**
+ * {@link BouncerlessScrimController} handles scrim progression when no keyguard is set. When
+ * fully expanded, the controller dismisses the dream.
+ */
+@SysUISingleton
+public class BouncerlessScrimController implements ScrimController,
+        CallbackController<BouncerlessScrimController.Callback> {
+    private static final String TAG = "BLScrimController";
+
+    /**
+     * {@link Callback} allows {@link BouncerlessScrimController} clients to be informed of
+     * expansion progression and wakeup
+     */
+    public interface Callback {
+        /**
+         * Invoked when there is a change to the scrim expansion.
+         */
+        void onExpansion(ShadeExpansionChangeEvent event);
+
+        /**
+         * Invoked after {@link BouncerlessScrimController} has started waking up the device.
+         */
+        void onWakeup();
+    }
+
+    private final Executor mExecutor;
+    private final PowerManager mPowerManager;
+
+    @Override
+    public void addCallback(Callback listener) {
+        mExecutor.execute(() -> mCallbacks.add(listener));
+    }
+
+    @Override
+    public void removeCallback(Callback listener) {
+        mExecutor.execute(() -> mCallbacks.remove(listener));
+    }
+
+    private final HashSet<Callback> mCallbacks;
+
+
+    @Inject
+    public BouncerlessScrimController(@Main Executor executor,
+            PowerManager powerManager) {
+        mExecutor = executor;
+        mPowerManager = powerManager;
+        mCallbacks = new HashSet<>();
+    }
+
+    @Override
+    public void expand(ShadeExpansionChangeEvent event) {
+        if (event.getExpanded())  {
+            mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+                    "com.android.systemui:SwipeUp");
+            mExecutor.execute(() -> mCallbacks.forEach(callback -> callback.onWakeup()));
+        } else {
+            mExecutor.execute(() -> mCallbacks.forEach(callback -> callback.onExpansion(event)));
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimController.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimController.java
new file mode 100644
index 0000000..61629ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimController.java
@@ -0,0 +1,44 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+
+/**
+ * {@link ScrimController} provides an interface for the different consumers of scrolling/expansion
+ * events over the dream.
+ */
+public interface ScrimController {
+    /**
+     * Called at the start of expansion before any expansion amount updates.
+     */
+    default void show() {
+    }
+
+    /**
+     * Called for every expansion update.
+     * @param event {@link ShadeExpansionChangeEvent} detailing the change.
+     */
+    default void expand(ShadeExpansionChangeEvent event) {
+    }
+
+    /**
+     * Called at the end of the movement.
+     */
+    default void reset() {
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimManager.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimManager.java
new file mode 100644
index 0000000..0d0dff6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimManager.java
@@ -0,0 +1,112 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import static com.android.systemui.dreams.touch.scrim.dagger.ScrimModule.BOUNCERLESS_SCRIM_CONTROLLER;
+import static com.android.systemui.dreams.touch.scrim.dagger.ScrimModule.BOUNCER_SCRIM_CONTROLLER;
+
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * {@link ScrimManager} helps manage multiple {@link ScrimController} instances, specifying the
+ * appropriate one to use at the current moment and managing the handoff between controllers.
+ */
+public class ScrimManager {
+    private final ScrimController mBouncerScrimController;
+    private final ScrimController mBouncerlessScrimController;
+    private final KeyguardStateController mKeyguardStateController;
+    private final Executor mExecutor;
+
+    private ScrimController mCurrentController;
+    private final HashSet<Callback> mCallbacks;
+
+    /**
+     * Interface implemented for receiving updates to the active {@link ScrimController}.
+     */
+    public interface Callback {
+        /**
+         * Invoked when the controller changes.
+         * @param controller The currently active {@link ScrimController}.
+         */
+        void onScrimControllerChanged(ScrimController controller);
+    }
+
+    private final KeyguardStateController.Callback mKeyguardStateCallback =
+            new KeyguardStateController.Callback() {
+                @Override
+                public void onKeyguardShowingChanged() {
+                    mExecutor.execute(() -> updateController());
+                }
+            };
+
+    @Inject
+    ScrimManager(@Main Executor executor,
+            @Named(BOUNCER_SCRIM_CONTROLLER) ScrimController bouncerScrimController,
+            @Named(BOUNCERLESS_SCRIM_CONTROLLER)ScrimController bouncerlessScrimController,
+            KeyguardStateController keyguardStateController) {
+        mExecutor = executor;
+        mCallbacks = new HashSet<>();
+        mBouncerlessScrimController = bouncerlessScrimController;
+        mBouncerScrimController = bouncerScrimController;
+        mKeyguardStateController = keyguardStateController;
+
+        mKeyguardStateController.addCallback(mKeyguardStateCallback);
+        updateController();
+    }
+
+    private void updateController() {
+        final ScrimController existingController = mCurrentController;
+        mCurrentController =  mKeyguardStateController.canDismissLockScreen()
+                ? mBouncerlessScrimController
+                : mBouncerScrimController;
+
+        if (existingController == mCurrentController) {
+            return;
+        }
+
+        mCallbacks.forEach(callback -> callback.onScrimControllerChanged(mCurrentController));
+    }
+
+    /**
+     * Adds a {@link Callback} to receive future changes to the active {@link ScrimController}.
+     */
+    public void addCallback(Callback callback) {
+        mExecutor.execute(() -> mCallbacks.add(callback));
+    }
+
+    /**
+     * Removes the {@link Callback} from receiving further updates.
+     */
+    public void removeCallback(Callback callback) {
+        mExecutor.execute(() -> mCallbacks.remove(callback));
+    }
+
+    /**
+     * Returns the currently get {@link ScrimController}.
+     * @return
+     */
+    public ScrimController getCurrentController() {
+        return mCurrentController;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/dagger/ScrimModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/dagger/ScrimModule.java
new file mode 100644
index 0000000..40bc0ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/dagger/ScrimModule.java
@@ -0,0 +1,51 @@
+/*
+ * 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.dreams.touch.scrim.dagger;
+
+import com.android.systemui.dreams.touch.scrim.BouncerScrimController;
+import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
+import com.android.systemui.dreams.touch.scrim.ScrimController;
+
+import javax.inject.Named;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Module for scrim related dependencies.
+ */
+@Module
+public interface ScrimModule {
+    String BOUNCERLESS_SCRIM_CONTROLLER = "bouncerless_scrim_controller";
+    String BOUNCER_SCRIM_CONTROLLER = "bouncer_scrim_controller";
+
+    /** */
+    @Provides
+    @Named(BOUNCERLESS_SCRIM_CONTROLLER)
+    static ScrimController providesBouncerlessScrimController(
+            BouncerlessScrimController controller) {
+        return controller;
+    }
+
+    /** */
+    @Provides
+    @Named(BOUNCER_SCRIM_CONTROLLER)
+    static ScrimController providesBouncerScrimController(
+            BouncerScrimController controller) {
+        return controller;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index d1a14a1..58fe094 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -19,7 +19,7 @@
 import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS;
 import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG;
 import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS;
-import static com.android.systemui.flags.FlagManager.EXTRA_ID;
+import static com.android.systemui.flags.FlagManager.EXTRA_NAME;
 import static com.android.systemui.flags.FlagManager.EXTRA_VALUE;
 import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS;
 
@@ -39,7 +39,7 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.GlobalSettings;
 
 import org.jetbrains.annotations.NotNull;
 
@@ -72,21 +72,22 @@
 
     private final FlagManager mFlagManager;
     private final Context mContext;
-    private final SecureSettings mSecureSettings;
+    private final GlobalSettings mGlobalSettings;
     private final Resources mResources;
     private final SystemPropertiesHelper mSystemProperties;
     private final ServerFlagReader mServerFlagReader;
-    private final Map<Integer, Flag<?>> mAllFlags;
-    private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>();
-    private final Map<Integer, String> mStringFlagCache = new TreeMap<>();
-    private final Map<Integer, Integer> mIntFlagCache = new TreeMap<>();
+    private final Map<String, Flag<?>> mAllFlags;
+    private final Map<String, Boolean> mBooleanFlagCache = new TreeMap<>();
+    private final Map<String, String> mStringFlagCache = new TreeMap<>();
+    private final Map<String, Integer> mIntFlagCache = new TreeMap<>();
     private final Restarter mRestarter;
 
     private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
             new ServerFlagReader.ChangeListener() {
                 @Override
-                public void onChange() {
-                    mRestarter.restartSystemUI();
+                public void onChange(Flag<?> flag) {
+                    mRestarter.restartSystemUI(
+                            "Server flag change: " + flag.getNamespace() + "." + flag.getName());
                 }
             };
 
@@ -94,15 +95,15 @@
     public FeatureFlagsDebug(
             FlagManager flagManager,
             Context context,
-            SecureSettings secureSettings,
+            GlobalSettings globalSettings,
             SystemPropertiesHelper systemProperties,
             @Main Resources resources,
             ServerFlagReader serverFlagReader,
-            @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
+            @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags,
             Restarter restarter) {
         mFlagManager = flagManager;
         mContext = context;
-        mSecureSettings = secureSettings;
+        mGlobalSettings = globalSettings;
         mResources = resources;
         mSystemProperties = systemProperties;
         mServerFlagReader = serverFlagReader;
@@ -115,7 +116,8 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_SET_FLAG);
         filter.addAction(ACTION_GET_FLAGS);
-        mFlagManager.setOnSettingsChangedAction(this::restartSystemUI);
+        mFlagManager.setOnSettingsChangedAction(
+                suppressRestart -> restartSystemUI(suppressRestart, "Settings changed"));
         mFlagManager.setClearCacheAction(this::removeFromCache);
         mContext.registerReceiver(mReceiver, filter, null, null,
                 Context.RECEIVER_EXPORTED_UNAUDITED);
@@ -133,96 +135,107 @@
     }
 
     private boolean isEnabledInternal(@NotNull BooleanFlag flag) {
-        int id = flag.getId();
-        if (!mBooleanFlagCache.containsKey(id)) {
-            mBooleanFlagCache.put(id,
+        String name = flag.getName();
+        if (!mBooleanFlagCache.containsKey(name)) {
+            mBooleanFlagCache.put(name,
                     readBooleanFlagInternal(flag, flag.getDefault()));
         }
 
-        return mBooleanFlagCache.get(id);
+        return mBooleanFlagCache.get(name);
     }
 
     @Override
     public boolean isEnabled(@NonNull ResourceBooleanFlag flag) {
-        int id = flag.getId();
-        if (!mBooleanFlagCache.containsKey(id)) {
-            mBooleanFlagCache.put(id,
+        String name = flag.getName();
+        if (!mBooleanFlagCache.containsKey(name)) {
+            mBooleanFlagCache.put(name,
                     readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId())));
         }
 
-        return mBooleanFlagCache.get(id);
+        return mBooleanFlagCache.get(name);
     }
 
     @Override
     public boolean isEnabled(@NonNull SysPropBooleanFlag flag) {
-        int id = flag.getId();
-        if (!mBooleanFlagCache.containsKey(id)) {
+        String name = flag.getName();
+        if (!mBooleanFlagCache.containsKey(name)) {
             // Use #readFlagValue to get the default. That will allow it to fall through to
             // teamfood if need be.
             mBooleanFlagCache.put(
-                    id,
+                    name,
                     mSystemProperties.getBoolean(
                             flag.getName(),
                             readBooleanFlagInternal(flag, flag.getDefault())));
         }
 
-        return mBooleanFlagCache.get(id);
+        return mBooleanFlagCache.get(name);
     }
 
     @NonNull
     @Override
     public String getString(@NonNull StringFlag flag) {
-        int id = flag.getId();
-        if (!mStringFlagCache.containsKey(id)) {
-            mStringFlagCache.put(id,
-                    readFlagValueInternal(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
+        String name = flag.getName();
+        if (!mStringFlagCache.containsKey(name)) {
+            mStringFlagCache.put(name,
+                    readFlagValueInternal(
+                            flag.getId(), name, flag.getDefault(), StringFlagSerializer.INSTANCE));
         }
 
-        return mStringFlagCache.get(id);
+        return mStringFlagCache.get(name);
     }
 
     @NonNull
     @Override
     public String getString(@NonNull ResourceStringFlag flag) {
-        int id = flag.getId();
-        if (!mStringFlagCache.containsKey(id)) {
-            mStringFlagCache.put(id,
-                    readFlagValueInternal(id, mResources.getString(flag.getResourceId()),
+        String name = flag.getName();
+        if (!mStringFlagCache.containsKey(name)) {
+            mStringFlagCache.put(name,
+                    readFlagValueInternal(
+                            flag.getId(), name, mResources.getString(flag.getResourceId()),
                             StringFlagSerializer.INSTANCE));
         }
 
-        return mStringFlagCache.get(id);
+        return mStringFlagCache.get(name);
     }
 
 
     @NonNull
     @Override
     public int getInt(@NonNull IntFlag flag) {
-        int id = flag.getId();
-        if (!mIntFlagCache.containsKey(id)) {
-            mIntFlagCache.put(id,
-                    readFlagValueInternal(id, flag.getDefault(), IntFlagSerializer.INSTANCE));
+        String name = flag.getName();
+        if (!mIntFlagCache.containsKey(name)) {
+            mIntFlagCache.put(name,
+                    readFlagValueInternal(
+                            flag.getId(), name, flag.getDefault(), IntFlagSerializer.INSTANCE));
         }
 
-        return mIntFlagCache.get(id);
+        return mIntFlagCache.get(name);
     }
 
     @NonNull
     @Override
     public int getInt(@NonNull ResourceIntFlag flag) {
-        int id = flag.getId();
-        if (!mIntFlagCache.containsKey(id)) {
-            mIntFlagCache.put(id,
-                    readFlagValueInternal(id, mResources.getInteger(flag.getResourceId()),
+        String name = flag.getName();
+        if (!mIntFlagCache.containsKey(name)) {
+            mIntFlagCache.put(name,
+                    readFlagValueInternal(
+                            flag.getId(), name, mResources.getInteger(flag.getResourceId()),
                             IntFlagSerializer.INSTANCE));
         }
 
-        return mIntFlagCache.get(id);
+        return mIntFlagCache.get(name);
     }
 
     /** Specific override for Boolean flags that checks against the teamfood list.*/
     private boolean readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue) {
-        Boolean result = readBooleanFlagOverride(flag.getId());
+        Boolean result = readBooleanFlagOverride(flag.getName());
+        if (result == null) {
+            result = readBooleanFlagOverride(flag.getId());
+            if (result != null) {
+                // Move overrides from id to name
+                setFlagValueInternal(flag.getName(), result, BooleanFlagSerializer.INSTANCE);
+            }
+        }
         boolean hasServerOverride = mServerFlagReader.hasOverride(
                 flag.getNamespace(), flag.getName());
 
@@ -231,7 +244,7 @@
         if (!hasServerOverride
                 && !defaultValue
                 && result == null
-                && flag.getId() != Flags.TEAMFOOD.getId()
+                && !flag.getName().equals(Flags.TEAMFOOD.getName())
                 && flag.getTeamfood()) {
             return isEnabled(Flags.TEAMFOOD);
         }
@@ -244,16 +257,31 @@
         return readFlagValueInternal(id, BooleanFlagSerializer.INSTANCE);
     }
 
+    private Boolean readBooleanFlagOverride(String name) {
+        return readFlagValueInternal(name, BooleanFlagSerializer.INSTANCE);
+    }
+
+    // TODO(b/265188950): Remove id from this method once ids are fully deprecated.
     @NonNull
     private <T> T readFlagValueInternal(
-            int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
+            int id, String name, @NonNull T defaultValue, FlagSerializer<T> serializer) {
         requireNonNull(defaultValue, "defaultValue");
-        T result = readFlagValueInternal(id, serializer);
-        return result == null ? defaultValue : result;
+        T resultForName = readFlagValueInternal(name, serializer);
+        if (resultForName == null) {
+            T resultForId = readFlagValueInternal(id, serializer);
+            if (resultForId == null) {
+                return defaultValue;
+            } else {
+                setFlagValue(name, resultForId, serializer);
+                return resultForId;
+            }
+        }
+        return resultForName;
     }
 
 
     /** Returns the stored value or null if not set. */
+    // TODO(b/265188950): Remove method this once ids are fully deprecated.
     @Nullable
     private <T> T readFlagValueInternal(int id, FlagSerializer<T> serializer) {
         try {
@@ -264,51 +292,87 @@
         return null;
     }
 
-    private <T> void setFlagValue(int id, @NonNull T value, FlagSerializer<T> serializer) {
+    /** Returns the stored value or null if not set. */
+    @Nullable
+    private <T> T readFlagValueInternal(String name, FlagSerializer<T> serializer) {
+        try {
+            return mFlagManager.readFlagValue(name, serializer);
+        } catch (Exception e) {
+            eraseInternal(name);
+        }
+        return null;
+    }
+
+    private <T> void setFlagValue(String name, @NonNull T value, FlagSerializer<T> serializer) {
         requireNonNull(value, "Cannot set a null value");
-        T currentValue = readFlagValueInternal(id, serializer);
+        T currentValue = readFlagValueInternal(name, serializer);
         if (Objects.equals(currentValue, value)) {
-            Log.i(TAG, "Flag id " + id + " is already " + value);
+            Log.i(TAG, "Flag \"" + name + "\" is already " + value);
             return;
         }
+        setFlagValueInternal(name, value, serializer);
+        Log.i(TAG, "Set flag \"" + name + "\" to " + value);
+        removeFromCache(name);
+        mFlagManager.dispatchListenersAndMaybeRestart(
+                name,
+                suppressRestart -> restartSystemUI(
+                        suppressRestart, "Flag \"" + name + "\" changed to " + value));
+    }
+
+    private <T> void setFlagValueInternal(
+            String name, @NonNull T value, FlagSerializer<T> serializer) {
         final String data = serializer.toSettingsData(value);
         if (data == null) {
-            Log.w(TAG, "Failed to set id " + id + " to " + value);
+            Log.w(TAG, "Failed to set flag " + name + " to " + value);
             return;
         }
-        mSecureSettings.putStringForUser(mFlagManager.idToSettingsKey(id), data,
+        mGlobalSettings.putStringForUser(mFlagManager.nameToSettingsKey(name), data,
                 UserHandle.USER_CURRENT);
-        Log.i(TAG, "Set id " + id + " to " + value);
-        removeFromCache(id);
-        mFlagManager.dispatchListenersAndMaybeRestart(id, this::restartSystemUI);
     }
 
     <T> void eraseFlag(Flag<T> flag) {
         if (flag instanceof SysPropFlag) {
-            mSystemProperties.erase(((SysPropFlag<T>) flag).getName());
-            dispatchListenersAndMaybeRestart(flag.getId(), this::restartAndroid);
+            mSystemProperties.erase(flag.getName());
+            dispatchListenersAndMaybeRestart(
+                    flag.getName(),
+                    suppressRestart -> restartSystemUI(
+                            suppressRestart,
+                            "SysProp Flag \"" + flag.getNamespace() + "."
+                                    + flag.getName() + "\" reset to default."));
         } else {
-            eraseFlag(flag.getId());
+            eraseFlag(flag.getName());
         }
     }
 
     /** Erase a flag's overridden value if there is one. */
-    private void eraseFlag(int id) {
-        eraseInternal(id);
-        removeFromCache(id);
-        dispatchListenersAndMaybeRestart(id, this::restartSystemUI);
+    private void eraseFlag(String name) {
+        eraseInternal(name);
+        removeFromCache(name);
+        dispatchListenersAndMaybeRestart(
+                name,
+                suppressRestart -> restartSystemUI(
+                        suppressRestart, "Flag \"" + name + "\" reset to default"));
     }
 
-    private void dispatchListenersAndMaybeRestart(int id, Consumer<Boolean> restartAction) {
-        mFlagManager.dispatchListenersAndMaybeRestart(id, restartAction);
+    private void dispatchListenersAndMaybeRestart(String name, Consumer<Boolean> restartAction) {
+        mFlagManager.dispatchListenersAndMaybeRestart(name, restartAction);
     }
 
-    /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
+    /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */
+    // TODO(b/265188950): Remove method this once ids are fully deprecated.
     private void eraseInternal(int id) {
-        // We can't actually "erase" things from sysprops, but we can set them to empty!
-        mSecureSettings.putStringForUser(mFlagManager.idToSettingsKey(id), "",
+        // We can't actually "erase" things from settings, but we can set them to empty!
+        mGlobalSettings.putStringForUser(mFlagManager.idToSettingsKey(id), "",
                 UserHandle.USER_CURRENT);
-        Log.i(TAG, "Erase id " + id);
+        Log.i(TAG, "Erase name " + id);
+    }
+
+    /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */
+    private void eraseInternal(String name) {
+        // We can't actually "erase" things from settings, but we can set them to empty!
+        mGlobalSettings.putStringForUser(mFlagManager.nameToSettingsKey(name), "",
+                UserHandle.USER_CURRENT);
+        Log.i(TAG, "Erase name " + name);
     }
 
     @Override
@@ -321,32 +385,35 @@
         mFlagManager.removeListener(listener);
     }
 
-    private void restartSystemUI(boolean requestSuppress) {
+    private void restartSystemUI(boolean requestSuppress, String reason) {
         if (requestSuppress) {
             Log.i(TAG, "SystemUI Restart Suppressed");
             return;
         }
-        mRestarter.restartSystemUI();
+        mRestarter.restartSystemUI(reason);
     }
 
-    private void restartAndroid(boolean requestSuppress) {
+    private void restartAndroid(boolean requestSuppress, String reason) {
         if (requestSuppress) {
             Log.i(TAG, "Android Restart Suppressed");
             return;
         }
-        mRestarter.restartAndroid();
+        mRestarter.restartAndroid(reason);
     }
 
     void setBooleanFlagInternal(Flag<?> flag, boolean value) {
         if (flag instanceof BooleanFlag) {
-            setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
+            setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE);
         } else if (flag instanceof ResourceBooleanFlag) {
-            setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
+            setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE);
         } else if (flag instanceof SysPropBooleanFlag) {
             // Store SysProp flags in SystemProperties where they can read by outside parties.
             mSystemProperties.setBoolean(((SysPropBooleanFlag) flag).getName(), value);
-            dispatchListenersAndMaybeRestart(flag.getId(),
-                    FeatureFlagsDebug.this::restartAndroid);
+            dispatchListenersAndMaybeRestart(
+                    flag.getName(),
+                    suppressRestart -> restartSystemUI(
+                            suppressRestart,
+                            "Flag \"" + flag.getName() + "\" changed to " + value));
         } else {
             throw new IllegalArgumentException("Unknown flag type");
         }
@@ -354,9 +421,9 @@
 
     void setStringFlagInternal(Flag<?> flag, String value) {
         if (flag instanceof StringFlag) {
-            setFlagValue(flag.getId(), value, StringFlagSerializer.INSTANCE);
+            setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE);
         } else if (flag instanceof ResourceStringFlag) {
-            setFlagValue(flag.getId(), value, StringFlagSerializer.INSTANCE);
+            setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE);
         } else {
             throw new IllegalArgumentException("Unknown flag type");
         }
@@ -364,9 +431,9 @@
 
     void setIntFlagInternal(Flag<?> flag, int value) {
         if (flag instanceof IntFlag) {
-            setFlagValue(flag.getId(), value, IntFlagSerializer.INSTANCE);
+            setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE);
         } else if (flag instanceof ResourceIntFlag) {
-            setFlagValue(flag.getId(), value, IntFlagSerializer.INSTANCE);
+            setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE);
         } else {
             throw new IllegalArgumentException("Unknown flag type");
         }
@@ -405,17 +472,17 @@
                 Log.w(TAG, "No extras");
                 return;
             }
-            int id = extras.getInt(EXTRA_ID);
-            if (id <= 0) {
-                Log.w(TAG, "ID not set or less than  or equal to 0: " + id);
+            String name = extras.getString(EXTRA_NAME);
+            if (name == null || name.isEmpty()) {
+                Log.w(TAG, "NAME not set or is empty: " + name);
                 return;
             }
 
-            if (!mAllFlags.containsKey(id)) {
-                Log.w(TAG, "Tried to set unknown id: " + id);
+            if (!mAllFlags.containsKey(name)) {
+                Log.w(TAG, "Tried to set unknown name: " + name);
                 return;
             }
-            Flag<?> flag = mAllFlags.get(id);
+            Flag<?> flag = mAllFlags.get(name);
 
             if (!extras.containsKey(EXTRA_VALUE)) {
                 eraseFlag(flag);
@@ -452,13 +519,16 @@
 
             if (f instanceof ReleasedFlag) {
                 enabled = isEnabled((ReleasedFlag) f);
-                overridden = readBooleanFlagOverride(f.getId()) != null;
+                overridden = readBooleanFlagOverride(f.getName()) != null
+                            || readBooleanFlagOverride(f.getId()) != null;
             } else if (f instanceof UnreleasedFlag) {
                 enabled = isEnabled((UnreleasedFlag) f);
-                overridden = readBooleanFlagOverride(f.getId()) != null;
+                overridden = readBooleanFlagOverride(f.getName()) != null
+                            || readBooleanFlagOverride(f.getId()) != null;
             } else if (f instanceof ResourceBooleanFlag) {
                 enabled = isEnabled((ResourceBooleanFlag) f);
-                overridden = readBooleanFlagOverride(f.getId()) != null;
+                overridden = readBooleanFlagOverride(f.getName()) != null
+                            || readBooleanFlagOverride(f.getId()) != null;
             } else if (f instanceof SysPropBooleanFlag) {
                 // TODO(b/223379190): Teamfood not supported for sysprop flags yet.
                 enabled = isEnabled((SysPropBooleanFlag) f);
@@ -480,9 +550,9 @@
         }
     };
 
-    private void removeFromCache(int id) {
-        mBooleanFlagCache.remove(id);
-        mStringFlagCache.remove(id);
+    private void removeFromCache(String name) {
+        mBooleanFlagCache.remove(name);
+        mStringFlagCache.remove(name);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
index 069e612..a6956a4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
@@ -29,6 +29,7 @@
 ) : Restarter {
 
     private var androidRestartRequested = false
+    private var pendingReason = ""
 
     val observer =
         object : WakefulnessLifecycle.Observer {
@@ -38,18 +39,20 @@
             }
         }
 
-    override fun restartSystemUI() {
+    override fun restartSystemUI(reason: String) {
         Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting on next screen off.")
-        scheduleRestart()
+        Log.i(FeatureFlagsDebug.TAG, reason)
+        scheduleRestart(reason)
     }
 
-    override fun restartAndroid() {
+    override fun restartAndroid(reason: String) {
         Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting on next screen off.")
         androidRestartRequested = true
-        scheduleRestart()
+        scheduleRestart(reason)
     }
 
-    fun scheduleRestart() {
+    fun scheduleRestart(reason: String) {
+        pendingReason = reason
         if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) {
             restartNow()
         } else {
@@ -59,9 +62,9 @@
 
     private fun restartNow() {
         if (androidRestartRequested) {
-            systemExitRestarter.restartAndroid()
+            systemExitRestarter.restartAndroid(pendingReason)
         } else {
-            systemExitRestarter.restartSystemUI()
+            systemExitRestarter.restartSystemUI(pendingReason)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index b94d781..06ca0ad 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -21,6 +21,8 @@
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.util.InitializationChecker
+import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
@@ -34,7 +36,10 @@
     private val commandRegistry: CommandRegistry,
     private val flagCommand: FlagCommand,
     private val featureFlags: FeatureFlagsDebug,
-    private val broadcastSender: BroadcastSender
+    private val broadcastSender: BroadcastSender,
+    private val initializationChecker: InitializationChecker,
+    private val restartDozeListener: RestartDozeListener,
+    private val delayableExecutor: DelayableExecutor
 ) : CoreStartable {
 
     init {
@@ -46,8 +51,14 @@
     override fun start() {
         featureFlags.init()
         commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand }
-        val intent = Intent(FlagManager.ACTION_SYSUI_STARTED)
-        broadcastSender.sendBroadcast(intent)
+        if (initializationChecker.initializeComponents()) {
+            // protected broadcast should only be sent for the main process
+            val intent = Intent(FlagManager.ACTION_SYSUI_STARTED)
+            broadcastSender.sendBroadcast(intent)
+
+            restartDozeListener.init()
+            delayableExecutor.executeDelayed({ restartDozeListener.maybeRestartSleep() }, 1000)
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index 8bddacc..9859ff6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -21,18 +21,16 @@
 import static java.util.Objects.requireNonNull;
 
 import android.content.res.Resources;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
 
 import androidx.annotation.NonNull;
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.DeviceConfigProxy;
 
 import org.jetbrains.annotations.NotNull;
 
 import java.io.PrintWriter;
+import java.util.HashMap;
 import java.util.Map;
 
 import javax.inject.Inject;
@@ -50,18 +48,18 @@
 
     private final Resources mResources;
     private final SystemPropertiesHelper mSystemProperties;
-    private final DeviceConfigProxy mDeviceConfigProxy;
     private final ServerFlagReader mServerFlagReader;
     private final Restarter mRestarter;
-    private final Map<Integer, Flag<?>> mAllFlags;
-    SparseBooleanArray mBooleanCache = new SparseBooleanArray();
-    SparseArray<String> mStringCache = new SparseArray<>();
+    private final Map<String, Flag<?>> mAllFlags;
+    private final Map<String, Boolean> mBooleanCache = new HashMap<>();
+    private final Map<String, String> mStringCache = new HashMap<>();
 
     private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
             new ServerFlagReader.ChangeListener() {
                 @Override
-                public void onChange() {
-                    mRestarter.restartSystemUI();
+                public void onChange(Flag<?> flag) {
+                    mRestarter.restartSystemUI(
+                            "Server flag change: " + flag.getNamespace() + "." + flag.getName());
                 }
             };
 
@@ -69,13 +67,11 @@
     public FeatureFlagsRelease(
             @Main Resources resources,
             SystemPropertiesHelper systemProperties,
-            DeviceConfigProxy deviceConfigProxy,
             ServerFlagReader serverFlagReader,
-            @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
+            @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags,
             Restarter restarter) {
         mResources = resources;
         mSystemProperties = systemProperties;
-        mDeviceConfigProxy = deviceConfigProxy;
         mServerFlagReader = serverFlagReader;
         mAllFlags = allFlags;
         mRestarter = restarter;
@@ -106,50 +102,48 @@
 
     @Override
     public boolean isEnabled(ResourceBooleanFlag flag) {
-        int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
-        if (cacheIndex < 0) {
-            return isEnabled(flag.getId(), mResources.getBoolean(flag.getResourceId()));
+        if (!mBooleanCache.containsKey(flag.getName())) {
+            return isEnabled(flag.getName(), mResources.getBoolean(flag.getResourceId()));
         }
 
-        return mBooleanCache.valueAt(cacheIndex);
+        return mBooleanCache.get(flag.getName());
     }
 
     @Override
     public boolean isEnabled(SysPropBooleanFlag flag) {
-        int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
-        if (cacheIndex < 0) {
+        if (!mBooleanCache.containsKey(flag.getName())) {
             return isEnabled(
-                    flag.getId(), mSystemProperties.getBoolean(flag.getName(), flag.getDefault()));
+                    flag.getName(),
+                    mSystemProperties.getBoolean(flag.getName(), flag.getDefault()));
         }
 
-        return mBooleanCache.valueAt(cacheIndex);
+        return mBooleanCache.get(flag.getName());
     }
 
-    private boolean isEnabled(int key, boolean defaultValue) {
-        mBooleanCache.append(key, defaultValue);
+    private boolean isEnabled(String name, boolean defaultValue) {
+        mBooleanCache.put(name, defaultValue);
         return defaultValue;
     }
 
     @NonNull
     @Override
     public String getString(@NonNull StringFlag flag) {
-        return getString(flag.getId(), flag.getDefault());
+        return getString(flag.getName(), flag.getDefault());
     }
 
     @NonNull
     @Override
     public String getString(@NonNull ResourceStringFlag flag) {
-        int cacheIndex = mStringCache.indexOfKey(flag.getId());
-        if (cacheIndex < 0) {
-            return getString(flag.getId(),
+        if (!mStringCache.containsKey(flag.getName())) {
+            return getString(flag.getName(),
                     requireNonNull(mResources.getString(flag.getResourceId())));
         }
 
-        return mStringCache.valueAt(cacheIndex);
+        return mStringCache.get(flag.getName());
     }
 
-    private String getString(int key, String defaultValue) {
-        mStringCache.append(key, defaultValue);
+    private String getString(String name, String defaultValue) {
+        mStringCache.put(name, defaultValue);
         return defaultValue;
     }
 
@@ -169,11 +163,17 @@
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("can override: false");
         Map<String, Flag<?>> knownFlags = FlagsFactory.INSTANCE.getKnownFlags();
+        pw.println("Booleans: ");
         for (Map.Entry<String, Flag<?>> nameToFlag : knownFlags.entrySet()) {
             Flag<?> flag = nameToFlag.getValue();
-            int id = flag.getId();
+            if (!(flag instanceof BooleanFlag)
+                    || !(flag instanceof ResourceBooleanFlag)
+                    || !(flag instanceof SysPropBooleanFlag)) {
+                continue;
+            }
+
             boolean def = false;
-            if (mBooleanCache.indexOfKey(flag.getId()) < 0) {
+            if (!mBooleanCache.containsKey(flag.getName())) {
                 if (flag instanceof SysPropBooleanFlag) {
                     SysPropBooleanFlag f = (SysPropBooleanFlag) flag;
                     def = mSystemProperties.getBoolean(f.getName(), f.getDefault());
@@ -185,15 +185,32 @@
                     def = f.getDefault();
                 }
             }
-            pw.println("  sysui_flag_" + id + ": " + (mBooleanCache.get(id, def)));
+            pw.println(
+                    "  " + flag.getName() + ": "
+                            + (mBooleanCache.getOrDefault(flag.getName(), def)));
         }
-        int numStrings = mStringCache.size();
-        pw.println("Strings: " + numStrings);
-        for (int i = 0; i < numStrings; i++) {
-            final int id = mStringCache.keyAt(i);
-            final String value = mStringCache.valueAt(i);
-            final int length = value.length();
-            pw.println("  sysui_flag_" + id + ": [length=" + length + "] \"" + value + "\"");
+
+        pw.println("Strings: ");
+        for (Map.Entry<String, Flag<?>> nameToFlag : knownFlags.entrySet()) {
+            Flag<?> flag = nameToFlag.getValue();
+            if (!(flag instanceof StringFlag)
+                    || !(flag instanceof ResourceStringFlag)) {
+                continue;
+            }
+
+            String def = "";
+            if (!mBooleanCache.containsKey(flag.getName())) {
+                if (flag instanceof ResourceStringFlag) {
+                    ResourceStringFlag f = (ResourceStringFlag) flag;
+                    def = mResources.getString(f.getResourceId());
+                } else if (flag instanceof StringFlag) {
+                    StringFlag f = (StringFlag) flag;
+                    def = f.getDefault();
+                }
+            }
+            String value = mStringCache.getOrDefault(flag.getName(), def);
+            pw.println(
+                    "  " + flag.getName() + ": [length=" + value.length() + "] \"" + value + "\"");
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
index 7ff3876..c08266c 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
@@ -36,41 +36,43 @@
 ) : Restarter {
     var listenersAdded = false
     var pendingRestart: Runnable? = null
+    private var pendingReason = ""
     var androidRestartRequested = false
 
     val observer =
         object : WakefulnessLifecycle.Observer {
             override fun onFinishedGoingToSleep() {
-                scheduleRestart()
+                scheduleRestart(pendingReason)
             }
         }
 
     val batteryCallback =
         object : BatteryController.BatteryStateChangeCallback {
             override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
-                scheduleRestart()
+                scheduleRestart(pendingReason)
             }
         }
 
-    override fun restartSystemUI() {
+    override fun restartSystemUI(reason: String) {
         Log.d(
             FeatureFlagsDebug.TAG,
             "SystemUI Restart requested. Restarting when plugged in and idle."
         )
-        scheduleRestart()
+        scheduleRestart(reason)
     }
 
-    override fun restartAndroid() {
+    override fun restartAndroid(reason: String) {
         Log.d(
             FeatureFlagsDebug.TAG,
             "Android Restart requested. Restarting when plugged in and idle."
         )
         androidRestartRequested = true
-        scheduleRestart()
+        scheduleRestart(reason)
     }
 
-    private fun scheduleRestart() {
+    private fun scheduleRestart(reason: String) {
         // Don't bother adding listeners twice.
+        pendingReason = reason
         if (!listenersAdded) {
             listenersAdded = true
             wakefulnessLifecycle.addObserver(observer)
@@ -91,9 +93,9 @@
     private fun restartNow() {
         Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change")
         if (androidRestartRequested) {
-            systemExitRestarter.restartAndroid()
+            systemExitRestarter.restartAndroid(pendingReason)
         } else {
-            systemExitRestarter.restartSystemUI()
+            systemExitRestarter.restartSystemUI(pendingReason)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
index d088d74..133e67f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.CoreStartable
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.InitializationChecker
+import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
@@ -26,7 +28,13 @@
 
 class FeatureFlagsReleaseStartable
 @Inject
-constructor(dumpManager: DumpManager, featureFlags: FeatureFlags) : CoreStartable {
+constructor(
+    dumpManager: DumpManager,
+    featureFlags: FeatureFlags,
+    private val initializationChecker: InitializationChecker,
+    private val restartDozeListener: RestartDozeListener,
+    private val delayableExecutor: DelayableExecutor
+) : CoreStartable {
 
     init {
         dumpManager.registerCriticalDumpable(FeatureFlagsRelease.TAG) { pw, args ->
@@ -35,7 +43,10 @@
     }
 
     override fun start() {
-        // no-op
+        if (initializationChecker.initializeComponents()) {
+            restartDozeListener.init()
+            delayableExecutor.executeDelayed({ restartDozeListener.maybeRestartSleep() }, 1000)
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
index b7fc0e4..daf9429 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
@@ -39,12 +39,12 @@
     private final List<String> mOffCommands = List.of("false", "off", "0", "disable");
     private final List<String> mSetCommands = List.of("set", "put");
     private final FeatureFlagsDebug mFeatureFlags;
-    private final Map<Integer, Flag<?>> mAllFlags;
+    private final Map<String, Flag<?>> mAllFlags;
 
     @Inject
     FlagCommand(
             FeatureFlagsDebug featureFlags,
-            @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags
+            @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags
     ) {
         mFeatureFlags = featureFlags;
         mAllFlags = allFlags;
@@ -53,30 +53,22 @@
     @Override
     public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) {
         if (args.size() == 0) {
-            pw.println("Error: no flag id supplied");
+            pw.println("Error: no flag name supplied");
             help(pw);
             pw.println();
             printKnownFlags(pw);
             return;
         }
 
-        int id = 0;
-        try {
-            id = Integer.parseInt(args.get(0));
-            if (!mAllFlags.containsKey(id)) {
-                pw.println("Unknown flag id: " + id);
-                pw.println();
-                printKnownFlags(pw);
-                return;
-            }
-        } catch (NumberFormatException e) {
-            id = flagNameToId(args.get(0));
-            if (id == 0) {
-                pw.println("Invalid flag. Must an integer id or flag name: " + args.get(0));
-                return;
-            }
+        String name = args.get(0);
+        if (!mAllFlags.containsKey(name)) {
+            pw.println("Unknown flag name: " + name);
+            pw.println();
+            printKnownFlags(pw);
+            return;
         }
-        Flag<?> flag = mAllFlags.get(id);
+
+        Flag<?> flag = mAllFlags.get(name);
 
         String cmd = "";
         if (args.size() > 1) {
@@ -117,7 +109,7 @@
                 return;
             }
 
-            pw.println("Flag " + id + " is " + newValue);
+            pw.println("Flag " + name + " is " + newValue);
             pw.flush();  // Next command will restart sysui, so flush before we do so.
             if (shouldSet) {
                 mFeatureFlags.setBooleanFlagInternal(flag, newValue);
@@ -136,11 +128,11 @@
                     return;
                 }
                 String value = args.get(2);
-                pw.println("Setting Flag " + id + " to " + value);
+                pw.println("Setting Flag " + name + " to " + value);
                 pw.flush();  // Next command will restart sysui, so flush before we do so.
                 mFeatureFlags.setStringFlagInternal(flag, args.get(2));
             } else {
-                pw.println("Flag " + id + " is " + getStringFlag(flag));
+                pw.println("Flag " + name + " is " + getStringFlag(flag));
             }
             return;
         } else if (isIntFlag(flag)) {
@@ -155,11 +147,11 @@
                     return;
                 }
                 int value = Integer.parseInt(args.get(2));
-                pw.println("Setting Flag " + id + " to " + value);
+                pw.println("Setting Flag " + name + " to " + value);
                 pw.flush();  // Next command will restart sysui, so flush before we do so.
                 mFeatureFlags.setIntFlagInternal(flag, value);
             } else {
-                pw.println("Flag " + id + " is " + getIntFlag(flag));
+                pw.println("Flag " + name + " is " + getIntFlag(flag));
             }
             return;
         }
@@ -182,8 +174,7 @@
     private boolean isBooleanFlag(Flag<?> flag) {
         return (flag instanceof BooleanFlag)
                 || (flag instanceof ResourceBooleanFlag)
-                || (flag instanceof SysPropFlag)
-                || (flag instanceof DeviceConfigBooleanFlag);
+                || (flag instanceof SysPropFlag);
     }
 
     private boolean isBooleanFlagEnabled(Flag<?> flag) {
@@ -252,15 +243,14 @@
         for (int i = 0; i < longestFieldName - "Flag Name".length() + 1; i++) {
             pw.print(" ");
         }
-        pw.println("ID   Value");
+        pw.println(" Value");
         for (int i = 0; i < longestFieldName; i++) {
             pw.print("=");
         }
-        pw.println(" ==== ========");
+        pw.println(" ========");
         for (String fieldName : fields.keySet()) {
             Flag<?> flag = fields.get(fieldName);
-            int id = flag.getId();
-            if (id == 0 || !mAllFlags.containsKey(id)) {
+            if (!mAllFlags.containsKey(flag.getName())) {
                 continue;
             }
             pw.print(fieldName);
@@ -268,9 +258,9 @@
             for (int i = 0; i < longestFieldName - fieldWidth + 1; i++) {
                 pw.print(" ");
             }
-            pw.printf("%-4d ", id);
+            pw.print(" ");
             if (isBooleanFlag(flag)) {
-                pw.println(isBooleanFlagEnabled(mAllFlags.get(id)));
+                pw.println(isBooleanFlagEnabled(flag));
             } else if (isStringFlag(flag)) {
                 pw.println(getStringFlag(flag));
             } else if (isIntFlag(flag)) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 20ae64c..4fb763f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -59,13 +59,16 @@
         )
 
     // TODO(b/254512517): Tracking Bug
-    val FSI_REQUIRES_KEYGUARD = unreleasedFlag(110, "fsi_requires_keyguard", teamfood = true)
+    val FSI_REQUIRES_KEYGUARD = releasedFlag(110, "fsi_requires_keyguard")
 
     // TODO(b/259130119): Tracking Bug
-    val FSI_ON_DND_UPDATE = unreleasedFlag(259130119, "fsi_on_dnd_update", teamfood = true)
+    val FSI_ON_DND_UPDATE = releasedFlag(259130119, "fsi_on_dnd_update")
+
+    // TODO(b/265804648): Tracking Bug
+    @JvmField val DISABLE_FSI = unreleasedFlag(265804648, "disable_fsi")
 
     // TODO(b/254512538): Tracking Bug
-    val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true)
+    val INSTANT_VOICE_REPLY = releasedFlag(111, "instant_voice_reply")
 
     // TODO(b/254512425): Tracking Bug
     val NOTIFICATION_MEMORY_MONITOR_ENABLED =
@@ -75,22 +78,14 @@
         unreleasedFlag(119, "notification_memory_logging_enabled", teamfood = true)
 
     // TODO(b/254512731): Tracking Bug
-    @JvmField
-    val NOTIFICATION_DISMISSAL_FADE =
-        unreleasedFlag(113, "notification_dismissal_fade", teamfood = true)
-
-    // TODO(b/259558771): Tracking Bug
-    val STABILITY_INDEX_FIX = releasedFlag(114, "stability_index_fix")
-
-    // TODO(b/259559750): Tracking Bug
-    val SEMI_STABLE_SORT = releasedFlag(115, "semi_stable_sort")
+    @JvmField val NOTIFICATION_DISMISSAL_FADE = releasedFlag(113, "notification_dismissal_fade")
 
     @JvmField val USE_ROUNDNESS_SOURCETYPES = releasedFlag(116, "use_roundness_sourcetype")
 
     // TODO(b/259217907)
     @JvmField
     val NOTIFICATION_GROUP_DISMISSAL_ANIMATION =
-        unreleasedFlag(259217907, "notification_group_dismissal_animation", teamfood = true)
+        releasedFlag(259217907, "notification_group_dismissal_animation")
 
     // TODO(b/257506350): Tracking Bug
     @JvmField val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
@@ -105,17 +100,19 @@
     // TODO(b/260335638): Tracking Bug
     @JvmField
     val NOTIFICATION_INLINE_REPLY_ANIMATION =
-        unreleasedFlag(174148361, "notification_inline_reply_animation", teamfood = true)
+        releasedFlag(174148361, "notification_inline_reply_animation")
 
     val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
-        unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
+        releasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
+
+    // TODO(b/263414400): Tracking Bug
+    @JvmField
+    val NOTIFICATION_ANIMATE_BIG_PICTURE = unreleasedFlag(120, "notification_animate_big_picture")
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
     // public static final BooleanFlag KEYGUARD_LAYOUT =
     //         new BooleanFlag(200, true);
-    // TODO(b/254512713): Tracking Bug
-    @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations")
 
     // TODO(b/254512750): Tracking Bug
     val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation")
@@ -131,13 +128,6 @@
     val LOCKSCREEN_CUSTOM_CLOCKS = unreleasedFlag(207, "lockscreen_custom_clocks", teamfood = true)
 
     /**
-     * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual
-     * replacement of KeyguardBouncer.java.
-     */
-    // TODO(b/254512385): Tracking Bug
-    @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer")
-
-    /**
      * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
      * the digits when the clock moves.
      */
@@ -165,7 +155,7 @@
     // TODO(b/255618149): Tracking Bug
     @JvmField
     val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES =
-        unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false)
+        unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = true)
 
     /** Shows chipbar UI whenever the device is unlocked by ActiveUnlock (watch). */
     // TODO(b/256513609): Tracking Bug
@@ -180,6 +170,13 @@
     @JvmField
     val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
 
+    /**
+     * Whether to use the new alternate bouncer architecture, a refactor of and eventual replacement
+     * of the Alternate/Authentication Bouncer. No visual UI changes.
+     */
+    // TODO(b/260619425): Tracking Bug
+    @JvmField val MODERN_ALTERNATE_BOUNCER = releasedFlag(219, "modern_alternate_bouncer")
+
     /** Flag to control the migration of face auth to modern architecture. */
     // TODO(b/262838215): Tracking bug
     @JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -190,18 +187,35 @@
 
     // TODO(b/262780002): Tracking Bug
     @JvmField
-    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
-
-    /** A different path for unocclusion transitions back to keyguard */
-    // TODO(b/262859270): Tracking Bug
-    @JvmField
-    val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = true)
 
     // flag for controlling auto pin confirmation and material u shapes in bouncer
     @JvmField
     val AUTO_PIN_CONFIRMATION =
         unreleasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
 
+    // TODO(b/262859270): Tracking Bug
+    @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
+
+    /** Enables code to show contextual loyalty cards in wallet entrypoints */
+    // TODO(b/247587924): Tracking Bug
+    @JvmField
+    val ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS =
+        unreleasedFlag(226, "enable_wallet_contextual_loyalty_cards", teamfood = false)
+
+    // TODO(b/242908637): Tracking Bug
+    @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag(227, "wallpaper_fullscreen_preview")
+
+    /** Whether the long-press gesture to open wallpaper picker is enabled. */
+    // TODO(b/266242192): Tracking Bug
+    @JvmField
+    val LOCK_SCREEN_LONG_PRESS_ENABLED =
+        unreleasedFlag(
+            228,
+            "lock_screen_long_press_enabled",
+            teamfood = true,
+        )
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -211,16 +225,18 @@
     // TODO(b/254513100): Tracking Bug
     val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
         releasedFlag(401, "smartspace_shared_element_transition_enabled")
-    val SMARTSPACE = resourceBooleanFlag(402, R.bool.flag_smartspace, "smartspace")
 
     // TODO(b/258517050): Clean up after the feature is launched.
     @JvmField
-    val SMARTSPACE_DATE_WEATHER_DECOUPLED = unreleasedFlag(403, "smartspace_date_weather_decoupled")
+    val SMARTSPACE_DATE_WEATHER_DECOUPLED =
+        sysPropBooleanFlag(403, "persist.sysui.ss.dw_decoupled", default = false)
+
+    // TODO(b/270223352): Tracking Bug
+    @JvmField
+    val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = unreleasedFlag(404, "hide_smartspace_on_dream_overlay")
 
     // 500 - quick settings
 
-    // TODO(b/254512321): Tracking Bug
-    @JvmField val COMBINED_QS_HEADERS = releasedFlag(501, "combined_qs_headers")
     val PEOPLE_TILE = resourceBooleanFlag(502, R.bool.flag_conversations, "people_tile")
 
     @JvmField
@@ -231,6 +247,9 @@
             "qs_user_detail_shortcut"
         )
 
+    @JvmField
+    val QS_PIPELINE_NEW_HOST = unreleasedFlag(504, "qs_pipeline_new_host", teamfood = false)
+
     // TODO(b/254512383): Tracking Bug
     @JvmField
     val FULL_SCREEN_USER_SWITCHER =
@@ -245,20 +264,33 @@
     val QS_SECONDARY_DATA_SUB_INFO =
         unreleasedFlag(508, "qs_secondary_data_sub_info", teamfood = true)
 
+    /** Enables Font Scaling Quick Settings tile */
+    // TODO(b/269341316): Tracking Bug
+    @JvmField
+    val ENABLE_FONT_SCALING_TILE = unreleasedFlag(509, "enable_font_scaling_tile", teamfood = false)
+
+    /** Enables new QS Edit Mode visual refresh */
+    // TODO(b/269787742): Tracking Bug
+    @JvmField
+    val ENABLE_NEW_QS_EDIT_MODE =
+        unreleasedFlag(510, "enable_new_qs_edit_mode", teamfood = false)
+
     // 600- status bar
 
     // TODO(b/256614753): Tracking Bug
-    val NEW_STATUS_BAR_MOBILE_ICONS = unreleasedFlag(606, "new_status_bar_mobile_icons")
+    val NEW_STATUS_BAR_MOBILE_ICONS =
+        unreleasedFlag(606, "new_status_bar_mobile_icons", teamfood = true)
 
     // TODO(b/256614210): Tracking Bug
-    val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon")
+    val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon", teamfood = true)
 
     // TODO(b/256614751): Tracking Bug
     val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND =
-        unreleasedFlag(608, "new_status_bar_mobile_icons_backend")
+        unreleasedFlag(608, "new_status_bar_mobile_icons_backend", teamfood = true)
 
     // TODO(b/256613548): Tracking Bug
-    val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
+    val NEW_STATUS_BAR_WIFI_ICON_BACKEND =
+        unreleasedFlag(609, "new_status_bar_wifi_icon_backend", teamfood = true)
 
     // TODO(b/256623670): Tracking Bug
     @JvmField
@@ -269,6 +301,9 @@
     val NEW_STATUS_BAR_ICONS_DEBUG_COLORING =
         unreleasedFlag(611, "new_status_bar_icons_debug_coloring")
 
+    // TODO(b/265892345): Tracking Bug
+    val PLUG_IN_STATUS_BAR_CHIP = unreleasedFlag(265892345, "plug_in_status_bar_chip")
+
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
     val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
@@ -284,20 +319,18 @@
 
     // 801 - region sampling
     // TODO(b/254512848): Tracking Bug
-    val REGION_SAMPLING = unreleasedFlag(801, "region_sampling", teamfood = true)
+    val REGION_SAMPLING = unreleasedFlag(801, "region_sampling")
 
     // 803 - screen contents translation
     // TODO(b/254513187): Tracking Bug
     val SCREEN_CONTENTS_TRANSLATION = unreleasedFlag(803, "screen_contents_translation")
 
     // 804 - monochromatic themes
-    @JvmField
-    val MONOCHROMATIC_THEMES =
-        sysPropBooleanFlag(804, "persist.sysui.monochromatic", default = false)
+    @JvmField val MONOCHROMATIC_THEME = unreleasedFlag(804, "monochromatic", teamfood = true)
 
     // 900 - media
     // TODO(b/254512697): Tracking Bug
-    val MEDIA_TAP_TO_TRANSFER = unreleasedFlag(900, "media_tap_to_transfer", teamfood = true)
+    val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer")
 
     // TODO(b/254512502): Tracking Bug
     val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions")
@@ -317,15 +350,35 @@
     // TODO(b/254513168): Tracking Bug
     @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
 
-    @JvmField
-    val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media", teamfood = true)
+    @JvmField val MEDIA_FALSING_PENALTY = releasedFlag(908, "media_falsing_media")
 
     // TODO(b/261734857): Tracking Bug
     @JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
 
     // TODO(b/263272731): Tracking Bug
-    val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
-        unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+    val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = releasedFlag(910, "media_ttt_receiver_success_ripple")
+
+    // TODO(b/263512203): Tracking Bug
+    val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
+
+    // TODO(b/265813373): Tracking Bug
+    val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE = releasedFlag(912, "media_ttt_dismiss_gesture")
+
+    // TODO(b/266157412): Tracking Bug
+    val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
+
+    // TODO(b/266739309): Tracking Bug
+    @JvmField
+    val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update")
+
+    // TODO(b/267007629): Tracking Bug
+    val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress")
+
+    // TODO(b/267166152) : Tracking Bug
+    val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations")
+
+    // TODO(b/270437894): Tracking Bug
+    val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume")
 
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
@@ -333,7 +386,8 @@
     // TODO(b/254512758): Tracking Bug
     @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
 
-    val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot")
+    // TODO(b/265045965): Tracking Bug
+    val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot")
 
     // 1100 - windowing
     @Keep
@@ -396,6 +450,29 @@
     val WM_DESKTOP_WINDOWING_2 =
         sysPropBooleanFlag(1112, "persist.wm.debug.desktop_mode_2", default = false)
 
+    // TODO(b/254513207): Tracking Bug to delete
+    @Keep
+    @JvmField
+    val WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES =
+        unreleasedFlag(
+            1113,
+            name = "screen_record_enterprise_policies",
+            namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+            teamfood = false
+        )
+
+    // TODO(b/198643358): Tracking bug
+    @Keep
+    @JvmField
+    val ENABLE_PIP_SIZE_LARGE_SCREEN =
+        sysPropBooleanFlag(1114, "persist.wm.debug.enable_pip_size_large_screen", default = false)
+
+    // TODO(b/265998256): Tracking bug
+    @Keep
+    @JvmField
+    val ENABLE_PIP_APP_ICON_OVERLAY =
+        sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = false)
+
     // 1200 - predictive back
     @Keep
     @JvmField
@@ -405,7 +482,7 @@
     @Keep
     @JvmField
     val WM_ENABLE_PREDICTIVE_BACK_ANIM =
-        sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = false)
+        sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = true)
 
     @Keep
     @JvmField
@@ -413,8 +490,7 @@
         sysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", default = false)
 
     // TODO(b/254512728): Tracking Bug
-    @JvmField
-    val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = false)
+    @JvmField val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = true)
 
     // TODO(b/255854141): Tracking Bug
     @JvmField
@@ -430,14 +506,37 @@
     val WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM =
         unreleasedFlag(1206, "persist.wm.debug.predictive_back_bouncer_anim", teamfood = true)
 
-    // 1300 - screenshots
-    // TODO(b/254512719): Tracking Bug
-    @JvmField val SCREENSHOT_REQUEST_PROCESSOR = releasedFlag(1300, "screenshot_request_processor")
+    // TODO(b/238475428): Tracking Bug
+    @JvmField
+    val WM_SHADE_ALLOW_BACK_GESTURE =
+        sysPropBooleanFlag(1207, "persist.wm.debug.shade_allow_back_gesture", default = false)
 
+    // TODO(b/238475428): Tracking Bug
+    @JvmField
+    val WM_SHADE_ANIMATE_BACK_GESTURE =
+        unreleasedFlag(1208, "persist.wm.debug.shade_animate_back_gesture", teamfood = false)
+
+    // TODO(b/265639042): Tracking Bug
+    @JvmField
+    val WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM =
+        unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
+
+    // 1300 - screenshots
     // TODO(b/254513155): Tracking Bug
     @JvmField
-    val SCREENSHOT_WORK_PROFILE_POLICY =
-        unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
+    val SCREENSHOT_WORK_PROFILE_POLICY = releasedFlag(1301, "screenshot_work_profile_policy")
+
+    // TODO(b/264916608): Tracking Bug
+    @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata", teamfood = true)
+
+    // TODO(b/266955521): Tracking bug
+    @JvmField
+    val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection", teamfood = true)
+
+    // TODO(b/268484562): Tracking bug
+    @JvmField
+    val SCREENSHOT_METADATA_REFACTOR =
+        unreleasedFlag(1305, "screenshot_metadata_refactor", teamfood = true)
 
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
@@ -447,9 +546,25 @@
     val QUICK_TAP_FLOW_FRAMEWORK =
         unreleasedFlag(1401, "quick_tap_flow_framework", teamfood = false)
 
-    // 1500 - chooser
+    // 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 =
+        unreleasedFlag(1501, "sharesheet_custom_actions", teamfood = true)
+
+    // TODO(b/266982749) Tracking Bug
+    val SHARESHEET_RESELECTION_ACTION =
+        unreleasedFlag(1502, "sharesheet_reselection_action", teamfood = true)
+
+    // TODO(b/266983474) Tracking Bug
+    val SHARESHEET_IMAGE_AND_TEXT_PREVIEW =
+        unreleasedFlag(1503, "sharesheet_image_text_preview", teamfood = true)
+
+    // TODO(b/267355521) Tracking Bug
+    val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW =
+        unreleasedFlag(1504, "sharesheet_scrollable_image_preview")
 
     // 1600 - accessibility
     @JvmField
@@ -457,24 +572,37 @@
         unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
 
     // 1700 - clipboard
-    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor")
     @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
+    // TODO(b/267162944): Tracking bug
+    @JvmField
+    val CLIPBOARD_MINIMIZED_LAYOUT = unreleasedFlag(1702, "clipboard_data_model", teamfood = true)
 
     // 1800 - shade container
     @JvmField
-    val LEAVE_SHADE_OPEN_FOR_BUGREPORT =
-        unreleasedFlag(1800, "leave_shade_open_for_bugreport", teamfood = true)
+    val LEAVE_SHADE_OPEN_FOR_BUGREPORT = releasedFlag(1800, "leave_shade_open_for_bugreport")
+    // TODO(b/265944639): Tracking Bug
+    @JvmField val DUAL_SHADE = releasedFlag(1801, "dual_shade")
 
     // 1900
     @JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag")
 
     // 2000 - device controls
-    @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true)
+    @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels", teamfood = true)
 
     @JvmField
     val APP_PANELS_ALL_APPS_ALLOWED =
         unreleasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true)
 
+    @JvmField
+    val CONTROLS_MANAGEMENT_NEW_FLOWS =
+        unreleasedFlag(2002, "controls_management_new_flows", teamfood = true)
+
+    // Enables removing app from Home control panel as a part of a new flow
+    // TODO(b/269132640): Tracking Bug
+    @JvmField
+    val APP_PANELS_REMOVE_APPS_ALLOWED =
+        unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = false)
+
     // 2100 - Falsing Manager
     @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
 
@@ -485,10 +613,12 @@
     @JvmField val UDFPS_ELLIPSE_DETECTION = unreleasedFlag(2202, "udfps_ellipse_detection")
 
     // 2300 - stylus
-    @JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used")
+    @JvmField
+    val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used", teamfood = true)
     @JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui")
     @JvmField
     val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications")
+    @JvmField val ENABLE_STYLUS_EDUCATION = unreleasedFlag(2303, "enable_stylus_education")
 
     // 2400 - performance tools and debugging info
     // TODO(b/238923086): Tracking Bug
@@ -506,6 +636,26 @@
     @JvmField
     val OUTPUT_SWITCHER_DEVICE_STATUS = unreleasedFlag(2502, "output_switcher_device_status")
 
+    // TODO(b/20911786): Tracking Bug
+    @JvmField
+    val OUTPUT_SWITCHER_SHOW_API_ENABLED =
+        releasedFlag(2503, "output_switcher_show_api_enabled", teamfood = true)
+
+    // 2700 - unfold transitions
+    // TODO(b/265764985): Tracking Bug
+    @Keep
+    @JvmField
+    val ENABLE_DARK_VIGNETTE_WHEN_FOLDING =
+        unreleasedFlag(2700, "enable_dark_vignette_when_folding")
+
     // TODO(b259590361): Tracking bug
     val EXPERIMENTAL_FLAG = unreleasedFlag(2, "exp_flag_release")
+
+    // 2600 - keyboard
+    // TODO(b/259352579): Tracking Bug
+    @JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout")
+
+    // TODO(b/259428678): Tracking Bug
+    @JvmField
+    val KEYBOARD_BACKLIGHT_INDICATOR = unreleasedFlag(2601, "keyboard_backlight_indicator")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
index 8442230..0054d26 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
@@ -28,8 +28,8 @@
         @JvmStatic
         @Provides
         @Named(ALL_FLAGS)
-        fun providesAllFlags(): Map<Int, Flag<*>> {
-            return FlagsFactory.knownFlags.map { it.value.id to it.value }.toMap()
+        fun providesAllFlags(): Map<String, Flag<*>> {
+            return FlagsFactory.knownFlags
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/RestartDozeListener.kt b/packages/SystemUI/src/com/android/systemui/flags/RestartDozeListener.kt
new file mode 100644
index 0000000..bd74f4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/RestartDozeListener.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.flags
+
+import android.os.PowerManager
+import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+
+@SysUISingleton
+class RestartDozeListener
+@Inject
+constructor(
+    private val settings: SecureSettings,
+    private val statusBarStateController: StatusBarStateController,
+    private val powerManager: PowerManager,
+    private val systemClock: SystemClock,
+) {
+
+    companion object {
+        @VisibleForTesting val RESTART_NAP_KEY = "restart_nap_after_start"
+    }
+
+    private var inited = false
+
+    val listener =
+        object : StatusBarStateController.StateListener {
+            override fun onDreamingChanged(isDreaming: Boolean) {
+                settings.putBool(RESTART_NAP_KEY, isDreaming)
+            }
+        }
+
+    fun init() {
+        if (inited) {
+            return
+        }
+        inited = true
+
+        statusBarStateController.addCallback(listener)
+    }
+
+    fun destroy() {
+        statusBarStateController.removeCallback(listener)
+    }
+
+    fun maybeRestartSleep() {
+        if (settings.getBool(RESTART_NAP_KEY, false)) {
+            Log.d("RestartDozeListener", "Restarting sleep state")
+            powerManager.wakeUp(systemClock.uptimeMillis())
+            powerManager.goToSleep(systemClock.uptimeMillis())
+            settings.putBool(RESTART_NAP_KEY, false)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
index ce8b821..9c67795 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
@@ -16,7 +16,7 @@
 package com.android.systemui.flags
 
 interface Restarter {
-    fun restartSystemUI()
+    fun restartSystemUI(reason: String)
 
-    fun restartAndroid()
+    fun restartAndroid(reason: String)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
index ae05c46..9b748d0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.flags
 
 import android.provider.DeviceConfig
+import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.TestHarness
 import com.android.systemui.util.DeviceConfigProxy
 import dagger.Module
 import dagger.Provides
@@ -35,21 +37,28 @@
     fun listenForChanges(values: Collection<Flag<*>>, listener: ChangeListener)
 
     interface ChangeListener {
-        fun onChange()
+        fun onChange(flag: Flag<*>)
     }
 }
 
 class ServerFlagReaderImpl @Inject constructor(
     private val namespace: String,
     private val deviceConfig: DeviceConfigProxy,
-    @Background private val executor: Executor
+    @Background private val executor: Executor,
+    @TestHarness private val isTestHarness: Boolean
 ) : ServerFlagReader {
 
+    private val TAG = "ServerFlagReader"
+
     private val listeners =
         mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>()
 
     private val onPropertiesChangedListener = object : DeviceConfig.OnPropertiesChangedListener {
         override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
+            if (isTestHarness) {
+                Log.w(TAG, "Ignore server flag changes in Test Harness mode.")
+                return
+            }
             if (properties.namespace != namespace) {
                 return
             }
@@ -57,8 +66,8 @@
             for ((listener, flags) in listeners) {
                 propLoop@ for (propName in properties.keyset) {
                     for (flag in flags) {
-                        if (propName == getServerOverrideName(flag.id)) {
-                            listener.onChange()
+                        if (propName == flag.name) {
+                            listener.onChange(flag)
                             break@propLoop
                         }
                     }
@@ -94,10 +103,6 @@
         }
         listeners.add(Pair(listener, flags))
     }
-
-    private fun getServerOverrideName(flagId: Int): String {
-        return "flag_override_$flagId"
-    }
 }
 
 @Module
@@ -110,10 +115,11 @@
         @SysUISingleton
         fun bindsReader(
             deviceConfig: DeviceConfigProxy,
-            @Background executor: Executor
+            @Background executor: Executor,
+            @TestHarness isTestHarness: Boolean
         ): ServerFlagReader {
             return ServerFlagReaderImpl(
-                SYSUI_NAMESPACE, deviceConfig, executor
+                SYSUI_NAMESPACE, deviceConfig, executor, isTestHarness
             )
         }
     }
@@ -138,7 +144,7 @@
         for ((listener, flags) in listeners) {
             flagLoop@ for (flag in flags) {
                 if (name == flag.name) {
-                    listener.onChange()
+                    listener.onChange(flag)
                     break@flagLoop
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
index 89daa64..46e28a7 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.flags
 
+import android.util.Log
 import com.android.internal.statusbar.IStatusBarService
 import javax.inject.Inject
 
@@ -24,11 +25,13 @@
 constructor(
     private val barService: IStatusBarService,
 ) : Restarter {
-    override fun restartAndroid() {
+    override fun restartAndroid(reason: String) {
+        Log.d(FeatureFlagsDebug.TAG, "Restarting Android: " + reason)
         barService.restart()
     }
 
-    override fun restartSystemUI() {
+    override fun restartSystemUI(reason: String) {
+        Log.d(FeatureFlagsDebug.TAG, "Restarting SystemUI: " + reason)
         System.exit(0)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
index d9bcb50..418aeca 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
@@ -37,9 +37,14 @@
     private final int mId;
     private String mOldClass;
 
-    private ExtensionFragmentListener(View view, String tag, int id, Extension<T> extension) {
+    private ExtensionFragmentListener(
+            FragmentService fragmentService,
+            View view,
+            String tag,
+            int id,
+            Extension<T> extension) {
         mTag = tag;
-        mFragmentHostManager = FragmentHostManager.get(view);
+        mFragmentHostManager = fragmentService.getFragmentHostManager(view);
         mExtension = extension;
         mId = id;
         mFragmentHostManager.getFragmentManager().beginTransaction()
@@ -60,8 +65,13 @@
         mExtension.clearItem(true);
     }
 
-    public static <T> void attachExtensonToFragment(View view, String tag, int id,
+    public static <T> void attachExtensonToFragment(
+            FragmentService fragmentService,
+            View view,
+            String tag,
+            int id,
             Extension<T> extension) {
-        extension.addCallback(new ExtensionFragmentListener(view, tag, id, extension));
+        extension.addCallback(
+                new ExtensionFragmentListener(fragmentService, view, tag, id, extension));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
index 9c7411b..6a27ee7 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
@@ -36,7 +36,6 @@
 import androidx.annotation.NonNull;
 
 import com.android.settingslib.applications.InterestingConfigChanges;
-import com.android.systemui.Dependency;
 import com.android.systemui.plugins.Plugin;
 import com.android.systemui.util.leak.LeakDetector;
 
@@ -46,12 +45,17 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 public class FragmentHostManager {
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final Context mContext;
     private final HashMap<String, ArrayList<FragmentListener>> mListeners = new HashMap<>();
     private final View mRootView;
+    private final LeakDetector mLeakDetector;
     private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
             ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_LOCALE
                     | ActivityInfo.CONFIG_ASSETS_PATHS);
@@ -61,14 +65,24 @@
     private FragmentController mFragments;
     private FragmentLifecycleCallbacks mLifecycleCallbacks;
 
-    FragmentHostManager(FragmentService manager, View rootView) {
+    @AssistedInject
+    FragmentHostManager(
+            @Assisted View rootView,
+            FragmentService manager,
+            LeakDetector leakDetector) {
         mContext = rootView.getContext();
         mManager = manager;
         mRootView = rootView;
+        mLeakDetector = leakDetector;
         mConfigChanges.applyNewConfig(mContext.getResources());
         createFragmentHost(null);
     }
 
+    @AssistedFactory
+    public interface Factory {
+        FragmentHostManager create(View rootView);
+    }
+
     private void createFragmentHost(Parcelable savedState) {
         mFragments = FragmentController.createController(new HostCallbacks());
         mFragments.attachHost(null);
@@ -86,7 +100,7 @@
 
             @Override
             public void onFragmentDestroyed(FragmentManager fm, Fragment f) {
-                Dependency.get(LeakDetector.class).trackGarbage(f);
+                mLeakDetector.trackGarbage(f);
             }
         };
         mFragments.getFragmentManager().registerFragmentLifecycleCallbacks(mLifecycleCallbacks,
@@ -211,19 +225,6 @@
         }
     }
 
-    public static FragmentHostManager get(View view) {
-        try {
-            return Dependency.get(FragmentService.class).getFragmentHostManager(view);
-        } catch (ClassCastException e) {
-            // TODO: Some auto handling here?
-            throw e;
-        }
-    }
-
-    public static void removeAndDestroy(View view) {
-        Dependency.get(FragmentService.class).removeAndDestroy(view);
-    }
-
     public void reloadFragments() {
         Trace.beginSection("FrargmentHostManager#reloadFragments");
         // Save the old state.
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
index fe945fb..d302b13a 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
@@ -53,6 +53,7 @@
      */
     private final ArrayMap<String, FragmentInstantiationInfo> mInjectionMap = new ArrayMap<>();
     private final Handler mHandler = new Handler();
+    private final FragmentHostManager.Factory mFragmentHostManagerFactory;
 
     private ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -67,8 +68,10 @@
     @Inject
     public FragmentService(
             FragmentCreator.Factory fragmentCreatorFactory,
+            FragmentHostManager.Factory fragmentHostManagerFactory,
             ConfigurationController configurationController,
             DumpManager dumpManager) {
+        mFragmentHostManagerFactory = fragmentHostManagerFactory;
         addFragmentInstantiationProvider(fragmentCreatorFactory.build());
         configurationController.addCallback(mConfigurationListener);
 
@@ -152,7 +155,7 @@
 
         public FragmentHostState(View view) {
             mView = view;
-            mFragmentHostManager = new FragmentHostManager(FragmentService.this, mView);
+            mFragmentHostManager = mFragmentHostManagerFactory.create(mView);
         }
 
         public void sendConfigurationChange(Configuration newConfig) {
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index db2cd91..e80e71c 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -21,7 +21,6 @@
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
-import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
@@ -901,7 +900,7 @@
                         | Intent.FLAG_ACTIVITY_CLEAR_TOP);
                 intent.putExtra(EmergencyDialerConstants.EXTRA_ENTRY_TYPE,
                         EmergencyDialerConstants.ENTRY_TYPE_POWER_MENU);
-                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
             }
         }
     }
@@ -959,8 +958,7 @@
             mHandler.postDelayed(new Runnable() {
                 @Override
                 public void run() {
-                    mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
-                            SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
+                    mScreenshotHelper.takeScreenshot(SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
                     mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
                     mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
new file mode 100644
index 0000000..496c64e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
@@ -0,0 +1,30 @@
+/*
+ *  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.keyboard
+
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class KeyboardModule {
+
+    @Binds
+    abstract fun bindKeyboardRepository(repository: KeyboardRepositoryImpl): KeyboardRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index 4f1a2b3..ad7973e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -24,7 +24,6 @@
 import android.bluetooth.le.ScanRecord;
 import android.bluetooth.le.ScanResult;
 import android.bluetooth.le.ScanSettings;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.hardware.input.InputManager;
@@ -53,6 +52,7 @@
 import com.android.systemui.CoreStartable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -108,6 +108,7 @@
     protected volatile Context mContext;
 
     private final Provider<LocalBluetoothManager> mBluetoothManagerProvider;
+    private final SecureSettings mSecureSettings;
 
     private boolean mEnabled;
     private String mKeyboardName;
@@ -125,9 +126,11 @@
     private int mState;
 
     @Inject
-    public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider) {
+    public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider,
+            SecureSettings secureSettings) {
         mContext = context;
         this.mBluetoothManagerProvider = bluetoothManagerProvider;
+        mSecureSettings = secureSettings;
     }
 
     @Override
@@ -298,9 +301,8 @@
     }
 
     private boolean isUserSetupComplete() {
-        ContentResolver resolver = mContext.getContentResolver();
-        return Secure.getIntForUser(
-                resolver, Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
+        return mSecureSettings.getIntForUser(
+                Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
     }
 
     private CachedBluetoothDevice getPairedKeyboard() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
new file mode 100644
index 0000000..b0f9c4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.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.keyboard
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+/** A [CoreStartable] that launches components interested in physical keyboard interaction. */
+@SysUISingleton
+class PhysicalKeyboardCoreStartable
+@Inject
+constructor(
+    private val featureFlags: FeatureFlags,
+) : CoreStartable {
+    override fun start() {
+        if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) {
+            // TODO(b/268645743) start listening for keyboard backlight brightness
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt
new file mode 100644
index 0000000..ea15a9f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.keyboard.data.model
+
+/**
+ * Model for current state of keyboard backlight brightness. [level] indicates current level of
+ * backlight brightness and [maxLevel] its max possible value.
+ */
+data class BacklightModel(val level: Int, val maxLevel: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
new file mode 100644
index 0000000..70faf40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.keyboard.data.repository
+
+import android.hardware.input.InputManager
+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.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.data.model.BacklightModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+interface KeyboardRepository {
+    val keyboardConnected: Flow<Boolean>
+    val backlight: Flow<BacklightModel>
+}
+
+@SysUISingleton
+class KeyboardRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val inputManager: InputManager,
+) : KeyboardRepository {
+
+    private val connectedDeviceIds: Flow<Set<Int>> =
+        conflatedCallbackFlow {
+                fun send(element: Set<Int>) = trySendWithFailureLogging(element, TAG)
+
+                var connectedKeyboards = inputManager.inputDeviceIds.toSet()
+                val listener =
+                    object : InputManager.InputDeviceListener {
+                        override fun onInputDeviceAdded(deviceId: Int) {
+                            connectedKeyboards = connectedKeyboards + deviceId
+                            send(connectedKeyboards)
+                        }
+
+                        override fun onInputDeviceChanged(deviceId: Int) = Unit
+
+                        override fun onInputDeviceRemoved(deviceId: Int) {
+                            connectedKeyboards = connectedKeyboards - deviceId
+                            send(connectedKeyboards)
+                        }
+                    }
+                send(connectedKeyboards)
+                inputManager.registerInputDeviceListener(listener, /* handler= */ null)
+                awaitClose { inputManager.unregisterInputDeviceListener(listener) }
+            }
+            .shareIn(
+                scope = applicationScope,
+                started = SharingStarted.Lazily,
+                replay = 1,
+            )
+
+    override val keyboardConnected: Flow<Boolean> =
+        connectedDeviceIds
+            .map { it.any { deviceId -> isPhysicalFullKeyboard(deviceId) } }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+
+    override val backlight: Flow<BacklightModel> =
+        conflatedCallbackFlow {
+            // TODO(b/268645734) register BacklightListener
+        }
+
+    private fun isPhysicalFullKeyboard(deviceId: Int): Boolean {
+        val device = inputManager.getInputDevice(deviceId)
+        return !device.isVirtual && device.isFullKeyboard
+    }
+
+    companion object {
+        const val TAG = "KeyboardRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
index eaf1081..27a5974 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
@@ -31,10 +31,12 @@
 import android.util.Log
 import com.android.systemui.SystemUIAppComponentFactoryBase
 import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
 import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.runBlocking
 
 class CustomizationProvider :
@@ -42,6 +44,7 @@
 
     @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
     @Inject lateinit var previewManager: KeyguardRemotePreviewManager
+    @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher
 
     private lateinit var contextAvailableCallback: ContextAvailableCallback
 
@@ -128,7 +131,7 @@
             throw UnsupportedOperationException()
         }
 
-        return insertSelection(values)
+        return runBlocking(mainDispatcher) { insertSelection(values) }
     }
 
     override fun query(
@@ -138,12 +141,14 @@
         selectionArgs: Array<out String>?,
         sortOrder: String?,
     ): Cursor? {
-        return when (uriMatcher.match(uri)) {
-            MATCH_CODE_ALL_AFFORDANCES -> runBlocking { queryAffordances() }
-            MATCH_CODE_ALL_SLOTS -> querySlots()
-            MATCH_CODE_ALL_SELECTIONS -> runBlocking { querySelections() }
-            MATCH_CODE_ALL_FLAGS -> queryFlags()
-            else -> null
+        return runBlocking(mainDispatcher) {
+            when (uriMatcher.match(uri)) {
+                MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
+                MATCH_CODE_ALL_SLOTS -> querySlots()
+                MATCH_CODE_ALL_SELECTIONS -> querySelections()
+                MATCH_CODE_ALL_FLAGS -> queryFlags()
+                else -> null
+            }
         }
     }
 
@@ -166,7 +171,7 @@
             throw UnsupportedOperationException()
         }
 
-        return deleteSelection(uri, selectionArgs)
+        return runBlocking(mainDispatcher) { deleteSelection(uri, selectionArgs) }
     }
 
     override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
@@ -184,7 +189,7 @@
         }
     }
 
-    private fun insertSelection(values: ContentValues?): Uri? {
+    private suspend fun insertSelection(values: ContentValues?): Uri? {
         if (values == null) {
             throw IllegalArgumentException("Cannot insert selection, no values passed in!")
         }
@@ -282,6 +287,7 @@
                         .ENABLEMENT_ACTION_TEXT,
                     Contract.LockScreenQuickAffordances.AffordanceTable.Columns
                         .ENABLEMENT_COMPONENT_NAME,
+                    Contract.LockScreenQuickAffordances.AffordanceTable.Columns.CONFIGURE_INTENT,
                 )
             )
             .apply {
@@ -298,13 +304,14 @@
                             ),
                             representation.actionText,
                             representation.actionComponentName,
+                            representation.configureIntent?.toUri(0),
                         )
                     )
                 }
             }
     }
 
-    private fun querySlots(): Cursor {
+    private suspend fun querySlots(): Cursor {
         return MatrixCursor(
                 arrayOf(
                     Contract.LockScreenQuickAffordances.SlotTable.Columns.ID,
@@ -323,7 +330,7 @@
             }
     }
 
-    private fun queryFlags(): Cursor {
+    private suspend fun queryFlags(): Cursor {
         return MatrixCursor(
                 arrayOf(
                     Contract.FlagsTable.Columns.NAME,
@@ -346,7 +353,7 @@
             }
     }
 
-    private fun deleteSelection(
+    private suspend fun deleteSelection(
         uri: Uri,
         selectionArgs: Array<out String>?,
     ): Int {
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/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index f4a1227..4d40db0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard;
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -80,6 +79,7 @@
 import com.android.internal.policy.IKeyguardStateCallback;
 import com.android.keyguard.mediator.ScreenOnCoordinator;
 import com.android.systemui.SystemUIApplication;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 
@@ -123,6 +123,7 @@
     private final KeyguardLifecyclesDispatcher mKeyguardLifecyclesDispatcher;
     private final ScreenOnCoordinator mScreenOnCoordinator;
     private final ShellTransitions mShellTransitions;
+    private final DisplayTracker mDisplayTracker;
 
     private static int newModeToLegacyMode(int newMode) {
         switch (newMode) {
@@ -286,12 +287,14 @@
     public KeyguardService(KeyguardViewMediator keyguardViewMediator,
                            KeyguardLifecyclesDispatcher keyguardLifecyclesDispatcher,
                            ScreenOnCoordinator screenOnCoordinator,
-                           ShellTransitions shellTransitions) {
+                           ShellTransitions shellTransitions,
+                           DisplayTracker displayTracker) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
         mScreenOnCoordinator = screenOnCoordinator;
         mShellTransitions = shellTransitions;
+        mDisplayTracker = displayTracker;
     }
 
     @Override
@@ -328,7 +331,7 @@
                         unoccludeAnimationAdapter);
             }
             ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay(
-                    DEFAULT_DISPLAY, definition);
+                    mDisplayTracker.getDefaultDisplayId(), definition);
             return;
         }
         if (sEnableRemoteKeyguardGoingAwayAnimation) {
@@ -635,6 +638,7 @@
             checkPermission();
             mKeyguardViewMediator.onScreenTurnedOff();
             mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_OFF);
+            mScreenOnCoordinator.onScreenTurnedOff();
         }
 
         @Override // Binder interface
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 228320b..f964cb3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -933,7 +933,7 @@
         }
 
         // The smartspace is not visible if the bouncer is showing, so don't shared element it.
-        if (keyguardStateController.isBouncerShowing) {
+        if (keyguardStateController.isPrimaryBouncerShowing) {
             return false
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index e644ed6..d6fbc67 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -34,6 +34,7 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS;
+import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -57,6 +58,7 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.SoundPool;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.DeadObjectException;
 import android.os.Handler;
@@ -65,6 +67,7 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
@@ -100,6 +103,7 @@
 import com.android.internal.policy.IKeyguardExitCallback;
 import com.android.internal.policy.IKeyguardStateCallback;
 import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardConstants;
@@ -266,6 +270,8 @@
     private AlarmManager mAlarmManager;
     private AudioManager mAudioManager;
     private StatusBarManager mStatusBarManager;
+    private final IStatusBarService mStatusBarService;
+    private final IBinder mStatusBarDisableToken = new Binder();
     private final UserTracker mUserTracker;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final Executor mUiBgExecutor;
@@ -958,8 +964,6 @@
                 public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
                         RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                         IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
-                    setOccluded(true /* isOccluded */, true /* animate */);
-
                     if (apps == null || apps.length == 0 || apps[0] == null) {
                         if (DEBUG) {
                             Log.d(TAG, "No apps provided to the OccludeByDream runner; "
@@ -1001,9 +1005,20 @@
                                     applier.scheduleApply(paramsBuilder.build());
                                 });
                         mOccludeByDreamAnimator.addListener(new AnimatorListenerAdapter() {
+                            private boolean mIsCancelled = false;
+                            @Override
+                            public void onAnimationCancel(Animator animation) {
+                                mIsCancelled = true;
+                            }
+
                             @Override
                             public void onAnimationEnd(Animator animation) {
                                 try {
+                                    if (!mIsCancelled) {
+                                        // We're already on the main thread, don't queue this call
+                                        handleSetOccluded(true /* isOccluded */,
+                                                false /* animate */);
+                                    }
                                     finishedCallback.onAnimationFinished();
                                     mOccludeByDreamAnimator = null;
                                 } catch (RemoteException e) {
@@ -1131,12 +1146,12 @@
     private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
             new KeyguardStateController.Callback() {
         @Override
-        public void onBouncerShowingChanged() {
+        public void onPrimaryBouncerShowingChanged() {
             synchronized (KeyguardViewMediator.this) {
-                if (mKeyguardStateController.isBouncerShowing()) {
+                if (mKeyguardStateController.isPrimaryBouncerShowing()) {
                     mPendingPinLock = false;
                 }
-                adjustStatusBarLocked(mKeyguardStateController.isBouncerShowing(), false);
+                adjustStatusBarLocked(mKeyguardStateController.isPrimaryBouncerShowing(), false);
             }
         }
     };
@@ -1193,6 +1208,8 @@
         mPM = powerManager;
         mTrustManager = trustManager;
         mUserSwitcherController = userSwitcherController;
+        mStatusBarService = IStatusBarService.Stub.asInterface(
+                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
         mKeyguardDisplayManager = keyguardDisplayManager;
         mShadeController = shadeControllerLazy;
         dumpManager.registerDumpable(getClass().getName(), this);
@@ -1230,8 +1247,7 @@
                 R.dimen.physical_power_button_center_screen_location_y);
         mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
 
-        mDreamOpenAnimationDuration = context.getResources().getInteger(
-                com.android.internal.R.integer.config_dreamOpenAnimationDuration);
+        mDreamOpenAnimationDuration = (int) DREAMING_ANIMATION_DURATION_MS;
         mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS;
     }
 
@@ -1316,7 +1332,7 @@
         mHideAnimation = AnimationUtils.loadAnimation(mContext,
                 com.android.internal.R.anim.lock_screen_behind_enter);
 
-        mWorkLockController = new WorkLockActivityController(mContext);
+        mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
     }
 
     @Override
@@ -1792,7 +1808,6 @@
 
         Trace.beginSection("KeyguardViewMediator#setOccluded");
         if (DEBUG) Log.d(TAG, "setOccluded " + isOccluded);
-        mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD);
         mHandler.removeMessages(SET_OCCLUDED);
         Message msg = mHandler.obtainMessage(SET_OCCLUDED, isOccluded ? 1 : 0, animate ? 1 : 0);
         mHandler.sendMessage(msg);
@@ -1825,6 +1840,8 @@
     private void handleSetOccluded(boolean isOccluded, boolean animate) {
         Trace.beginSection("KeyguardViewMediator#handleSetOccluded");
         Log.d(TAG, "handleSetOccluded(" + isOccluded + ")");
+        mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD);
+
         synchronized (KeyguardViewMediator.this) {
             if (mHiding && isOccluded) {
                 // We're in the process of going away but WindowManager wants to show a
@@ -1893,12 +1910,6 @@
      * Enable the keyguard if the settings are appropriate.
      */
     private void doKeyguardLocked(Bundle options) {
-        if (KeyguardUpdateMonitor.CORE_APPS_ONLY) {
-            // Don't show keyguard during half-booted cryptkeeper stage.
-            if (DEBUG) Log.d(TAG, "doKeyguard: not showing because booting to cryptkeeper");
-            return;
-        }
-
         // if another app is disabling us, don't show
         if (!mExternallyEnabled) {
             if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled");
@@ -1907,13 +1918,23 @@
             return;
         }
 
-        // if the keyguard is already showing, don't bother. check flags in both files
-        // to account for the hiding animation which results in a delay and discrepancy
-        // between flags
+        // If the keyguard is already showing, see if we don't need to bother re-showing it. Check
+        // flags in both files to account for the hiding animation which results in a delay and
+        // discrepancy between flags.
         if (mShowing && mKeyguardStateController.isShowing()) {
-            if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
-            resetStateLocked();
-            return;
+            if (mPM.isInteractive()) {
+                // It's already showing, and we're not trying to show it while the screen is off.
+                // We can simply reset all of the views.
+                if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
+                resetStateLocked();
+                return;
+            } else {
+                // We are trying to show the keyguard while the screen is off - this results from
+                // race conditions involving locking while unlocking. Don't short-circuit here and
+                // ensure the keyguard is fully re-shown.
+                Log.e(TAG,
+                        "doKeyguard: already showing, but re-showing since we're not interactive");
+            }
         }
 
         // In split system user mode, we never unlock system user.
@@ -2258,6 +2279,10 @@
         }
         if (!mKeyguardDonePending && mHideAnimationRun && !mHideAnimationRunning) {
             handleKeyguardDone();
+        } else if (mSurfaceBehindRemoteAnimationRunning) {
+            // We're already running the keyguard exit animation, likely due to an in-progress swipe
+            // to unlock.
+           exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false /* cancelled */);
         } else if (!mHideAnimationRun) {
             if (DEBUG) Log.d(TAG, "tryKeyguardDone: starting pre-hide animation");
             mHideAnimationRun = true;
@@ -2418,15 +2443,28 @@
         }
         mKeyguardDisplayManager.show();
 
-        // schedule 4hr idle timeout after which non-strong biometrics (i.e. weak or convenience
-        // biometric) can't be used to unlock device until unlocking with strong biometric or
-        // primary auth (i.e. PIN/pattern/password)
-        mLockPatternUtils.scheduleNonStrongBiometricIdleTimeout(
-                KeyguardUpdateMonitor.getCurrentUser());
+        scheduleNonStrongBiometricIdleTimeout();
 
         Trace.endSection();
     }
 
+    /**
+     * Schedule 4-hour idle timeout for non-strong biometrics when the device is locked
+     */
+    private void scheduleNonStrongBiometricIdleTimeout() {
+        final int currentUser = KeyguardUpdateMonitor.getCurrentUser();
+        // If unlocking with non-strong (i.e. weak or convenience) biometrics is possible, schedule
+        // 4hr idle timeout after which non-strong biometrics can't be used to unlock device until
+        // unlocking with strong biometric or primary auth (i.e. PIN/pattern/password)
+        if (mUpdateMonitor.isUnlockingWithNonStrongBiometricsPossible(currentUser)) {
+            if (DEBUG) {
+                Log.d(TAG, "scheduleNonStrongBiometricIdleTimeout: schedule an alarm for "
+                        + "currentUser=" + currentUser);
+            }
+            mLockPatternUtils.scheduleNonStrongBiometricIdleTimeout(currentUser);
+        }
+    }
+
     private final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
         @Override
         public void run() {
@@ -2891,7 +2929,12 @@
             // TODO (b/155663717) After restart, status bar will not properly hide home button
             //  unless disable is called to show un-hide it once first
             if (forceClearFlags) {
-                mStatusBarManager.disable(flags);
+                try {
+                    mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
+                            mContext.getPackageName(), mUserTracker.getUserId());
+                } catch (RemoteException e) {
+                    Log.d(TAG, "Failed to force clear flags", e);
+                }
             }
 
             if (forceHideHomeRecentsButtons || isShowingAndNotOccluded()) {
@@ -2907,7 +2950,12 @@
                         +  " --> flags=0x" + Integer.toHexString(flags));
             }
 
-            mStatusBarManager.disable(flags);
+            try {
+                mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
+                        mContext.getPackageName(), mUserTracker.getUserId());
+            } catch (RemoteException e) {
+                Log.d(TAG, "Failed to set disable flags: " + flags, e);
+            }
         }
     }
 
@@ -2920,6 +2968,8 @@
             if (DEBUG) Log.d(TAG, "handleReset");
             mKeyguardViewControllerLazy.get().reset(true /* hideBouncerWhenShowing */);
         }
+
+        scheduleNonStrongBiometricIdleTimeout();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
index 017b65a..ffd8a02 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
@@ -33,6 +33,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.time.SystemClock;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -63,6 +64,7 @@
 
     private final Context mContext;
     private final DisplayMetrics mDisplayMetrics;
+    private final SystemClock mSystemClock;
 
     @Nullable
     private final IWallpaperManager mWallpaperManagerService;
@@ -71,6 +73,9 @@
 
     private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN;
 
+    public static final long UNKNOWN_LAST_WAKE_TIME = -1;
+    private long mLastWakeTime = UNKNOWN_LAST_WAKE_TIME;
+
     @Nullable
     private Point mLastWakeOriginLocation = null;
 
@@ -84,10 +89,12 @@
     public WakefulnessLifecycle(
             Context context,
             @Nullable IWallpaperManager wallpaperManagerService,
+            SystemClock systemClock,
             DumpManager dumpManager) {
         mContext = context;
         mDisplayMetrics = context.getResources().getDisplayMetrics();
         mWallpaperManagerService = wallpaperManagerService;
+        mSystemClock = systemClock;
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
@@ -104,6 +111,14 @@
     }
 
     /**
+     * Returns the most recent time (in device uptimeMillis) the display woke up.
+     * Returns {@link UNKNOWN_LAST_WAKE_TIME} if there hasn't been a wakeup yet.
+     */
+    public long getLastWakeTime() {
+        return mLastWakeTime;
+    }
+
+    /**
      * Returns the most recent reason the device went to sleep up. This is one of
      * PowerManager.GO_TO_SLEEP_REASON_*.
      */
@@ -117,6 +132,7 @@
         }
         setWakefulness(WAKEFULNESS_WAKING);
         mLastWakeReason = pmWakeReason;
+        mLastWakeTime = mSystemClock.uptimeMillis();
         updateLastWakeOriginLocation();
 
         if (mWallpaperManagerService != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
index 16817ed..b92499e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
@@ -26,10 +26,10 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
@@ -37,16 +37,20 @@
     private static final String TAG = WorkLockActivityController.class.getSimpleName();
 
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final IActivityTaskManager mIatm;
 
-    public WorkLockActivityController(Context context) {
-        this(context, TaskStackChangeListeners.getInstance(), ActivityTaskManager.getService());
+    public WorkLockActivityController(Context context, UserTracker userTracker) {
+        this(context, userTracker, TaskStackChangeListeners.getInstance(),
+                ActivityTaskManager.getService());
     }
 
     @VisibleForTesting
     WorkLockActivityController(
-            Context context, TaskStackChangeListeners tscl, IActivityTaskManager iAtm) {
+            Context context, UserTracker userTracker, TaskStackChangeListeners tscl,
+            IActivityTaskManager iAtm) {
         mContext = context;
+        mUserTracker = userTracker;
         mIatm = iAtm;
 
         tscl.registerTaskStackListener(mLockListener);
@@ -65,7 +69,8 @@
         options.setLaunchTaskId(info.taskId);
         options.setTaskOverlay(true, false /* canResume */);
 
-        final int result = startActivityAsUser(intent, options.toBundle(), UserHandle.USER_CURRENT);
+        final int result = startActivityAsUser(intent, options.toBundle(),
+                mUserTracker.getUserId());
         if (ActivityManager.isStartResultSuccessful(result)) {
             // OK
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
index 80c6130..faeb485 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data
 
 import android.view.KeyEvent
+import android.window.OnBackAnimationCallback
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import java.lang.ref.WeakReference
@@ -51,4 +52,6 @@
         cancelAction: Runnable?,
     )
     fun willDismissWithActions(): Boolean
+    /** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */
+    fun getBackCallback(): OnBackAnimationCallback
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index ea5b4f4..80675d3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -25,10 +25,13 @@
 object BuiltInKeyguardQuickAffordanceKeys {
     // Please keep alphabetical order of const names to simplify future maintenance.
     const val CAMERA = "camera"
+    const val CREATE_NOTE = "create_note"
     const val DO_NOT_DISTURB = "do_not_disturb"
     const val FLASHLIGHT = "flashlight"
     const val HOME_CONTROLS = "home"
+    const val MUTE = "mute"
     const val QR_CODE_SCANNER = "qr_code_scanner"
     const val QUICK_ACCESS_WALLET = "wallet"
+    const val VIDEO_CAMERA = "video_camera"
     // Please keep alphabetical order of const names to simplify future maintenance.
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
index dbc376e..c9f645d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
@@ -18,7 +18,9 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.app.StatusBarManager
+import android.app.admin.DevicePolicyManager
 import android.content.Context
+import android.content.pm.PackageManager
 import com.android.systemui.R
 import com.android.systemui.animation.Expandable
 import com.android.systemui.camera.CameraGestureHelper
@@ -26,17 +28,25 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.settings.UserTracker
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.withContext
 
 @SysUISingleton
 class CameraQuickAffordanceConfig
 @Inject
 constructor(
     @Application private val context: Context,
+    private val packageManager: PackageManager,
     private val cameraGestureHelper: Lazy<CameraGestureHelper>,
+    private val userTracker: UserTracker,
+    private val devicePolicyManager: DevicePolicyManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : KeyguardQuickAffordanceConfig {
 
     override val key: String
@@ -46,7 +56,7 @@
         get() = context.getString(R.string.accessibility_camera_button)
 
     override val pickerIconResourceId: Int
-        get() = com.android.internal.R.drawable.perm_group_camera
+        get() = R.drawable.ic_camera
 
     override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
         get() =
@@ -54,12 +64,20 @@
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     icon =
                         Icon.Resource(
-                            com.android.internal.R.drawable.perm_group_camera,
+                            R.drawable.ic_camera,
                             ContentDescription.Resource(R.string.accessibility_camera_button)
                         )
                 )
             )
 
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+        return if (isLaunchable()) {
+            super.getPickerScreenState()
+        } else {
+            KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+        }
+    }
+
     override fun onTriggered(
         expandable: Expandable?
     ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
@@ -68,4 +86,13 @@
             .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
         return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
     }
+
+    private suspend fun isLaunchable(): Boolean {
+        return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) &&
+            withContext(backgroundDispatcher) {
+                !devicePolicyManager.getCameraDisabled(null, userTracker.userId) &&
+                    devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId) and
+                        DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA == 0
+            }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
index 8efb366..be73f85 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Context
+import android.content.Intent
 import android.net.Uri
 import android.provider.Settings
 import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
@@ -39,6 +40,7 @@
 import com.android.systemui.statusbar.policy.ZenModeController
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -48,10 +50,10 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
-import javax.inject.Inject
 
 @SysUISingleton
-class DoNotDisturbQuickAffordanceConfig constructor(
+class DoNotDisturbQuickAffordanceConfig
+constructor(
     private val context: Context,
     private val controller: ZenModeController,
     private val secureSettings: SecureSettings,
@@ -59,7 +61,7 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val testConditionId: Uri?,
     testDialog: EnableZenModeDialog?,
-): KeyguardQuickAffordanceConfig {
+) : KeyguardQuickAffordanceConfig {
 
     @Inject
     constructor(
@@ -76,20 +78,23 @@
 
     private val conditionUri: Uri
         get() =
-            testConditionId ?: ZenModeConfig.toTimeCondition(
-                context,
-                settingsValue,
-                userTracker.userId,
-                true, /* shortVersion */
-            ).id
+            testConditionId
+                ?: ZenModeConfig.toTimeCondition(
+                        context,
+                        settingsValue,
+                        userTracker.userId,
+                        true, /* shortVersion */
+                    )
+                    .id
 
     private val dialog: EnableZenModeDialog by lazy {
-        testDialog ?: EnableZenModeDialog(
-            context,
-            R.style.Theme_SystemUI_Dialog,
-            true, /* cancelIsNeutral */
-            ZenModeDialogMetricsLogger(context),
-        )
+        testDialog
+            ?: EnableZenModeDialog(
+                context,
+                R.style.Theme_SystemUI_Dialog,
+                true, /* cancelIsNeutral */
+                ZenModeDialogMetricsLogger(context),
+            )
     }
 
     override val key: String = BuiltInKeyguardQuickAffordanceKeys.DO_NOT_DISTURB
@@ -98,58 +103,62 @@
 
     override val pickerIconResourceId: Int = R.drawable.ic_do_not_disturb
 
-    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = combine(
-        conflatedCallbackFlow {
-            val callback = object: ZenModeController.Callback {
-                override fun onZenChanged(zen: Int) {
-                    dndMode = zen
-                    trySendWithFailureLogging(updateState(), TAG)
-                }
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        combine(
+            conflatedCallbackFlow {
+                val callback =
+                    object : ZenModeController.Callback {
+                        override fun onZenChanged(zen: Int) {
+                            dndMode = zen
+                            trySendWithFailureLogging(updateState(), TAG)
+                        }
 
-                override fun onZenAvailableChanged(available: Boolean) {
-                    isAvailable = available
-                    trySendWithFailureLogging(updateState(), TAG)
-                }
-            }
+                        override fun onZenAvailableChanged(available: Boolean) {
+                            isAvailable = available
+                            trySendWithFailureLogging(updateState(), TAG)
+                        }
+                    }
 
-            dndMode = controller.zen
-            isAvailable = controller.isZenAvailable
-            trySendWithFailureLogging(updateState(), TAG)
+                dndMode = controller.zen
+                isAvailable = controller.isZenAvailable
+                trySendWithFailureLogging(updateState(), TAG)
 
-            controller.addCallback(callback)
+                controller.addCallback(callback)
 
-            awaitClose { controller.removeCallback(callback) }
-        },
-        secureSettings
-            .observerFlow(Settings.Secure.ZEN_DURATION)
-            .onStart { emit(Unit) }
-            .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
-            .flowOn(backgroundDispatcher)
-            .distinctUntilChanged()
-            .onEach { settingsValue = it }
-    ) { callbackFlowValue, _ -> callbackFlowValue }
+                awaitClose { controller.removeCallback(callback) }
+            },
+            secureSettings
+                .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION)
+                .onStart { emit(Unit) }
+                .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
+                .flowOn(backgroundDispatcher)
+                .distinctUntilChanged()
+                .onEach { settingsValue = it }
+        ) { callbackFlowValue, _ -> callbackFlowValue }
 
     override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
         return if (controller.isZenAvailable) {
-            KeyguardQuickAffordanceConfig.PickerScreenState.Default
+            KeyguardQuickAffordanceConfig.PickerScreenState.Default(
+                configureIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
+            )
         } else {
             KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
         }
     }
 
-    override fun onTriggered(expandable: Expandable?):
-            KeyguardQuickAffordanceConfig.OnTriggeredResult {
+    override fun onTriggered(
+        expandable: Expandable?
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
         return when {
-            !isAvailable ->
-                KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+            !isAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
             dndMode != ZEN_MODE_OFF -> {
                 controller.setZen(ZEN_MODE_OFF, null, TAG)
                 KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
             }
             settingsValue == ZEN_DURATION_PROMPT ->
                 KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
-                        dialog.createDialog(),
-                        expandable
+                    dialog.createDialog(),
+                    expandable
                 )
             settingsValue == ZEN_DURATION_FOREVER -> {
                 controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG)
@@ -187,4 +196,4 @@
     companion object {
         const val TAG = "DoNotDisturbQuickAffordanceConfig"
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
index 62fe80a..3412f35 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
@@ -135,7 +135,7 @@
 
     override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
         if (flashlightController.isAvailable) {
-            KeyguardQuickAffordanceConfig.PickerScreenState.Default
+            KeyguardQuickAffordanceConfig.PickerScreenState.Default()
         } else {
             KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 09e5ec0..a1e9137d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -90,7 +90,7 @@
             )
         }
 
-        return KeyguardQuickAffordanceConfig.PickerScreenState.Default
+        return KeyguardQuickAffordanceConfig.PickerScreenState.Default()
     }
 
     override fun onTriggered(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index 71d01eb..4556195 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -33,20 +33,24 @@
         @Provides
         @ElementsIntoSet
         fun quickAffordanceConfigs(
+            camera: CameraQuickAffordanceConfig,
             doNotDisturb: DoNotDisturbQuickAffordanceConfig,
             flashlight: FlashlightQuickAffordanceConfig,
             home: HomeControlsKeyguardQuickAffordanceConfig,
+            mute: MuteQuickAffordanceConfig,
             quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
             qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
-            camera: CameraQuickAffordanceConfig,
+            videoCamera: VideoCameraQuickAffordanceConfig,
         ): Set<KeyguardQuickAffordanceConfig> {
             return setOf(
                 camera,
                 doNotDisturb,
                 flashlight,
                 home,
+                mute,
                 quickAccessWallet,
                 qrCodeScanner,
+                videoCamera,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 20588e9..e32edcb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -46,7 +46,7 @@
      * Returns the [PickerScreenState] representing the affordance in the settings or selector
      * experience.
      */
-    suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default
+    suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default()
 
     /**
      * Notifies that the affordance was clicked by the user.
@@ -63,7 +63,10 @@
     sealed class PickerScreenState {
 
         /** The picker shows the item for selecting this affordance as it normally would. */
-        object Default : PickerScreenState()
+        data class Default(
+            /** Optional [Intent] to use to start an activity to configure this affordance. */
+            val configureIntent: Intent? = null,
+        ) : PickerScreenState()
 
         /**
          * The picker does not show an item for selecting this affordance as it is not supported on
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
new file mode 100644
index 0000000..da91572
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+@SysUISingleton
+class MuteQuickAffordanceConfig @Inject constructor(
+        context: Context,
+        private val userTracker: UserTracker,
+        private val userFileManager: UserFileManager,
+        private val ringerModeTracker: RingerModeTracker,
+        private val audioManager: AudioManager,
+        @Application private val coroutineScope: CoroutineScope,
+        @Main private val mainDispatcher: CoroutineDispatcher,
+        @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : KeyguardQuickAffordanceConfig {
+
+    private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.MUTE
+
+    override val pickerName: String = context.getString(R.string.volume_ringer_status_silent)
+
+    override val pickerIconResourceId: Int = R.drawable.ic_notifications_silence
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        ringerModeTracker.ringerModeInternal.asFlow()
+            .onStart { getLastNonSilentRingerMode() }
+            .distinctUntilChanged()
+            .onEach { mode ->
+                // only remember last non-SILENT ringer mode
+                if (mode != null && mode != AudioManager.RINGER_MODE_SILENT) {
+                    previousNonSilentMode = mode
+                }
+            }
+            .map { mode ->
+                val (activationState, contentDescriptionRes) = when {
+                    audioManager.isVolumeFixed ->
+                        ActivationState.NotSupported to
+                            R.string.volume_ringer_hint_mute
+                    mode == AudioManager.RINGER_MODE_SILENT ->
+                        ActivationState.Active to
+                            R.string.volume_ringer_hint_mute
+                    else ->
+                        ActivationState.Inactive to
+                            R.string.volume_ringer_hint_unmute
+                }
+
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    Icon.Resource(
+                        R.drawable.ic_notifications_silence,
+                        ContentDescription.Resource(contentDescriptionRes),
+                    ),
+                    activationState,
+                )
+            }
+            .flowOn(backgroundDispatcher)
+
+    override fun onTriggered(
+        expandable: Expandable?
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        coroutineScope.launch(backgroundDispatcher) {
+            val newRingerMode: Int
+            val currentRingerMode = audioManager.ringerModeInternal
+            if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
+                newRingerMode = previousNonSilentMode
+            } else {
+                previousNonSilentMode = currentRingerMode
+                newRingerMode = AudioManager.RINGER_MODE_SILENT
+            }
+
+            if (currentRingerMode != newRingerMode) {
+                audioManager.ringerModeInternal = newRingerMode
+            }
+        }
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+    }
+
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
+        withContext(backgroundDispatcher) {
+            if (audioManager.isVolumeFixed) {
+                KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+            } else {
+                KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+            }
+        }
+
+    /**
+     * Gets the last non-silent ringer mode from shared-preferences if it exists. This is
+     *  cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
+     */
+    private suspend fun getLastNonSilentRingerMode(): Int =
+        withContext(backgroundDispatcher) {
+            userFileManager.getSharedPreferences(
+                    MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+                    Context.MODE_PRIVATE,
+                    userTracker.userId
+            ).getInt(
+                    LAST_NON_SILENT_RINGER_MODE_KEY,
+                    ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+            )
+        }
+
+    private fun <T> LiveData<T>.asFlow(): Flow<T?> =
+        conflatedCallbackFlow {
+            val observer = Observer { value: T -> trySend(value) }
+            observeForever(observer)
+            send(value)
+            awaitClose { removeObserver(observer) }
+        }.flowOn(mainDispatcher)
+
+    companion object {
+        const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
+        const val MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME = "quick_affordance_mute_ringer_mode_cache"
+        private const val DEFAULT_LAST_NON_SILENT_VALUE = AudioManager.RINGER_MODE_NORMAL
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
new file mode 100644
index 0000000..cd0805e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.Observer
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/**
+ * Store previous non-silent Ringer Mode into shared prefs to be used for Mute Lockscreen Shortcut
+ */
+@SysUISingleton
+class MuteQuickAffordanceCoreStartable @Inject constructor(
+    private val featureFlags: FeatureFlags,
+    private val userTracker: UserTracker,
+    private val ringerModeTracker: RingerModeTracker,
+    private val userFileManager: UserFileManager,
+    private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
+    @Application private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : CoreStartable {
+
+    private val observer = Observer(this::updateLastNonSilentRingerMode)
+
+    override fun start() {
+        if (!featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)) return
+
+        // only listen to ringerModeInternal changes when Mute is one of the selected affordances
+        keyguardQuickAffordanceRepository
+            .selections
+            .map { selections ->
+                // determines if Mute is selected in any lockscreen shortcut position
+                val muteSelected: Boolean = selections.values.any { configList ->
+                    configList.any { config ->
+                        config.key == BuiltInKeyguardQuickAffordanceKeys.MUTE
+                    }
+                }
+                if (muteSelected) {
+                    ringerModeTracker.ringerModeInternal.observeForever(observer)
+                } else {
+                    ringerModeTracker.ringerModeInternal.removeObserver(observer)
+                }
+            }
+            .launchIn(coroutineScope)
+    }
+
+    private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
+        coroutineScope.launch(backgroundDispatcher) {
+            if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
+                userFileManager.getSharedPreferences(
+                        MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+                        Context.MODE_PRIVATE,
+                        userTracker.userId
+                )
+                .edit()
+                .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
+                .apply()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index 4f7990f..ea6c107 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -89,7 +89,7 @@
                             ),
                         ),
                 )
-            else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
+            else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 1928f40..4ba2eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -100,9 +100,9 @@
 
     override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
         return when {
-            !walletController.isWalletEnabled ->
+            !walletController.walletClient.isWalletServiceAvailable ->
                 KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
-            walletController.walletClient.tileIcon == null || queryCards().isEmpty() -> {
+            !walletController.isWalletEnabled || queryCards().isEmpty() -> {
                 val componentName =
                     walletController.walletClient.createWalletSettingsIntent().toComponentName()
                 val actionText =
@@ -128,7 +128,7 @@
                     actionComponentName = componentName,
                 )
             }
-            else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
+            else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
new file mode 100644
index 0000000..6f821a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
@@ -0,0 +1,115 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import android.app.StatusBarManager
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.camera.CameraIntents
+import com.android.systemui.camera.CameraIntentsWrapper
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class VideoCameraQuickAffordanceConfig
+@Inject
+constructor(
+    @Application private val context: Context,
+    private val cameraIntents: CameraIntentsWrapper,
+    private val activityIntentHelper: ActivityIntentHelper,
+    private val userTracker: UserTracker,
+    private val devicePolicyManager: DevicePolicyManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : KeyguardQuickAffordanceConfig {
+
+    private val intent: Intent by lazy {
+        cameraIntents.getVideoCameraIntent().apply {
+            putExtra(
+                CameraIntents.EXTRA_LAUNCH_SOURCE,
+                StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE,
+            )
+        }
+    }
+
+    override val key: String
+        get() = BuiltInKeyguardQuickAffordanceKeys.VIDEO_CAMERA
+
+    override val pickerName: String
+        get() = context.getString(R.string.video_camera)
+
+    override val pickerIconResourceId: Int
+        get() = R.drawable.ic_videocam
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
+        get() = flow {
+            emit(
+                if (isLaunchable()) {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        icon =
+                            Icon.Resource(
+                                R.drawable.ic_videocam,
+                                ContentDescription.Resource(R.string.video_camera)
+                            )
+                    )
+                } else {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+                }
+            )
+        }
+
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+        return if (isLaunchable()) {
+            super.getPickerScreenState()
+        } else {
+            KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+        }
+    }
+
+    override fun onTriggered(
+        expandable: Expandable?
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
+            intent = intent,
+            canShowWhileLocked = false,
+        )
+    }
+
+    private suspend fun isLaunchable(): Boolean {
+        return activityIntentHelper.getTargetActivityInfo(
+            intent,
+            userTracker.userId,
+            true,
+        ) != null &&
+            withContext(backgroundDispatcher) {
+                !devicePolicyManager.getCameraDisabled(null, userTracker.userId)
+            }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
new file mode 100644
index 0000000..84abf57
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
+import android.content.Context
+import android.content.IntentFilter
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback
+import android.os.Looper
+import android.os.UserHandle
+import android.util.Log
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.broadcast.BroadcastDispatcher
+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.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.shared.model.DevicePosture
+import com.android.systemui.user.data.repository.UserRepository
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
+
+/**
+ * Acts as source of truth for biometric authentication related settings like enrollments, device
+ * policy, etc.
+ *
+ * Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about
+ * upstream changes.
+ */
+interface BiometricSettingsRepository {
+    /** Whether any fingerprints are enrolled for the current user. */
+    val isFingerprintEnrolled: StateFlow<Boolean>
+
+    /** Whether face authentication is enrolled for the current user. */
+    val isFaceEnrolled: Flow<Boolean>
+
+    /**
+     * Whether face authentication is enabled/disabled based on system settings like device policy,
+     * biometrics setting.
+     */
+    val isFaceAuthenticationEnabled: Flow<Boolean>
+
+    /**
+     * Whether the current user is allowed to use a strong biometric for device entry based on
+     * Android Security policies. If false, the user may be able to use primary authentication for
+     * device entry.
+     */
+    val isStrongBiometricAllowed: StateFlow<Boolean>
+
+    /** Whether fingerprint feature is enabled for the current user by the DevicePolicy */
+    val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean>
+
+    /**
+     * Whether face authentication is supported for the current device posture. Face auth can be
+     * restricted to specific postures using [R.integer.config_face_auth_supported_posture]
+     */
+    val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
+}
+
+@SysUISingleton
+class BiometricSettingsRepositoryImpl
+@Inject
+constructor(
+    context: Context,
+    lockPatternUtils: LockPatternUtils,
+    broadcastDispatcher: BroadcastDispatcher,
+    authController: AuthController,
+    userRepository: UserRepository,
+    devicePolicyManager: DevicePolicyManager,
+    @Application scope: CoroutineScope,
+    @Background backgroundDispatcher: CoroutineDispatcher,
+    biometricManager: BiometricManager?,
+    @Main looper: Looper,
+    devicePostureRepository: DevicePostureRepository,
+    dumpManager: DumpManager,
+) : BiometricSettingsRepository, Dumpable {
+
+    override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
+
+    init {
+        dumpManager.registerDumpable(this)
+        val configFaceAuthSupportedPosture =
+            DevicePosture.toPosture(
+                context.resources.getInteger(R.integer.config_face_auth_supported_posture)
+            )
+        isFaceAuthSupportedInCurrentPosture =
+            if (configFaceAuthSupportedPosture == DevicePosture.UNKNOWN) {
+                    flowOf(true)
+                } else {
+                    devicePostureRepository.currentDevicePosture.map {
+                        it == configFaceAuthSupportedPosture
+                    }
+                }
+                .onEach { Log.d(TAG, "isFaceAuthSupportedInCurrentPosture value changed to: $it") }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<String?>) {
+        pw.println("isFingerprintEnrolled=${isFingerprintEnrolled.value}")
+        pw.println("isStrongBiometricAllowed=${isStrongBiometricAllowed.value}")
+        pw.println("isFingerprintEnabledByDevicePolicy=${isFingerprintEnabledByDevicePolicy.value}")
+    }
+
+    /** UserId of the current selected user. */
+    private val selectedUserId: Flow<Int> =
+        userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged()
+
+    private val devicePolicyChangedForAllUsers =
+        broadcastDispatcher.broadcastFlow(
+            filter = IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+            user = UserHandle.ALL
+        )
+
+    override val isFingerprintEnrolled: StateFlow<Boolean> =
+        selectedUserId
+            .flatMapLatest { currentUserId ->
+                conflatedCallbackFlow {
+                    val callback =
+                        object : AuthController.Callback {
+                            override fun onEnrollmentsChanged(
+                                sensorBiometricType: BiometricType,
+                                userId: Int,
+                                hasEnrollments: Boolean
+                            ) {
+                                if (sensorBiometricType.isFingerprint && userId == currentUserId) {
+                                    trySendWithFailureLogging(
+                                        hasEnrollments,
+                                        TAG,
+                                        "update fpEnrollment"
+                                    )
+                                }
+                            }
+                        }
+                    authController.addCallback(callback)
+                    awaitClose { authController.removeCallback(callback) }
+                }
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.Eagerly,
+                initialValue =
+                    authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id)
+            )
+
+    override val isFaceEnrolled: Flow<Boolean> =
+        selectedUserId.flatMapLatest { selectedUserId: Int ->
+            conflatedCallbackFlow {
+                val callback =
+                    object : AuthController.Callback {
+                        override fun onEnrollmentsChanged(
+                            sensorBiometricType: BiometricType,
+                            userId: Int,
+                            hasEnrollments: Boolean
+                        ) {
+                            // TODO(b/242022358), use authController.isFaceAuthEnrolled after
+                            //  ag/20176811 is available.
+                            if (
+                                sensorBiometricType == BiometricType.FACE &&
+                                    userId == selectedUserId
+                            ) {
+                                trySendWithFailureLogging(
+                                    hasEnrollments,
+                                    TAG,
+                                    "Face enrollment changed"
+                                )
+                            }
+                        }
+                    }
+                authController.addCallback(callback)
+                trySendWithFailureLogging(
+                    authController.isFaceAuthEnrolled(selectedUserId),
+                    TAG,
+                    "Initial value of face auth enrollment"
+                )
+                awaitClose { authController.removeCallback(callback) }
+            }
+        }
+
+    override val isFaceAuthenticationEnabled: Flow<Boolean>
+        get() =
+            combine(isFaceEnabledByBiometricsManager, isFaceEnabledByDevicePolicy) {
+                biometricsManagerSetting,
+                devicePolicySetting ->
+                biometricsManagerSetting && devicePolicySetting
+            }
+
+    private val isFaceEnabledByDevicePolicy: Flow<Boolean> =
+        combine(selectedUserId, devicePolicyChangedForAllUsers) { userId, _ ->
+                devicePolicyManager.isFaceDisabled(userId)
+            }
+            .onStart {
+                emit(devicePolicyManager.isFaceDisabled(userRepository.getSelectedUserInfo().id))
+            }
+            .flowOn(backgroundDispatcher)
+            .distinctUntilChanged()
+
+    private val isFaceEnabledByBiometricsManager =
+        conflatedCallbackFlow {
+                val callback =
+                    object : IBiometricEnabledOnKeyguardCallback.Stub() {
+                        override fun onChanged(enabled: Boolean, userId: Int) {
+                            trySendWithFailureLogging(
+                                enabled,
+                                TAG,
+                                "biometricsEnabled state changed"
+                            )
+                        }
+                    }
+                biometricManager?.registerEnabledOnKeyguardCallback(callback)
+                awaitClose {}
+            }
+            // This is because the callback is binder-based and we want to avoid multiple callbacks
+            // being registered.
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    override val isStrongBiometricAllowed: StateFlow<Boolean> =
+        selectedUserId
+            .flatMapLatest { currUserId ->
+                conflatedCallbackFlow {
+                    val callback =
+                        object : LockPatternUtils.StrongAuthTracker(context, looper) {
+                            override fun onStrongAuthRequiredChanged(userId: Int) {
+                                if (currUserId != userId) {
+                                    return
+                                }
+
+                                trySendWithFailureLogging(
+                                    isBiometricAllowedForUser(true, currUserId),
+                                    TAG
+                                )
+                            }
+
+                            override fun onIsNonStrongBiometricAllowedChanged(userId: Int) {
+                                // no-op
+                            }
+                        }
+                    lockPatternUtils.registerStrongAuthTracker(callback)
+                    awaitClose { lockPatternUtils.unregisterStrongAuthTracker(callback) }
+                }
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.Eagerly,
+                initialValue =
+                    lockPatternUtils.isBiometricAllowedForUser(
+                        userRepository.getSelectedUserInfo().id
+                    )
+            )
+
+    override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> =
+        selectedUserId
+            .flatMapLatest { userId ->
+                devicePolicyChangedForAllUsers
+                    .transformLatest { emit(devicePolicyManager.isFingerprintDisabled(userId)) }
+                    .flowOn(backgroundDispatcher)
+                    .distinctUntilChanged()
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.Eagerly,
+                initialValue =
+                    devicePolicyManager.isFingerprintDisabled(
+                        userRepository.getSelectedUserInfo().id
+                    )
+            )
+
+    companion object {
+        private const val TAG = "BiometricsRepositoryImpl"
+    }
+}
+
+private fun DevicePolicyManager.isFaceDisabled(userId: Int): Boolean =
+    isNotActive(userId, DevicePolicyManager.KEYGUARD_DISABLE_FACE)
+
+private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean =
+    isNotActive(userId, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT)
+
+private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean =
+    (getKeyguardDisabledFeatures(null, userId) and policy) == 0
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt
new file mode 100644
index 0000000..93c9781
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+enum class BiometricType(val isFingerprint: Boolean) {
+    // An unsupported biometric type
+    UNKNOWN(false),
+
+    // Fingerprint sensor that is located on the back (opposite side of the display) of the device
+    REAR_FINGERPRINT(true),
+
+    // Fingerprint sensor that is located under the display
+    UNDER_DISPLAY_FINGERPRINT(true),
+
+    // Fingerprint sensor that is located on the side of the device, typically on the power button
+    SIDE_FINGERPRINT(true),
+    FACE(false),
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
new file mode 100644
index 0000000..7c46684
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.hardware.biometrics.BiometricSourceType
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Dumpable
+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.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Encapsulates state about device entry fingerprint auth mechanism. */
+interface DeviceEntryFingerprintAuthRepository {
+    /** Whether the device entry fingerprint auth is locked out. */
+    val isLockedOut: StateFlow<Boolean>
+}
+
+/**
+ * Implementation of [DeviceEntryFingerprintAuthRepository] that uses [KeyguardUpdateMonitor] as the
+ * source of truth.
+ *
+ * Dependency on [KeyguardUpdateMonitor] will be removed once fingerprint auth state is moved out of
+ * [KeyguardUpdateMonitor]
+ */
+@SysUISingleton
+class DeviceEntryFingerprintAuthRepositoryImpl
+@Inject
+constructor(
+    val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    @Application scope: CoroutineScope,
+    dumpManager: DumpManager,
+) : DeviceEntryFingerprintAuthRepository, Dumpable {
+
+    init {
+        dumpManager.registerDumpable(this)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<String?>) {
+        pw.println("isLockedOut=${isLockedOut.value}")
+    }
+
+    override val isLockedOut: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val sendLockoutUpdate =
+                    fun() {
+                        trySendWithFailureLogging(
+                            keyguardUpdateMonitor.isFingerprintLockedOut,
+                            TAG,
+                            "onLockedOutStateChanged"
+                        )
+                    }
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onLockedOutStateChanged(
+                            biometricSourceType: BiometricSourceType?
+                        ) {
+                            if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
+                                sendLockoutUpdate()
+                            }
+                        }
+                    }
+                keyguardUpdateMonitor.registerCallback(callback)
+                sendLockoutUpdate()
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .stateIn(scope, started = SharingStarted.Eagerly, initialValue = false)
+
+    companion object {
+        const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt
new file mode 100644
index 0000000..adb1e01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.keyguard.data.repository
+
+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.keyguard.shared.model.DevicePosture
+import com.android.systemui.statusbar.policy.DevicePostureController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Provide current device posture state. */
+interface DevicePostureRepository {
+    /** Provides the current device posture. */
+    val currentDevicePosture: Flow<DevicePosture>
+}
+
+@SysUISingleton
+class DevicePostureRepositoryImpl
+@Inject
+constructor(private val postureController: DevicePostureController) : DevicePostureRepository {
+    override val currentDevicePosture: Flow<DevicePosture>
+        get() = conflatedCallbackFlow {
+            val sendPostureUpdate = { posture: Int ->
+                val currentDevicePosture = DevicePosture.toPosture(posture)
+                trySendWithFailureLogging(
+                    currentDevicePosture,
+                    TAG,
+                    "Error sending posture update to $currentDevicePosture"
+                )
+            }
+            val callback = DevicePostureController.Callback { sendPostureUpdate(it) }
+            postureController.addCallback(callback)
+            sendPostureUpdate(postureController.devicePosture)
+
+            awaitClose { postureController.removeCallback(callback) }
+        }
+
+    companion object {
+        const val TAG = "PostureRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index 90f3c7d..0e85347 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -20,15 +20,17 @@
 import com.android.keyguard.ViewMediatorCallback
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
 import com.android.systemui.log.dagger.BouncerLog
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
-import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
@@ -39,30 +41,16 @@
  *
  * Make sure to add newly added flows to the logger.
  */
-@SysUISingleton
-class KeyguardBouncerRepository
-@Inject
-constructor(
-    private val viewMediatorCallback: ViewMediatorCallback,
-    @Application private val applicationScope: CoroutineScope,
-    @BouncerLog private val buffer: TableLogBuffer,
-) {
+interface KeyguardBouncerRepository {
     /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
-    private val _primaryBouncerVisible = MutableStateFlow(false)
-    val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
-    private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
-    val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
-    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
-    val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
-    private val _primaryBouncerHide = MutableStateFlow(false)
-    val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
-    private val _primaryBouncerStartingToHide = MutableStateFlow(false)
-    val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
-    private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
-    val primaryBouncerStartingDisappearAnimation = _primaryBouncerDisappearAnimation.asStateFlow()
+    val primaryBouncerVisible: StateFlow<Boolean>
+    val primaryBouncerShow: StateFlow<KeyguardBouncerModel?>
+    val primaryBouncerShowingSoon: StateFlow<Boolean>
+    val primaryBouncerHide: StateFlow<Boolean>
+    val primaryBouncerStartingToHide: StateFlow<Boolean>
+    val primaryBouncerStartingDisappearAnimation: StateFlow<Runnable?>
     /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
-    private val _primaryBouncerScrimmed = MutableStateFlow(false)
-    val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+    val primaryBouncerScrimmed: StateFlow<Boolean>
     /**
      * Set how much of the notification panel is showing on the screen.
      * ```
@@ -70,84 +58,186 @@
      *      1f = panel fully showing = bouncer fully hidden
      * ```
      */
-    private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncer.EXPANSION_HIDDEN)
-    val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
+    val panelExpansionAmount: StateFlow<Float>
+    val keyguardPosition: StateFlow<Float>
+    val isBackButtonEnabled: StateFlow<Boolean?>
+    /** Determines if user is already unlocked */
+    val keyguardAuthenticated: StateFlow<Boolean?>
+    val showMessage: StateFlow<BouncerShowMessageModel?>
+    val resourceUpdateRequests: StateFlow<Boolean>
+    val bouncerPromptReason: Int
+    val bouncerErrorMessage: CharSequence?
+    val alternateBouncerVisible: StateFlow<Boolean>
+    val alternateBouncerUIAvailable: StateFlow<Boolean>
+    val sideFpsShowing: StateFlow<Boolean>
+
+    var lastAlternateBouncerVisibleTime: Long
+
+    fun setPrimaryScrimmed(isScrimmed: Boolean)
+
+    fun setPrimaryVisible(isVisible: Boolean)
+
+    fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?)
+
+    fun setPrimaryShowingSoon(showingSoon: Boolean)
+
+    fun setPrimaryHide(hide: Boolean)
+
+    fun setPrimaryStartingToHide(startingToHide: Boolean)
+
+    fun setPrimaryStartDisappearAnimation(runnable: Runnable?)
+
+    fun setPanelExpansion(panelExpansion: Float)
+
+    fun setKeyguardPosition(keyguardPosition: Float)
+
+    fun setResourceUpdateRequests(willUpdateResources: Boolean)
+
+    fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?)
+
+    fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?)
+
+    fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean)
+
+    fun setAlternateVisible(isVisible: Boolean)
+
+    fun setAlternateBouncerUIAvailable(isAvailable: Boolean)
+
+    fun setSideFpsShowing(isShowing: Boolean)
+}
+
+@SysUISingleton
+class KeyguardBouncerRepositoryImpl
+@Inject
+constructor(
+    private val viewMediatorCallback: ViewMediatorCallback,
+    private val clock: SystemClock,
+    @Application private val applicationScope: CoroutineScope,
+    @BouncerLog private val buffer: TableLogBuffer,
+) : KeyguardBouncerRepository {
+    /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
+    private val _primaryBouncerVisible = MutableStateFlow(false)
+    override val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+    private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
+    override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+    override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+    private val _primaryBouncerHide = MutableStateFlow(false)
+    override val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+    private val _primaryBouncerStartingToHide = MutableStateFlow(false)
+    override val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+    private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
+    override val primaryBouncerStartingDisappearAnimation =
+        _primaryBouncerDisappearAnimation.asStateFlow()
+    /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
+    private val _primaryBouncerScrimmed = MutableStateFlow(false)
+    override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+    /**
+     * Set how much of the notification panel is showing on the screen.
+     * ```
+     *      0f = panel fully hidden = bouncer fully showing
+     *      1f = panel fully showing = bouncer fully hidden
+     * ```
+     */
+    private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
+    override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
     private val _keyguardPosition = MutableStateFlow(0f)
-    val keyguardPosition = _keyguardPosition.asStateFlow()
-    private val _onScreenTurnedOff = MutableStateFlow(false)
-    val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+    override val keyguardPosition = _keyguardPosition.asStateFlow()
     private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
-    val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+    override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
     private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
     /** Determines if user is already unlocked */
-    val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+    override val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
     private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
-    val showMessage = _showMessage.asStateFlow()
+    override val showMessage = _showMessage.asStateFlow()
     private val _resourceUpdateRequests = MutableStateFlow(false)
-    val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
-    val bouncerPromptReason: Int
+    override val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+    override val bouncerPromptReason: Int
         get() = viewMediatorCallback.bouncerPromptReason
-    val bouncerErrorMessage: CharSequence?
+    override val bouncerErrorMessage: CharSequence?
         get() = viewMediatorCallback.consumeCustomMessage()
 
+    /** Values associated with the AlternateBouncer */
+    private val _alternateBouncerVisible = MutableStateFlow(false)
+    override val alternateBouncerVisible = _alternateBouncerVisible.asStateFlow()
+    override var lastAlternateBouncerVisibleTime: Long = NOT_VISIBLE
+    private val _alternateBouncerUIAvailable = MutableStateFlow(false)
+    override val alternateBouncerUIAvailable: StateFlow<Boolean> =
+        _alternateBouncerUIAvailable.asStateFlow()
+    private val _sideFpsShowing = MutableStateFlow(false)
+    override val sideFpsShowing: StateFlow<Boolean> = _sideFpsShowing.asStateFlow()
+
     init {
         setUpLogging()
     }
 
-    fun setPrimaryScrimmed(isScrimmed: Boolean) {
+    override fun setPrimaryScrimmed(isScrimmed: Boolean) {
         _primaryBouncerScrimmed.value = isScrimmed
     }
 
-    fun setPrimaryVisible(isVisible: Boolean) {
+    override fun setPrimaryVisible(isVisible: Boolean) {
         _primaryBouncerVisible.value = isVisible
     }
 
-    fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+    override fun setAlternateVisible(isVisible: Boolean) {
+        if (isVisible && !_alternateBouncerVisible.value) {
+            lastAlternateBouncerVisibleTime = clock.uptimeMillis()
+        } else if (!isVisible) {
+            lastAlternateBouncerVisibleTime = NOT_VISIBLE
+        }
+        _alternateBouncerVisible.value = isVisible
+    }
+
+    override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+        _alternateBouncerUIAvailable.value = isAvailable
+    }
+
+    override fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
         _primaryBouncerShow.value = keyguardBouncerModel
     }
 
-    fun setPrimaryShowingSoon(showingSoon: Boolean) {
+    override fun setPrimaryShowingSoon(showingSoon: Boolean) {
         _primaryBouncerShowingSoon.value = showingSoon
     }
 
-    fun setPrimaryHide(hide: Boolean) {
+    override fun setPrimaryHide(hide: Boolean) {
         _primaryBouncerHide.value = hide
     }
 
-    fun setPrimaryStartingToHide(startingToHide: Boolean) {
+    override fun setPrimaryStartingToHide(startingToHide: Boolean) {
         _primaryBouncerStartingToHide.value = startingToHide
     }
 
-    fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+    override fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
         _primaryBouncerDisappearAnimation.value = runnable
     }
 
-    fun setPanelExpansion(panelExpansion: Float) {
+    override fun setPanelExpansion(panelExpansion: Float) {
         _panelExpansionAmount.value = panelExpansion
     }
 
-    fun setKeyguardPosition(keyguardPosition: Float) {
+    override fun setKeyguardPosition(keyguardPosition: Float) {
         _keyguardPosition.value = keyguardPosition
     }
 
-    fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+    override fun setResourceUpdateRequests(willUpdateResources: Boolean) {
         _resourceUpdateRequests.value = willUpdateResources
     }
 
-    fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+    override fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
         _showMessage.value = bouncerShowMessageModel
     }
 
-    fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+    override fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
         _keyguardAuthenticated.value = keyguardAuthenticated
     }
 
-    fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+    override fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
         _isBackButtonEnabled.value = isBackButtonEnabled
     }
 
-    fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
-        _onScreenTurnedOff.value = onScreenTurnedOff
+    override fun setSideFpsShowing(isShowing: Boolean) {
+        _sideFpsShowing.value = isShowing
     }
 
     /** Sets up logs for state flows. */
@@ -187,9 +277,6 @@
             .map { it.toInt() }
             .logDiffsForTable(buffer, "", "KeyguardPosition", -1)
             .launchIn(applicationScope)
-        onScreenTurnedOff
-            .logDiffsForTable(buffer, "", "OnScreenTurnedOff", false)
-            .launchIn(applicationScope)
         isBackButtonEnabled
             .filterNotNull()
             .logDiffsForTable(buffer, "", "IsBackButtonEnabled", false)
@@ -201,5 +288,15 @@
         resourceUpdateRequests
             .logDiffsForTable(buffer, "", "ResourceUpdateRequests", false)
             .launchIn(applicationScope)
+        alternateBouncerUIAvailable
+            .logDiffsForTable(buffer, "", "IsAlternateBouncerUIAvailable", false)
+            .launchIn(applicationScope)
+        sideFpsShowing
+            .logDiffsForTable(buffer, "", "isSideFpsShowing", false)
+            .launchIn(applicationScope)
+    }
+
+    companion object {
+        private const val NOT_VISIBLE = -1L
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt
new file mode 100644
index 0000000..2069891
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManager.kt
@@ -0,0 +1,342 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.hardware.face.FaceManager
+import android.os.CancellationSignal
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.UiEventLogger
+import com.android.keyguard.FaceAuthUiEvent
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus
+import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
+import com.android.systemui.log.FaceAuthenticationLogger
+import com.android.systemui.log.SessionTracker
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.user.data.repository.UserRepository
+import java.io.PrintWriter
+import java.util.Arrays
+import java.util.stream.Collectors
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * API to run face authentication and detection for device entry / on keyguard (as opposed to the
+ * biometric prompt).
+ */
+interface KeyguardFaceAuthManager {
+    /**
+     * Trigger face authentication.
+     *
+     * [uiEvent] provided should be logged whenever face authentication runs. Invocation should be
+     * ignored if face authentication is already running. Results should be propagated through
+     * [authenticationStatus]
+     */
+    suspend fun authenticate(uiEvent: FaceAuthUiEvent)
+
+    /**
+     * Trigger face detection.
+     *
+     * Invocation should be ignored if face authentication is currently running.
+     */
+    suspend fun detect()
+
+    /** Stop currently running face authentication or detection. */
+    fun cancel()
+
+    /** Provide the current status of face authentication. */
+    val authenticationStatus: Flow<AuthenticationStatus>
+
+    /** Provide the current status of face detection. */
+    val detectionStatus: Flow<DetectionStatus>
+
+    /** Current state of whether face authentication is locked out or not. */
+    val isLockedOut: Flow<Boolean>
+
+    /** Current state of whether face authentication is running. */
+    val isAuthRunning: Flow<Boolean>
+
+    /** Is face detection supported. */
+    val isDetectionSupported: Boolean
+}
+
+@SysUISingleton
+class KeyguardFaceAuthManagerImpl
+@Inject
+constructor(
+    context: Context,
+    private val faceManager: FaceManager? = null,
+    private val userRepository: UserRepository,
+    private val keyguardBypassController: KeyguardBypassController? = null,
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    private val sessionTracker: SessionTracker,
+    private val uiEventsLogger: UiEventLogger,
+    private val faceAuthLogger: FaceAuthenticationLogger,
+    dumpManager: DumpManager,
+) : KeyguardFaceAuthManager, Dumpable {
+    private var cancellationSignal: CancellationSignal? = null
+    private val lockscreenBypassEnabled: Boolean
+        get() = keyguardBypassController?.bypassEnabled ?: false
+    private var faceAcquiredInfoIgnoreList: Set<Int>
+
+    private val faceLockoutResetCallback =
+        object : FaceManager.LockoutResetCallback() {
+            override fun onLockoutReset(sensorId: Int) {
+                _isLockedOut.value = false
+            }
+        }
+
+    init {
+        faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
+        faceAcquiredInfoIgnoreList =
+            Arrays.stream(
+                    context.resources.getIntArray(
+                        R.array.config_face_acquire_device_entry_ignorelist
+                    )
+                )
+                .boxed()
+                .collect(Collectors.toSet())
+        dumpManager.registerCriticalDumpable("KeyguardFaceAuthManagerImpl", this)
+    }
+
+    private val faceAuthCallback =
+        object : FaceManager.AuthenticationCallback() {
+            override fun onAuthenticationFailed() {
+                _authenticationStatus.value = FailedAuthenticationStatus
+                faceAuthLogger.authenticationFailed()
+                onFaceAuthRequestCompleted()
+            }
+
+            override fun onAuthenticationAcquired(acquireInfo: Int) {
+                _authenticationStatus.value = AcquiredAuthenticationStatus(acquireInfo)
+                faceAuthLogger.authenticationAcquired(acquireInfo)
+            }
+
+            override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
+                val errorStatus = ErrorAuthenticationStatus(errorCode, errString.toString())
+                if (errorStatus.isLockoutError()) {
+                    _isLockedOut.value = true
+                }
+                _authenticationStatus.value = errorStatus
+                if (errorStatus.isCancellationError()) {
+                    cancelNotReceivedHandlerJob?.cancel()
+                    applicationScope.launch {
+                        faceAuthLogger.launchingQueuedFaceAuthRequest(
+                            faceAuthRequestedWhileCancellation
+                        )
+                        faceAuthRequestedWhileCancellation?.let { authenticate(it) }
+                        faceAuthRequestedWhileCancellation = null
+                    }
+                }
+                faceAuthLogger.authenticationError(
+                    errorCode,
+                    errString,
+                    errorStatus.isLockoutError(),
+                    errorStatus.isCancellationError()
+                )
+                onFaceAuthRequestCompleted()
+            }
+
+            override fun onAuthenticationHelp(code: Int, helpStr: CharSequence?) {
+                if (faceAcquiredInfoIgnoreList.contains(code)) {
+                    return
+                }
+                _authenticationStatus.value = HelpAuthenticationStatus(code, helpStr.toString())
+            }
+
+            override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) {
+                _authenticationStatus.value = SuccessAuthenticationStatus(result)
+                faceAuthLogger.faceAuthSuccess(result)
+                onFaceAuthRequestCompleted()
+            }
+        }
+
+    private fun onFaceAuthRequestCompleted() {
+        cancellationInProgress = false
+        _isAuthRunning.value = false
+        cancellationSignal = null
+    }
+
+    private val detectionCallback =
+        FaceManager.FaceDetectionCallback { sensorId, userId, isStrong ->
+            faceAuthLogger.faceDetected()
+            _detectionStatus.value = DetectionStatus(sensorId, userId, isStrong)
+        }
+
+    private var cancellationInProgress = false
+    private var faceAuthRequestedWhileCancellation: FaceAuthUiEvent? = null
+
+    override suspend fun authenticate(uiEvent: FaceAuthUiEvent) {
+        if (_isAuthRunning.value) {
+            faceAuthLogger.ignoredFaceAuthTrigger(uiEvent)
+            return
+        }
+
+        if (cancellationInProgress) {
+            faceAuthLogger.queuingRequestWhileCancelling(
+                faceAuthRequestedWhileCancellation,
+                uiEvent
+            )
+            faceAuthRequestedWhileCancellation = uiEvent
+            return
+        } else {
+            faceAuthRequestedWhileCancellation = null
+        }
+
+        withContext(mainDispatcher) {
+            // We always want to invoke face auth in the main thread.
+            cancellationSignal = CancellationSignal()
+            _isAuthRunning.value = true
+            uiEventsLogger.logWithInstanceIdAndPosition(
+                uiEvent,
+                0,
+                null,
+                keyguardSessionId,
+                uiEvent.extraInfo
+            )
+            faceAuthLogger.authenticating(uiEvent)
+            faceManager?.authenticate(
+                null,
+                cancellationSignal,
+                faceAuthCallback,
+                null,
+                currentUserId,
+                lockscreenBypassEnabled
+            )
+        }
+    }
+
+    override suspend fun detect() {
+        if (!isDetectionSupported) {
+            faceAuthLogger.detectionNotSupported(faceManager, faceManager?.sensorPropertiesInternal)
+            return
+        }
+        if (_isAuthRunning.value) {
+            faceAuthLogger.skippingBecauseAlreadyRunning("detection")
+            return
+        }
+
+        cancellationSignal = CancellationSignal()
+        withContext(mainDispatcher) {
+            // We always want to invoke face detect in the main thread.
+            faceAuthLogger.faceDetectionStarted()
+            faceManager?.detectFace(cancellationSignal, detectionCallback, currentUserId)
+        }
+    }
+
+    private val currentUserId: Int
+        get() = userRepository.getSelectedUserInfo().id
+
+    override fun cancel() {
+        if (cancellationSignal == null) return
+
+        cancellationSignal?.cancel()
+        cancelNotReceivedHandlerJob =
+            applicationScope.launch {
+                delay(DEFAULT_CANCEL_SIGNAL_TIMEOUT)
+                faceAuthLogger.cancelSignalNotReceived(
+                    _isAuthRunning.value,
+                    _isLockedOut.value,
+                    cancellationInProgress,
+                    faceAuthRequestedWhileCancellation
+                )
+                onFaceAuthRequestCompleted()
+            }
+        cancellationInProgress = true
+        _isAuthRunning.value = false
+    }
+
+    private var cancelNotReceivedHandlerJob: Job? = null
+
+    private val _authenticationStatus: MutableStateFlow<AuthenticationStatus?> =
+        MutableStateFlow(null)
+    override val authenticationStatus: Flow<AuthenticationStatus>
+        get() = _authenticationStatus.filterNotNull()
+
+    private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
+    override val detectionStatus: Flow<DetectionStatus>
+        get() = _detectionStatus.filterNotNull()
+
+    private val _isLockedOut = MutableStateFlow(false)
+    override val isLockedOut: Flow<Boolean> = _isLockedOut
+
+    override val isDetectionSupported =
+        faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false
+
+    private val _isAuthRunning = MutableStateFlow(false)
+    override val isAuthRunning: Flow<Boolean>
+        get() = _isAuthRunning
+
+    private val keyguardSessionId: InstanceId?
+        get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD)
+
+    companion object {
+        const val TAG = "KeyguardFaceAuthManager"
+
+        /**
+         * If no cancel signal has been received after this amount of time, assume that it is
+         * cancelled.
+         */
+        const val DEFAULT_CANCEL_SIGNAL_TIMEOUT = 3000L
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("KeyguardFaceAuthManagerImpl state:")
+        pw.println("  cancellationInProgress: $cancellationInProgress")
+        pw.println("  _isLockedOut.value: ${_isLockedOut.value}")
+        pw.println("  _isAuthRunning.value: ${_isAuthRunning.value}")
+        pw.println("  isDetectionSupported: $isDetectionSupported")
+        pw.println("  FaceManager state:")
+        pw.println("    faceManager: $faceManager")
+        pw.println("    sensorPropertiesInternal: ${faceManager?.sensorPropertiesInternal}")
+        pw.println(
+            "    supportsFaceDetection: " +
+                "${faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection}"
+        )
+        pw.println(
+            "  faceAuthRequestedWhileCancellation: ${faceAuthRequestedWhileCancellation?.reason}"
+        )
+        pw.println("  cancellationSignal: $cancellationSignal")
+        pw.println("  faceAcquiredInfoIgnoreList: $faceAcquiredInfoIgnoreList")
+        pw.println("  _authenticationStatus: ${_authenticationStatus.value}")
+        pw.println("  _detectionStatus: ${_detectionStatus.value}")
+        pw.println("  currentUserId: $currentUserId")
+        pw.println("  keyguardSessionId: $keyguardSessionId")
+        pw.println("  lockscreenBypassEnabled: $lockscreenBypassEnabled")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index e3f5e90..8ece318 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -146,7 +146,7 @@
      * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
      * slot with the given ID. The configs are sorted in descending priority order.
      */
-    fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
+    fun getCurrentSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
         val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
         return configs.filter { selections.contains(it.key) }
     }
@@ -155,7 +155,7 @@
      * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
      * are sorted in descending priority order.
      */
-    fun getSelections(): Map<String, List<String>> {
+    fun getCurrentSelections(): Map<String, List<String>> {
         return selectionManager.value.getSelections()
     }
 
@@ -187,6 +187,8 @@
                 pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
             }
             .map { (config, pickerState) ->
+                val defaultPickerState =
+                    pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Default
                 val disabledPickerState =
                     pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
                 KeyguardQuickAffordancePickerRepresentation(
@@ -198,6 +200,7 @@
                     instructions = disabledPickerState?.instructions,
                     actionText = disabledPickerState?.actionText,
                     actionComponentName = disabledPickerState?.actionComponentName,
+                    configureIntent = defaultPickerState?.configureIntent,
                 )
             }
     }
@@ -214,7 +217,7 @@
     private inner class Dumpster : Dumpable {
         override fun dump(pw: PrintWriter, args: Array<out String>) {
             val slotPickerRepresentations = getSlotPickerRepresentations()
-            val selectionsBySlotId = getSelections()
+            val selectionsBySlotId = getCurrentSelections()
             pw.println("Slots & selections:")
             slotPickerRepresentations.forEach { slotPickerRepresentation ->
                 val slotId = slotPickerRepresentation.id
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index a4fd087..76f20d25 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
+import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
@@ -79,14 +80,17 @@
      */
     val isKeyguardShowing: Flow<Boolean>
 
+    /** Is the keyguard in a unlocked state? */
+    val isKeyguardUnlocked: Flow<Boolean>
+
     /** Is an activity showing over the keyguard? */
     val isKeyguardOccluded: Flow<Boolean>
 
     /** Observable for the signal that keyguard is about to go away. */
     val isKeyguardGoingAway: Flow<Boolean>
 
-    /** Observable for whether the bouncer is showing. */
-    val isBouncerShowing: Flow<Boolean>
+    /** Is the always-on display available to be used? */
+    val isAodAvailable: Flow<Boolean>
 
     /**
      * Observable for whether we are in doze state.
@@ -144,6 +148,9 @@
     /** Source of the most recent biometric unlock, such as fingerprint or face. */
     val biometricUnlockSource: Flow<BiometricUnlockSource?>
 
+    /** Whether quick settings or quick-quick settings is visible. */
+    val isQuickSettingsVisible: Flow<Boolean>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -168,6 +175,9 @@
      * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
      */
     fun isUdfpsSupported(): Boolean
+
+    /** Sets whether quick settings or quick-quick settings is visible. */
+    fun setQuickSettingsVisible(isVisible: Boolean)
 }
 
 /** Encapsulates application state for the keyguard. */
@@ -182,6 +192,7 @@
     private val keyguardStateController: KeyguardStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val dozeTransitionListener: DozeTransitionListener,
+    private val dozeParameters: DozeParameters,
     private val authController: AuthController,
     private val dreamOverlayCallbackController: DreamOverlayCallbackController,
 ) : KeyguardRepository {
@@ -220,6 +231,31 @@
             }
             .distinctUntilChanged()
 
+    override val isAodAvailable: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DozeParameters.Callback {
+                        override fun onAlwaysOnChange() {
+                            trySendWithFailureLogging(
+                                dozeParameters.getAlwaysOn(),
+                                TAG,
+                                "updated isAodAvailable"
+                            )
+                        }
+                    }
+
+                dozeParameters.addCallback(callback)
+                // Adding the callback does not send an initial update.
+                trySendWithFailureLogging(
+                    dozeParameters.getAlwaysOn(),
+                    TAG,
+                    "initial isAodAvailable"
+                )
+
+                awaitClose { dozeParameters.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+
     override val isKeyguardOccluded: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
@@ -245,6 +281,31 @@
             }
             .distinctUntilChanged()
 
+    override val isKeyguardUnlocked: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardStateController.Callback {
+                        override fun onUnlockedChanged() {
+                            trySendWithFailureLogging(
+                                keyguardStateController.isUnlocked,
+                                TAG,
+                                "updated isKeyguardUnlocked"
+                            )
+                        }
+                    }
+
+                keyguardStateController.addCallback(callback)
+                // Adding the callback does not send an initial update.
+                trySendWithFailureLogging(
+                    keyguardStateController.isUnlocked,
+                    TAG,
+                    "initial isKeyguardUnlocked"
+                )
+
+                awaitClose { keyguardStateController.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+
     override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
         val callback =
             object : KeyguardStateController.Callback {
@@ -268,29 +329,6 @@
         awaitClose { keyguardStateController.removeCallback(callback) }
     }
 
-    override val isBouncerShowing: Flow<Boolean> = conflatedCallbackFlow {
-        val callback =
-            object : KeyguardStateController.Callback {
-                override fun onBouncerShowingChanged() {
-                    trySendWithFailureLogging(
-                        keyguardStateController.isBouncerShowing,
-                        TAG,
-                        "updated isBouncerShowing"
-                    )
-                }
-            }
-
-        keyguardStateController.addCallback(callback)
-        // Adding the callback does not send an initial update.
-        trySendWithFailureLogging(
-            keyguardStateController.isBouncerShowing,
-            TAG,
-            "initial isBouncerShowing"
-        )
-
-        awaitClose { keyguardStateController.removeCallback(callback) }
-    }
-
     override val isDozing: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
@@ -551,6 +589,9 @@
         awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
     }
 
+    private val _isQuickSettingsVisible = MutableStateFlow(false)
+    override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.value = animate
     }
@@ -565,6 +606,10 @@
 
     override fun isUdfpsSupported(): Boolean = keyguardUpdateMonitor.isUdfpsSupported
 
+    override fun setQuickSettingsVisible(isVisible: Boolean) {
+        _isQuickSettingsVisible.value = isVisible
+    }
+
     private fun statusBarStateIntToObject(value: Int): StatusBarState {
         return when (value) {
             0 -> StatusBarState.SHADE
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index 26f853f..f27f899 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -30,4 +30,19 @@
 
     @Binds
     fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository
+
+    @Binds fun devicePostureRepository(impl: DevicePostureRepositoryImpl): DevicePostureRepository
+
+    @Binds
+    fun biometricSettingsRepository(
+        impl: BiometricSettingsRepositoryImpl
+    ): BiometricSettingsRepository
+
+    @Binds
+    fun deviceEntryFingerprintAuthRepository(
+        impl: DeviceEntryFingerprintAuthRepositoryImpl
+    ): DeviceEntryFingerprintAuthRepository
+
+    @Binds
+    fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 343c2dc..100bc59 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -68,8 +68,11 @@
     /**
      * Begin a transition from one state to another. Transitions are interruptible, and will issue a
      * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one.
+     *
+     * When canceled, there are two options: to continue from the current position of the prior
+     * transition, or to reset the position. When [resetIfCanceled] == true, it will do the latter.
      */
-    fun startTransition(info: TransitionInfo): UUID?
+    fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean = false): UUID?
 
     /**
      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
@@ -130,16 +133,26 @@
         )
     }
 
-    override fun startTransition(info: TransitionInfo): UUID? {
+    override fun startTransition(
+        info: TransitionInfo,
+        resetIfCanceled: Boolean,
+    ): UUID? {
         if (lastStep.from == info.from && lastStep.to == info.to) {
             Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
             return null
         }
-        if (lastStep.transitionState != TransitionState.FINISHED) {
-            Log.i(TAG, "Transition still active: $lastStep, canceling")
-        }
+        val startingValue =
+            if (lastStep.transitionState != TransitionState.FINISHED) {
+                Log.i(TAG, "Transition still active: $lastStep, canceling")
+                if (resetIfCanceled) {
+                    0f
+                } else {
+                    lastStep.value
+                }
+            } else {
+                0f
+            }
 
-        val startingValue = 1f - lastStep.value
         lastAnimator?.cancel()
         lastAnimator = info.animator
 
@@ -206,7 +219,7 @@
             return
         }
 
-        if (state == TransitionState.FINISHED) {
+        if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
             updateTransitionId = null
         }
 
@@ -224,10 +237,7 @@
     }
 
     private fun trace(step: TransitionStep, isManual: Boolean) {
-        if (
-            step.transitionState != TransitionState.STARTED &&
-                step.transitionState != TransitionState.FINISHED
-        ) {
+        if (step.transitionState == TransitionState.RUNNING) {
             return
         }
         val traceName =
@@ -240,7 +250,10 @@
         val traceCookie = traceName.hashCode()
         if (step.transitionState == TransitionState.STARTED) {
             Trace.beginAsyncSection(traceName, traceCookie)
-        } else if (step.transitionState == TransitionState.FINISHED) {
+        } else if (
+            step.transitionState == TransitionState.FINISHED ||
+                step.transitionState == TransitionState.CANCELED
+        ) {
             Trace.endAsyncSection(traceName, traceCookie)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
new file mode 100644
index 0000000..d90f328
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.app.trust.TrustManager
+import com.android.keyguard.logging.TrustRepositoryLogger
+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.dagger.qualifiers.Application
+import com.android.systemui.keyguard.shared.model.TrustModel
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+
+/** Encapsulates any state relevant to trust agents and trust grants. */
+interface TrustRepository {
+    /** Flow representing whether the current user is trusted. */
+    val isCurrentUserTrusted: Flow<Boolean>
+}
+
+@SysUISingleton
+class TrustRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val userRepository: UserRepository,
+    private val trustManager: TrustManager,
+    private val logger: TrustRepositoryLogger,
+) : TrustRepository {
+    private val latestTrustModelForUser = mutableMapOf<Int, TrustModel>()
+
+    private val trust =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TrustManager.TrustListener {
+                        override fun onTrustChanged(
+                            enabled: Boolean,
+                            newlyUnlocked: Boolean,
+                            userId: Int,
+                            flags: Int,
+                            grantMsgs: List<String>?
+                        ) {
+                            logger.onTrustChanged(enabled, newlyUnlocked, userId, flags, grantMsgs)
+                            trySendWithFailureLogging(
+                                TrustModel(enabled, userId),
+                                TrustRepositoryLogger.TAG,
+                                "onTrustChanged"
+                            )
+                        }
+
+                        override fun onTrustError(message: CharSequence?) = Unit
+
+                        override fun onTrustManagedChanged(enabled: Boolean, userId: Int) = Unit
+                    }
+                trustManager.registerTrustListener(callback)
+                logger.trustListenerRegistered()
+                awaitClose {
+                    logger.trustListenerUnregistered()
+                    trustManager.unregisterTrustListener(callback)
+                }
+            }
+            .onEach {
+                latestTrustModelForUser[it.userId] = it
+                logger.trustModelEmitted(it)
+            }
+            .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
+
+    override val isCurrentUserTrusted: Flow<Boolean>
+        get() =
+            combine(trust, userRepository.selectedUserInfo, ::Pair)
+                .map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false }
+                .distinctUntilChanged()
+                .onEach { logger.isCurrentUserTrusted(it) }
+                .onStart { emit(false) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
index 0e865ce..fa6efa5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
@@ -29,16 +29,9 @@
 ) :
     SharedPreferencesBackupHelper(
         context,
-        if (UserFileManagerImpl.isPrimaryUser(userId)) {
-            KeyguardQuickAffordanceSelectionManager.FILE_NAME
-        } else {
-            UserFileManagerImpl.secondaryUserFile(
-                    context = context,
-                    fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
-                    directoryName = UserFileManagerImpl.SHARED_PREFS,
-                    userId = userId,
-                )
-                .also { UserFileManagerImpl.ensureParentDirExists(it) }
-                .toString()
-        }
+        UserFileManagerImpl.createFile(
+                userId = userId,
+                fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
+            )
+            .getPath()
     )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
new file mode 100644
index 0000000..0140529
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.keyguard.KeyguardUpdateMonitor
+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.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */
+@SysUISingleton
+class AlternateBouncerInteractor
+@Inject
+constructor(
+    private val keyguardStateController: KeyguardStateController,
+    private val bouncerRepository: KeyguardBouncerRepository,
+    private val biometricSettingsRepository: BiometricSettingsRepository,
+    private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    private val systemClock: SystemClock,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    featureFlags: FeatureFlags,
+) {
+    val isModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)
+    var legacyAlternateBouncer: LegacyAlternateBouncer? = null
+    var legacyAlternateBouncerVisibleTime: Long = NOT_VISIBLE
+
+    val isVisible: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
+
+    /**
+     * Sets the correct bouncer states to show the alternate bouncer if it can show.
+     * @return whether alternateBouncer is visible
+     */
+    fun show(): Boolean {
+        return when {
+            isModernAlternateBouncerEnabled -> {
+                bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint())
+                isVisibleState()
+            }
+            canShowAlternateBouncerForFingerprint() -> {
+                if (legacyAlternateBouncer?.showAlternateBouncer() == true) {
+                    legacyAlternateBouncerVisibleTime = systemClock.uptimeMillis()
+                    true
+                } else {
+                    false
+                }
+            }
+            else -> false
+        }
+    }
+
+    /**
+     * Sets the correct bouncer states to hide the bouncer. Should only be called through
+     * StatusBarKeyguardViewManager until ScrimController is refactored to use
+     * alternateBouncerInteractor.
+     * @return true if the alternate bouncer was newly hidden, else false.
+     */
+    fun hide(): Boolean {
+        return if (isModernAlternateBouncerEnabled) {
+            val wasAlternateBouncerVisible = isVisibleState()
+            bouncerRepository.setAlternateVisible(false)
+            wasAlternateBouncerVisible && !isVisibleState()
+        } else {
+            legacyAlternateBouncer?.hideAlternateBouncer() ?: false
+        }
+    }
+
+    fun isVisibleState(): Boolean {
+        return if (isModernAlternateBouncerEnabled) {
+            bouncerRepository.alternateBouncerVisible.value
+        } else {
+            legacyAlternateBouncer?.isShowingAlternateBouncer ?: false
+        }
+    }
+
+    fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+        bouncerRepository.setAlternateBouncerUIAvailable(isAvailable)
+    }
+
+    fun canShowAlternateBouncerForFingerprint(): Boolean {
+        return if (isModernAlternateBouncerEnabled) {
+            bouncerRepository.alternateBouncerUIAvailable.value &&
+                biometricSettingsRepository.isFingerprintEnrolled.value &&
+                biometricSettingsRepository.isStrongBiometricAllowed.value &&
+                biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
+                !deviceEntryFingerprintAuthRepository.isLockedOut.value &&
+                !keyguardStateController.isUnlocked
+        } else {
+            legacyAlternateBouncer != null &&
+                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true)
+        }
+    }
+
+    /**
+     * Whether the alt bouncer has shown for a minimum time before allowing touches to dismiss the
+     * alternate bouncer and show the primary bouncer.
+     */
+    fun hasAlternateBouncerShownWithMinTime(): Boolean {
+        return if (isModernAlternateBouncerEnabled) {
+            (systemClock.uptimeMillis() - bouncerRepository.lastAlternateBouncerVisibleTime) >
+                MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS
+        } else {
+            systemClock.uptimeMillis() - legacyAlternateBouncerVisibleTime > 200
+        }
+    }
+
+    companion object {
+        private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L
+        private const val NOT_VISIBLE = -1L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
new file mode 100644
index 0000000..310f44d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class FromAlternateBouncerTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor(FromAlternateBouncerTransitionInteractor::class.simpleName!!) {
+
+    override fun start() {
+        listenForAlternateBouncerToGone()
+        listenForAlternateBouncerToLockscreenAodOrDozing()
+        listenForAlternateBouncerToPrimaryBouncer()
+    }
+
+    private fun listenForAlternateBouncerToLockscreenAodOrDozing() {
+        scope.launch {
+            keyguardInteractor.alternateBouncerShowing
+                // Add a slight delay, as alternateBouncer and primaryBouncer showing event changes
+                // will arrive with a small gap in time. This prevents a transition to LOCKSCREEN
+                // happening prematurely.
+                .onEach { delay(50) }
+                .sample(
+                    combine(
+                        keyguardInteractor.primaryBouncerShowing,
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        keyguardInteractor.wakefulnessModel,
+                        keyguardInteractor.isAodAvailable,
+                        ::toQuad
+                    ),
+                    ::toQuint
+                )
+                .collect {
+                    (
+                        isAlternateBouncerShowing,
+                        isPrimaryBouncerShowing,
+                        lastStartedTransitionStep,
+                        wakefulnessState,
+                        isAodAvailable
+                    ) ->
+                    if (
+                        !isAlternateBouncerShowing &&
+                            !isPrimaryBouncerShowing &&
+                            lastStartedTransitionStep.to == KeyguardState.ALTERNATE_BOUNCER
+                    ) {
+                        val to =
+                            if (
+                                wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
+                                    wakefulnessState.state == WakefulnessState.ASLEEP
+                            ) {
+                                if (isAodAvailable) {
+                                    KeyguardState.AOD
+                                } else {
+                                    KeyguardState.DOZING
+                                }
+                            } else {
+                                KeyguardState.LOCKSCREEN
+                            }
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.ALTERNATE_BOUNCER,
+                                to = to,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun listenForAlternateBouncerToGone() {
+        scope.launch {
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+                .collect { (isKeyguardGoingAway, keyguardState) ->
+                    if (isKeyguardGoingAway && keyguardState == KeyguardState.ALTERNATE_BOUNCER) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.ALTERNATE_BOUNCER,
+                                to = KeyguardState.GONE,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun listenForAlternateBouncerToPrimaryBouncer() {
+        scope.launch {
+            keyguardInteractor.primaryBouncerShowing
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { (isPrimaryBouncerShowing, startedKeyguardState) ->
+                    if (
+                        isPrimaryBouncerShowing &&
+                            startedKeyguardState.to == KeyguardState.ALTERNATE_BOUNCER
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.ALTERNATE_BOUNCER,
+                                to = KeyguardState.PRIMARY_BOUNCER,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            interpolator = Interpolators.LINEAR
+            duration = TRANSITION_DURATION_MS
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
deleted file mode 100644
index 0e9c447..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.keyguard.shared.model.WakefulnessState
-import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.util.kotlin.sample
-import java.util.UUID
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class FromBouncerTransitionInteractor
-@Inject
-constructor(
-    @Application private val scope: CoroutineScope,
-    private val keyguardInteractor: KeyguardInteractor,
-    private val shadeRepository: ShadeRepository,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(FromBouncerTransitionInteractor::class.simpleName!!) {
-
-    private var transitionId: UUID? = null
-
-    override fun start() {
-        listenForBouncerToGone()
-        listenForBouncerToLockscreenOrAod()
-    }
-
-    private fun listenForBouncerToLockscreenOrAod() {
-        scope.launch {
-            keyguardInteractor.isBouncerShowing
-                .sample(
-                    combine(
-                        keyguardInteractor.wakefulnessModel,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { triple ->
-                    val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
-                    if (
-                        !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
-                    ) {
-                        val to =
-                            if (
-                                wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
-                                    wakefulnessState.state == WakefulnessState.ASLEEP
-                            ) {
-                                KeyguardState.AOD
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.BOUNCER,
-                                to = to,
-                                animator = getAnimator(),
-                            )
-                        )
-                    }
-                }
-        }
-    }
-
-    private fun listenForBouncerToGone() {
-        scope.launch {
-            keyguardInteractor.isKeyguardGoingAway
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
-                .collect { pair ->
-                    val (isKeyguardGoingAway, keyguardState) = pair
-                    if (isKeyguardGoingAway && keyguardState == KeyguardState.BOUNCER) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.BOUNCER,
-                                to = KeyguardState.GONE,
-                                animator = getAnimator(),
-                            )
-                        )
-                    }
-                }
-        }
-    }
-
-    private fun getAnimator(): ValueAnimator {
-        return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(TRANSITION_DURATION_MS)
-        }
-    }
-
-    companion object {
-        private const val TRANSITION_DURATION_MS = 300L
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index fd2d271..86f65dde 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,9 +21,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration
@@ -44,16 +45,16 @@
 
     override fun start() {
         listenForDozingToLockscreen()
+        listenForDozingToGone()
     }
 
     private fun listenForDozingToLockscreen() {
         scope.launch {
-            keyguardInteractor.dozeTransitionModel
+            keyguardInteractor.wakefulnessModel
                 .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
-                .collect { pair ->
-                    val (dozeTransitionModel, lastStartedTransition) = pair
+                .collect { (wakefulnessModel, lastStartedTransition) ->
                     if (
-                        isDozeOff(dozeTransitionModel.to) &&
+                        isWakingOrStartingToWake(wakefulnessModel) &&
                             lastStartedTransition.to == KeyguardState.DOZING
                     ) {
                         keyguardTransitionRepository.startTransition(
@@ -69,6 +70,28 @@
         }
     }
 
+    private fun listenForDozingToGone() {
+        scope.launch {
+            keyguardInteractor.biometricUnlockState
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { (biometricUnlockState, lastStartedTransition) ->
+                    if (
+                        lastStartedTransition.to == KeyguardState.DOZING &&
+                            isWakeAndUnlock(biometricUnlockState)
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.DOZING,
+                                KeyguardState.GONE,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
     private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
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 3b09ae7..3beac0b 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
@@ -21,7 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -31,8 +31,10 @@
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -54,9 +56,7 @@
 
     private fun listenForDreamingToLockscreen() {
         scope.launch {
-            // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
-            // otherwise would have gone through OCCLUDED first
-            keyguardInteractor.isDreamingWithOverlay
+            keyguardInteractor.isAbleToDream
                 .sample(
                     combine(
                         keyguardInteractor.dozeTransitionModel,
@@ -65,8 +65,7 @@
                     ),
                     ::toTriple
                 )
-                .collect { triple ->
-                    val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
+                .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) ->
                     if (
                         !isDreaming &&
                             isDozeOff(dozeTransitionModel.to) &&
@@ -88,6 +87,9 @@
     private fun listenForDreamingToOccluded() {
         scope.launch {
             keyguardInteractor.isDreaming
+                // Add a slight delay, as dreaming and occluded events will arrive with a small gap
+                // in time. This prevents a transition to OCCLUSION happening prematurely.
+                .onEach { delay(50) }
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardOccluded,
@@ -96,8 +98,7 @@
                     ),
                     ::toTriple
                 )
-                .collect { triple ->
-                    val (isDreaming, isOccluded, lastStartedTransition) = triple
+                .collect { (isDreaming, isOccluded, lastStartedTransition) ->
                     if (
                         isOccluded &&
                             !isDreaming &&
@@ -123,24 +124,18 @@
 
     private fun listenForDreamingToGone() {
         scope.launch {
-            keyguardInteractor.biometricUnlockState
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { pair ->
-                    val (biometricUnlockState, keyguardState) = pair
-                    if (
-                        keyguardState == KeyguardState.DREAMING &&
-                            isWakeAndUnlock(biometricUnlockState)
-                    ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.DREAMING,
-                                KeyguardState.GONE,
-                                getAnimator(),
-                            )
+            keyguardInteractor.biometricUnlockState.collect { biometricUnlockState ->
+                if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) {
+                    keyguardTransitionRepository.startTransition(
+                        TransitionInfo(
+                            name,
+                            KeyguardState.DREAMING,
+                            KeyguardState.GONE,
+                            getAnimator(),
                         )
-                    }
+                    )
                 }
+            }
         }
     }
 
@@ -151,8 +146,7 @@
                     keyguardTransitionInteractor.finishedKeyguardState,
                     ::Pair
                 )
-                .collect { pair ->
-                    val (dozeTransitionModel, keyguardState) = pair
+                .collect { (dozeTransitionModel, keyguardState) ->
                     if (
                         dozeTransitionModel.to == DozeStateModel.DOZE &&
                             keyguardState == KeyguardState.DREAMING
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 553fafe..b5bcd45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -26,7 +26,10 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -40,22 +43,23 @@
 ) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) {
 
     override fun start() {
-        listenForGoneToAod()
+        listenForGoneToAodOrDozing()
         listenForGoneToDreaming()
+        listenForGoneToLockscreen()
     }
 
-    private fun listenForGoneToDreaming() {
+    // Primarily for when the user chooses to lock down the device
+    private fun listenForGoneToLockscreen() {
         scope.launch {
-            keyguardInteractor.isAbleToDream
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { pair ->
-                    val (isAbleToDream, keyguardState) = pair
-                    if (isAbleToDream && keyguardState == KeyguardState.GONE) {
+            keyguardInteractor.isKeyguardShowing
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { (isKeyguardShowing, lastStartedStep) ->
+                    if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) {
                         keyguardTransitionRepository.startTransition(
                             TransitionInfo(
                                 name,
                                 KeyguardState.GONE,
-                                KeyguardState.DREAMING,
+                                KeyguardState.LOCKSCREEN,
                                 getAnimator(),
                             )
                         )
@@ -64,21 +68,50 @@
         }
     }
 
-    private fun listenForGoneToAod() {
+    private fun listenForGoneToDreaming() {
+        scope.launch {
+            keyguardInteractor.isAbleToDream
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { (isAbleToDream, lastStartedStep) ->
+                    if (isAbleToDream && lastStartedStep.to == KeyguardState.GONE) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.GONE,
+                                KeyguardState.DREAMING,
+                                getAnimator(TO_DREAMING_DURATION),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun listenForGoneToAodOrDozing() {
         scope.launch {
             keyguardInteractor.wakefulnessModel
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { pair ->
-                    val (wakefulnessState, keyguardState) = pair
+                .sample(
+                    combine(
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        keyguardInteractor.isAodAvailable,
+                        ::Pair
+                    ),
+                    ::toTriple
+                )
+                .collect { (wakefulnessState, lastStartedStep, isAodAvailable) ->
                     if (
-                        keyguardState == KeyguardState.GONE &&
+                        lastStartedStep.to == KeyguardState.GONE &&
                             wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
                     ) {
                         keyguardTransitionRepository.startTransition(
                             TransitionInfo(
                                 name,
                                 KeyguardState.GONE,
-                                KeyguardState.AOD,
+                                if (isAodAvailable) {
+                                    KeyguardState.AOD
+                                } else {
+                                    KeyguardState.DOZING
+                                },
                                 getAnimator(),
                             )
                         )
@@ -87,14 +120,15 @@
         }
     }
 
-    private fun getAnimator(): ValueAnimator {
+    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
-            setDuration(TRANSITION_DURATION_MS)
+            setDuration(duration.inWholeMilliseconds)
         }
     }
 
     companion object {
-        private const val TRANSITION_DURATION_MS = 500L
+        private val DEFAULT_DURATION = 500.milliseconds
+        val TO_DREAMING_DURATION = 933.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 326acc9..911861d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -21,17 +21,18 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.util.kotlin.sample
 import java.util.UUID
 import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
@@ -46,15 +47,15 @@
     private val keyguardTransitionRepository: KeyguardTransitionRepository,
 ) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
 
-    private var transitionId: UUID? = null
-
     override fun start() {
         listenForLockscreenToGone()
         listenForLockscreenToOccluded()
-        listenForLockscreenToAod()
-        listenForLockscreenToBouncer()
+        listenForLockscreenToCamera()
+        listenForLockscreenToAodOrDozing()
+        listenForLockscreenToPrimaryBouncer()
         listenForLockscreenToDreaming()
-        listenForLockscreenToBouncerDragging()
+        listenForLockscreenToPrimaryBouncerDragging()
+        listenForLockscreenToAlternateBouncer()
     }
 
     private fun listenForLockscreenToDreaming() {
@@ -69,7 +70,7 @@
                                 name,
                                 KeyguardState.LOCKSCREEN,
                                 KeyguardState.DREAMING,
-                                getAnimator(),
+                                getAnimator(TO_DREAMING_DURATION),
                             )
                         )
                     }
@@ -77,9 +78,9 @@
         }
     }
 
-    private fun listenForLockscreenToBouncer() {
+    private fun listenForLockscreenToPrimaryBouncer() {
         scope.launch {
-            keyguardInteractor.isBouncerShowing
+            keyguardInteractor.primaryBouncerShowing
                 .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isBouncerShowing, lastStartedTransitionStep) = pair
@@ -90,7 +91,30 @@
                             TransitionInfo(
                                 ownerName = name,
                                 from = KeyguardState.LOCKSCREEN,
-                                to = KeyguardState.BOUNCER,
+                                to = KeyguardState.PRIMARY_BOUNCER,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun listenForLockscreenToAlternateBouncer() {
+        scope.launch {
+            keyguardInteractor.alternateBouncerShowing
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { pair ->
+                    val (isAlternateBouncerShowing, lastStartedTransitionStep) = pair
+                    if (
+                        isAlternateBouncerShowing &&
+                            lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.LOCKSCREEN,
+                                to = KeyguardState.ALTERNATE_BOUNCER,
                                 animator = getAnimator(),
                             )
                         )
@@ -100,42 +124,65 @@
     }
 
     /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
-    private fun listenForLockscreenToBouncerDragging() {
+    private fun listenForLockscreenToPrimaryBouncerDragging() {
+        var transitionId: UUID? = null
         scope.launch {
             shadeRepository.shadeModel
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.finishedKeyguardState,
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.statusBarState,
-                        ::Pair
+                        keyguardInteractor.isKeyguardUnlocked,
+                        ::toTriple
                     ),
-                    ::toTriple
+                    ::toQuad
                 )
-                .collect { triple ->
-                    val (shadeModel, keyguardState, statusBarState) = triple
-
+                .collect { (shadeModel, keyguardState, statusBarState, isKeyguardUnlocked) ->
                     val id = transitionId
                     if (id != null) {
-                        // An existing `id` means a transition is started, and calls to
-                        // `updateTransition` will control it until FINISHED
-                        keyguardTransitionRepository.updateTransition(
-                            id,
-                            1f - shadeModel.expansionAmount,
+                        if (keyguardState.to == KeyguardState.PRIMARY_BOUNCER) {
+                            // An existing `id` means a transition is started, and calls to
+                            // `updateTransition` will control it until FINISHED or CANCELED
+                            var nextState =
+                                if (shadeModel.expansionAmount == 0f) {
+                                    TransitionState.FINISHED
+                                } else if (shadeModel.expansionAmount == 1f) {
+                                    TransitionState.CANCELED
+                                } else {
+                                    TransitionState.RUNNING
+                                }
+                            keyguardTransitionRepository.updateTransition(
+                                id,
+                                1f - shadeModel.expansionAmount,
+                                nextState,
+                            )
+
                             if (
-                                shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
+                                nextState == TransitionState.CANCELED ||
+                                    nextState == TransitionState.FINISHED
                             ) {
                                 transitionId = null
-                                TransitionState.FINISHED
-                            } else {
-                                TransitionState.RUNNING
                             }
-                        )
+
+                            // If canceled, just put the state back
+                            if (nextState == TransitionState.CANCELED) {
+                                keyguardTransitionRepository.startTransition(
+                                    TransitionInfo(
+                                        ownerName = name,
+                                        from = KeyguardState.PRIMARY_BOUNCER,
+                                        to = KeyguardState.LOCKSCREEN,
+                                        animator = getAnimator(0.milliseconds)
+                                    )
+                                )
+                            }
+                        }
                     } else {
                         // TODO (b/251849525): Remove statusbarstate check when that state is
                         // integrated into KeyguardTransitionRepository
                         if (
-                            keyguardState == KeyguardState.LOCKSCREEN &&
+                            keyguardState.to == KeyguardState.LOCKSCREEN &&
                                 shadeModel.isUserDragging &&
+                                !isKeyguardUnlocked &&
                                 statusBarState == KEYGUARD
                         ) {
                             transitionId =
@@ -143,7 +190,7 @@
                                     TransitionInfo(
                                         ownerName = name,
                                         from = KeyguardState.LOCKSCREEN,
-                                        to = KeyguardState.BOUNCER,
+                                        to = KeyguardState.PRIMARY_BOUNCER,
                                         animator = null,
                                     )
                                 )
@@ -184,17 +231,14 @@
                     ),
                     ::toTriple
                 )
-                .collect { triple ->
-                    val (isOccluded, keyguardState, isDreaming) = triple
-                    // Occlusion signals come from the framework, and should interrupt any
-                    // existing transition
-                    if (isOccluded && !isDreaming) {
+                .collect { (isOccluded, keyguardState, isDreaming) ->
+                    if (isOccluded && !isDreaming && keyguardState == KeyguardState.LOCKSCREEN) {
                         keyguardTransitionRepository.startTransition(
                             TransitionInfo(
                                 name,
                                 keyguardState,
                                 KeyguardState.OCCLUDED,
-                                getAnimator(),
+                                getAnimator(TO_OCCLUDED_DURATION),
                             )
                         )
                     }
@@ -202,19 +246,59 @@
         }
     }
 
-    private fun listenForLockscreenToAod() {
+    /** This signal may come in before the occlusion signal, and can provide a custom transition */
+    private fun listenForLockscreenToCamera() {
         scope.launch {
-            keyguardInteractor
-                .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+            keyguardInteractor.onCameraLaunchDetected
                 .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
-                .collect { pair ->
-                    val (dozeToAod, lastStartedStep) = pair
-                    if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+                .collect { (_, lastStartedStep) ->
+                    // DREAMING/AOD/OFF may trigger on the first power button push, so include this
+                    // state in order to cancel and correct the transition
+                    if (
+                        lastStartedStep.to == KeyguardState.LOCKSCREEN ||
+                            lastStartedStep.to == KeyguardState.DREAMING ||
+                            lastStartedStep.to == KeyguardState.DOZING ||
+                            lastStartedStep.to == KeyguardState.AOD ||
+                            lastStartedStep.to == KeyguardState.OFF
+                    ) {
                         keyguardTransitionRepository.startTransition(
                             TransitionInfo(
                                 name,
                                 KeyguardState.LOCKSCREEN,
-                                KeyguardState.AOD,
+                                KeyguardState.OCCLUDED,
+                                getAnimator(TO_OCCLUDED_DURATION),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun listenForLockscreenToAodOrDozing() {
+        scope.launch {
+            keyguardInteractor.wakefulnessModel
+                .sample(
+                    combine(
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        keyguardInteractor.isAodAvailable,
+                        ::Pair
+                    ),
+                    ::toTriple
+                )
+                .collect { (wakefulnessState, lastStartedStep, isAodAvailable) ->
+                    if (
+                        lastStartedStep.to == KeyguardState.LOCKSCREEN &&
+                            wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.LOCKSCREEN,
+                                if (isAodAvailable) {
+                                    KeyguardState.AOD
+                                } else {
+                                    KeyguardState.DOZING
+                                },
                                 getAnimator(),
                             )
                         )
@@ -223,14 +307,16 @@
         }
     }
 
-    private fun getAnimator(): ValueAnimator {
+    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
-            setDuration(TRANSITION_DURATION_MS)
+            setDuration(duration.inWholeMilliseconds)
         }
     }
 
     companion object {
-        private const val TRANSITION_DURATION_MS = 500L
+        private val DEFAULT_DURATION = 500.milliseconds
+        val TO_DREAMING_DURATION = 933.milliseconds
+        val TO_OCCLUDED_DURATION = 450.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 8878901..2dc8fee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -23,12 +23,14 @@
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -44,6 +46,7 @@
     override fun start() {
         listenForOccludedToLockscreen()
         listenForOccludedToDreaming()
+        listenForOccludedToAodOrDozing()
     }
 
     private fun listenForOccludedToDreaming() {
@@ -70,8 +73,7 @@
         scope.launch {
             keyguardInteractor.isKeyguardOccluded
                 .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
-                .collect { pair ->
-                    val (isOccluded, lastStartedKeyguardState) = pair
+                .collect { (isOccluded, lastStartedKeyguardState) ->
                     // Occlusion signals come from the framework, and should interrupt any
                     // existing transition
                     if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) {
@@ -88,6 +90,39 @@
         }
     }
 
+    private fun listenForOccludedToAodOrDozing() {
+        scope.launch {
+            keyguardInteractor.wakefulnessModel
+                .sample(
+                    combine(
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        keyguardInteractor.isAodAvailable,
+                        ::Pair
+                    ),
+                    ::toTriple
+                )
+                .collect { (wakefulnessState, lastStartedStep, isAodAvailable) ->
+                    if (
+                        lastStartedStep.to == KeyguardState.OCCLUDED &&
+                            wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.OCCLUDED,
+                                if (isAodAvailable) {
+                                    KeyguardState.AOD
+                                } else {
+                                    KeyguardState.DOZING
+                                },
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
     private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
new file mode 100644
index 0000000..94961cb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class FromPrimaryBouncerTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val keyguardSecurityModel: KeyguardSecurityModel,
+) : TransitionInteractor(FromPrimaryBouncerTransitionInteractor::class.simpleName!!) {
+
+    override fun start() {
+        listenForPrimaryBouncerToGone()
+        listenForPrimaryBouncerToLockscreenAodOrDozing()
+    }
+
+    private fun listenForPrimaryBouncerToLockscreenAodOrDozing() {
+        scope.launch {
+            keyguardInteractor.primaryBouncerShowing
+                .sample(
+                    combine(
+                        keyguardInteractor.wakefulnessModel,
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        keyguardInteractor.isAodAvailable,
+                        ::toTriple
+                    ),
+                    ::toQuad
+                )
+                .collect {
+                    (isBouncerShowing, wakefulnessState, lastStartedTransitionStep, isAodAvailable)
+                    ->
+                    if (
+                        !isBouncerShowing &&
+                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER
+                    ) {
+                        val to =
+                            if (
+                                wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
+                                    wakefulnessState.state == WakefulnessState.ASLEEP
+                            ) {
+                                if (isAodAvailable) {
+                                    KeyguardState.AOD
+                                } else {
+                                    KeyguardState.DOZING
+                                }
+                            } else {
+                                KeyguardState.LOCKSCREEN
+                            }
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.PRIMARY_BOUNCER,
+                                to = to,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun listenForPrimaryBouncerToGone() {
+        scope.launch {
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { (isKeyguardGoingAway, lastStartedTransitionStep) ->
+                    if (
+                        isKeyguardGoingAway &&
+                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER
+                    ) {
+                        val securityMode =
+                            keyguardSecurityModel.getSecurityMode(
+                                KeyguardUpdateMonitor.getCurrentUser()
+                            )
+                        // IME for password requires a slightly faster animation
+                        val duration =
+                            if (securityMode == Password) {
+                                TO_GONE_SHORT_DURATION
+                            } else {
+                                TO_GONE_DURATION
+                            }
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.PRIMARY_BOUNCER,
+                                to = KeyguardState.GONE,
+                                animator = getAnimator(duration),
+                            ),
+                            resetIfCanceled = true,
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(duration.inWholeMilliseconds)
+        }
+    }
+
+    companion object {
+        private val DEFAULT_DURATION = 300.milliseconds
+        val TO_GONE_DURATION = 250.milliseconds
+        val TO_GONE_SHORT_DURATION = 200.milliseconds
+    }
+}
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 402c179..ec99049 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
@@ -17,21 +17,37 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.app.StatusBarManager
 import android.graphics.Point
+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.KeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 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.
@@ -41,6 +57,9 @@
 @Inject
 constructor(
     private val repository: KeyguardRepository,
+    private val commandQueue: CommandQueue,
+    featureFlags: FeatureFlags,
+    bouncerRepository: KeyguardBouncerRepository,
 ) {
     /**
      * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -49,6 +68,8 @@
     val dozeAmount: Flow<Float> = repository.linearDozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
+    /** Whether Always-on Display mode is available. */
+    val isAodAvailable: Flow<Boolean> = repository.isAodAvailable
     /** Doze transition information. */
     val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel
     /**
@@ -58,31 +79,67 @@
     val isDreaming: Flow<Boolean> = repository.isDreaming
     /** Whether the system is dreaming with an overlay active */
     val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
+    /** Event for when the camera gesture is detected */
+    val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> = conflatedCallbackFlow {
+        val callback =
+            object : CommandQueue.Callbacks {
+                override fun onCameraLaunchGestureDetected(source: Int) {
+                    trySendWithFailureLogging(
+                        cameraLaunchSourceIntToModel(source),
+                        TAG,
+                        "updated onCameraLaunchGestureDetected"
+                    )
+                }
+            }
+
+        commandQueue.addCallback(callback)
+
+        awaitClose { commandQueue.removeCallback(callback) }
+    }
+
+    /** The device wake/sleep state */
+    val wakefulnessModel: Flow<WakefulnessModel> = repository.wakefulness
 
     /**
      * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
      * that doze mode is not running and DREAMING is ok to commence.
+     *
+     * Allow a brief moment to prevent rapidly oscillating between true/false signals.
      */
     val isAbleToDream: Flow<Boolean> =
         merge(isDreaming, isDreamingWithOverlay)
-            .sample(
+            .combine(
                 dozeTransitionModel,
                 { isDreaming, dozeTransitionModel ->
                     isDreaming && isDozeOff(dozeTransitionModel.to)
                 }
             )
+            .sample(
+                wakefulnessModel,
+                { isAbleToDream, wakefulnessModel ->
+                    isAbleToDream && isWakingOrStartingToWake(wakefulnessModel)
+                }
+            )
+            .flatMapLatest { isAbleToDream ->
+                flow {
+                    delay(50)
+                    emit(isAbleToDream)
+                }
+            }
             .distinctUntilChanged()
 
     /** Whether the keyguard is showing or not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+    /** Whether the keyguard is unlocked or not. */
+    val isKeyguardUnlocked: Flow<Boolean> = repository.isKeyguardUnlocked
     /** Whether the keyguard is occluded (covered by an activity). */
     val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
     /** Whether the keyguard is going away. */
     val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
-    /** Whether the bouncer is showing or not. */
-    val isBouncerShowing: Flow<Boolean> = repository.isBouncerShowing
-    /** The device wake/sleep state */
-    val wakefulnessModel: Flow<WakefulnessModel> = repository.wakefulness
+    /** Whether the primary bouncer is showing or not. */
+    val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerVisible
+    /** Whether the alternate bouncer is showing or not. */
+    val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
     /** Observable for the [StatusBarState] */
     val statusBarState: Flow<StatusBarState> = repository.statusBarState
     /**
@@ -91,6 +148,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,
+                    bouncerRepository.primaryBouncerVisible,
+                    onCameraLaunchDetected,
+                ) { isKeyguardVisible, isPrimaryBouncerShowing, cameraLaunchEvent ->
+                    when {
+                        isKeyguardVisible -> false
+                        isPrimaryBouncerShowing -> 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
 
@@ -103,4 +183,26 @@
     fun isKeyguardShowing(): Boolean {
         return repository.isKeyguardShowing()
     }
+
+    private fun cameraLaunchSourceIntToModel(value: Int): CameraLaunchSourceModel {
+        return when (value) {
+            StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE -> CameraLaunchSourceModel.WIGGLE
+            StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP ->
+                CameraLaunchSourceModel.POWER_DOUBLE_TAP
+            StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER ->
+                CameraLaunchSourceModel.LIFT_TRIGGER
+            StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE ->
+                CameraLaunchSourceModel.QUICK_AFFORDANCE
+            else -> throw IllegalArgumentException("Invalid CameraLaunchSourceModel value: $value")
+        }
+    }
+
+    /** Sets whether quick settings or quick-quick settings is visible. */
+    fun setQuickSettingsVisible(isVisible: Boolean) {
+        repository.setQuickSettingsVisible(isVisible)
+    }
+
+    companion object {
+        private const val TAG = "KeyguardInteractor"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
new file mode 100644
index 0000000..6525a13
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.shared.model.Position
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+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.domain.model.KeyguardSettingsPopupMenuModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.plugins.ActivityStarter
+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
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Business logic for use-cases related to the keyguard long-press feature. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class KeyguardLongPressInteractor
+@Inject
+constructor(
+    @Application unsafeContext: Context,
+    @Application scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    repository: KeyguardRepository,
+    private val activityStarter: ActivityStarter,
+    private val logger: UiEventLogger,
+    private val featureFlags: FeatureFlags,
+    broadcastDispatcher: BroadcastDispatcher,
+) {
+    private val appContext = unsafeContext.applicationContext
+
+    private val _isLongPressHandlingEnabled: StateFlow<Boolean> =
+        if (isFeatureEnabled()) {
+                combine(
+                    transitionInteractor.finishedKeyguardState.map {
+                        it == KeyguardState.LOCKSCREEN
+                    },
+                    repository.isQuickSettingsVisible,
+                ) { isFullyTransitionedToLockScreen, isQuickSettingsVisible ->
+                    isFullyTransitionedToLockScreen && !isQuickSettingsVisible
+                }
+            } else {
+                flowOf(false)
+            }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
+
+    /** Whether the long-press handling feature should be enabled. */
+    val isLongPressHandlingEnabled: Flow<Boolean> = _isLongPressHandlingEnabled
+
+    private val _menu = MutableStateFlow<KeyguardSettingsPopupMenuModel?>(null)
+    /** Model for a menu that should be shown; `null` when no menu should be shown. */
+    val menu: Flow<KeyguardSettingsPopupMenuModel?> =
+        isLongPressHandlingEnabled.flatMapLatest { isEnabled ->
+            if (isEnabled) {
+                _menu
+            } else {
+                flowOf(null)
+            }
+        }
+
+    init {
+        if (isFeatureEnabled()) {
+            broadcastDispatcher
+                .broadcastFlow(
+                    IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+                )
+                .onEach { hideMenu() }
+                .launchIn(scope)
+        }
+    }
+
+    /** Notifies that the user has long-pressed on the lock screen. */
+    fun onLongPress(x: Int, y: Int) {
+        if (!_isLongPressHandlingEnabled.value) {
+            return
+        }
+
+        showMenu(
+            x = x,
+            y = y,
+        )
+    }
+
+    private fun isFeatureEnabled(): Boolean {
+        return featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED) &&
+            featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI)
+    }
+
+    /** Updates application state to ask to show the menu at the given coordinates. */
+    private fun showMenu(
+        x: Int,
+        y: Int,
+    ) {
+        _menu.value =
+            KeyguardSettingsPopupMenuModel(
+                position =
+                    Position(
+                        x = x,
+                        y = y,
+                    ),
+                onClicked = {
+                    hideMenu()
+                    navigateToLockScreenSettings()
+                },
+                onDismissed = { hideMenu() },
+            )
+        logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
+    }
+
+    /** Updates application state to ask to hide the menu. */
+    private fun hideMenu() {
+        _menu.value = null
+    }
+
+    /** Opens the wallpaper picker screen after the device is unlocked by the user. */
+    private fun navigateToLockScreenSettings() {
+        logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
+        activityStarter.dismissKeyguardThenExecute(
+            /* action= */ {
+                appContext.startActivity(
+                    Intent(Intent.ACTION_SET_WALLPAPER).apply {
+                        flags = Intent.FLAG_ACTIVITY_NEW_TASK
+                        appContext
+                            .getString(R.string.config_wallpaperPickerPackage)
+                            .takeIf { it.isNotEmpty() }
+                            ?.let { packageName -> setPackage(packageName) }
+                    }
+                )
+                true
+            },
+            /* cancel= */ {},
+            /* afterKeyguardGone= */ true,
+        )
+    }
+
+    enum class LogEvents(
+        private val _id: Int,
+    ) : UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "The lock screen was long-pressed and we showed the settings popup menu.")
+        LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN(1292),
+        @UiEvent(doc = "The lock screen long-press popup menu was clicked.")
+        LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED(1293),
+        ;
+
+        override fun getId() = _id
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index c219380..dfbe1c2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -18,12 +18,14 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.app.AlertDialog
+import android.app.admin.DevicePolicyManager
 import android.content.Intent
 import android.util.Log
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
@@ -41,13 +43,17 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class KeyguardQuickAffordanceInteractor
 @Inject
@@ -61,6 +67,8 @@
     private val featureFlags: FeatureFlags,
     private val repository: Lazy<KeyguardQuickAffordanceRepository>,
     private val launchAnimator: DialogLaunchAnimator,
+    private val devicePolicyManager: DevicePolicyManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
     private val isUsingRepository: Boolean
         get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
@@ -74,9 +82,13 @@
         get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
 
     /** Returns an observable for the quick affordance at the given position. */
-    fun quickAffordance(
+    suspend fun quickAffordance(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceModel> {
+        if (isFeatureDisabledByDevicePolicy()) {
+            return flowOf(KeyguardQuickAffordanceModel.Hidden)
+        }
+
         return combine(
             quickAffordanceAlwaysVisible(position),
             keyguardInteractor.isDozing,
@@ -148,13 +160,20 @@
      *
      * @return `true` if the affordance was selected successfully; `false` otherwise.
      */
-    fun select(slotId: String, affordanceId: String): Boolean {
+    suspend fun select(slotId: String, affordanceId: String): Boolean {
         check(isUsingRepository)
+        if (isFeatureDisabledByDevicePolicy()) {
+            return false
+        }
 
         val slots = repository.get().getSlotPickerRepresentations()
         val slot = slots.find { it.id == slotId } ?: return false
         val selections =
-            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+            repository
+                .get()
+                .getCurrentSelections()
+                .getOrDefault(slotId, emptyList())
+                .toMutableList()
         val alreadySelected = selections.remove(affordanceId)
         if (!alreadySelected) {
             while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
@@ -183,8 +202,11 @@
      * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
      * the affordance was not on the slot to begin with).
      */
-    fun unselect(slotId: String, affordanceId: String?): Boolean {
+    suspend fun unselect(slotId: String, affordanceId: String?): Boolean {
         check(isUsingRepository)
+        if (isFeatureDisabledByDevicePolicy()) {
+            return false
+        }
 
         val slots = repository.get().getSlotPickerRepresentations()
         if (slots.find { it.id == slotId } == null) {
@@ -193,7 +215,7 @@
 
         if (affordanceId.isNullOrEmpty()) {
             return if (
-                repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty()
+                repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty()
             ) {
                 false
             } else {
@@ -203,7 +225,11 @@
         }
 
         val selections =
-            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+            repository
+                .get()
+                .getCurrentSelections()
+                .getOrDefault(slotId, emptyList())
+                .toMutableList()
         return if (selections.remove(affordanceId)) {
             repository
                 .get()
@@ -219,8 +245,12 @@
 
     /** Returns affordance IDs indexed by slot ID, for all known slots. */
     suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
+        if (isFeatureDisabledByDevicePolicy()) {
+            return emptyMap()
+        }
+
         val slots = repository.get().getSlotPickerRepresentations()
-        val selections = repository.get().getSelections()
+        val selections = repository.get().getCurrentSelections()
         val affordanceById =
             getAffordancePickerRepresentations().associateBy { affordance -> affordance.id }
         return slots.associate { slot ->
@@ -343,13 +373,17 @@
         return repository.get().getAffordancePickerRepresentations()
     }
 
-    fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
+    suspend fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
         check(isUsingRepository)
 
+        if (isFeatureDisabledByDevicePolicy()) {
+            return emptyList()
+        }
+
         return repository.get().getSlotPickerRepresentations()
     }
 
-    fun getPickerFlags(): List<KeyguardPickerFlag> {
+    suspend fun getPickerFlags(): List<KeyguardPickerFlag> {
         return listOf(
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_REVAMPED_WALLPAPER_UI,
@@ -357,15 +391,36 @@
             ),
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED,
-                value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
+                value =
+                    !isFeatureDisabledByDevicePolicy() &&
+                        featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
             ),
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED,
                 value = featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
             ),
+            KeyguardPickerFlag(
+                name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW,
+                value = featureFlags.isEnabled(Flags.WALLPAPER_FULLSCREEN_PREVIEW),
+            ),
+            KeyguardPickerFlag(
+                name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME,
+                value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME)
+            )
         )
     }
 
+    private suspend fun isFeatureDisabledByDevicePolicy(): Boolean {
+        val flags =
+            withContext(backgroundDispatcher) {
+                devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)
+            }
+        val flagsToCheck =
+            DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL or
+                DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL
+        return flagsToCheck and flags != 0
+    }
+
     companion object {
         private const val TAG = "KeyguardQuickAffordanceInteractor"
         private const val DELIMITER = "::"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index a2661d7..e650b9f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -19,11 +19,13 @@
 import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 
+private val TAG = KeyguardTransitionAuditLogger::class.simpleName!!
+
 /** Collect flows of interest for auditing keyguard transitions. */
 @SysUISingleton
 class KeyguardTransitionAuditLogger
@@ -37,35 +39,61 @@
 
     fun start() {
         scope.launch {
-            keyguardInteractor.wakefulnessModel.collect { logger.v("WakefulnessModel", it) }
+            keyguardInteractor.wakefulnessModel.collect {
+                logger.log(TAG, VERBOSE, "WakefulnessModel", it)
+            }
         }
 
         scope.launch {
-            keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) }
+            keyguardInteractor.primaryBouncerShowing.collect {
+                logger.log(TAG, VERBOSE, "Primary bouncer showing", it)
+            }
         }
 
-        scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } }
+        scope.launch {
+            keyguardInteractor.alternateBouncerShowing.collect {
+                logger.log(TAG, VERBOSE, "Alternate bouncer showing", it)
+            }
+        }
 
-        scope.launch { keyguardInteractor.isDreaming.collect { logger.v("isDreaming", it) } }
+        scope.launch {
+            keyguardInteractor.isDozing.collect { logger.log(TAG, VERBOSE, "isDozing", it) }
+        }
+
+        scope.launch {
+            keyguardInteractor.isAbleToDream.collect {
+                logger.log(TAG, VERBOSE, "isAbleToDream", it)
+            }
+        }
+
+        scope.launch {
+            keyguardInteractor.isKeyguardOccluded.collect {
+                logger.log(TAG, VERBOSE, "isOccluded", it)
+            }
+        }
 
         scope.launch {
             interactor.finishedKeyguardTransitionStep.collect {
-                logger.i("Finished transition", it)
+                logger.log(TAG, VERBOSE, "Finished transition", it)
             }
         }
 
         scope.launch {
             interactor.canceledKeyguardTransitionStep.collect {
-                logger.i("Canceled transition", it)
+                logger.log(TAG, VERBOSE, "Canceled transition", it)
             }
         }
 
         scope.launch {
-            interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) }
+            interactor.startedKeyguardTransitionStep.collect {
+                logger.log(TAG, VERBOSE, "Started transition", it)
+            }
         }
 
         scope.launch {
-            keyguardInteractor.dozeTransitionModel.collect { logger.i("Doze transition", it) }
+            keyguardInteractor.dozeTransitionModel.collect {
+                logger.log(TAG, VERBOSE, "Doze transition", it)
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index fbed446..efc1bd0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -37,13 +37,14 @@
             // exhaustive
             val ret =
                 when (it) {
-                    is FromBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is FromPrimaryBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
                     is FromAodTransitionInteractor -> Log.d(TAG, "Started $it")
                     is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it")
                     is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
                     is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it")
                     is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it")
                     is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is FromAlternateBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
                 }
             it.start()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 04024be..3c0ec35 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
@@ -19,17 +19,16 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.AnimationParams
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import javax.inject.Inject
-import kotlin.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
@@ -42,23 +41,46 @@
 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 }
+
     /** AOD->LOCKSCREEN transition information. */
     val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
 
-    /** LOCKSCREEN->AOD transition information. */
-    val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
-
     /** DREAMING->LOCKSCREEN transition information. */
     val dreamingToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(DREAMING, LOCKSCREEN)
 
+    /** GONE->DREAMING transition information. */
+    val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING)
+
+    /** LOCKSCREEN->AOD transition information. */
+    val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+    /** LOCKSCREEN->PRIMARY_BOUNCER transition information. */
+    val mLockscreenToPrimaryBouncerTransition: Flow<TransitionStep> =
+        repository.transition(LOCKSCREEN, PRIMARY_BOUNCER)
+
+    /** LOCKSCREEN->DREAMING transition information. */
+    val lockscreenToDreamingTransition: Flow<TransitionStep> =
+        repository.transition(LOCKSCREEN, DREAMING)
+
+    /** LOCKSCREEN->OCCLUDED transition information. */
+    val lockscreenToOccludedTransition: Flow<TransitionStep> =
+        repository.transition(LOCKSCREEN, OCCLUDED)
+
     /** OCCLUDED->LOCKSCREEN transition information. */
     val occludedToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(OCCLUDED, LOCKSCREEN)
 
-    /** (any)->AOD transition information */
-    val anyStateToAodTransition: Flow<TransitionStep> =
-        repository.transitions.filter { step -> step.to == KeyguardState.AOD }
+    /** PRIMARY_BOUNCER->GONE transition information. */
+    val primaryBouncerToGoneTransition: Flow<TransitionStep> =
+        repository.transition(PRIMARY_BOUNCER, GONE)
 
     /**
      * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
@@ -85,28 +107,4 @@
     /* The last completed [KeyguardState] transition */
     val finishedKeyguardState: Flow<KeyguardState> =
         finishedKeyguardTransitionStep.map { step -> step.to }
-
-    /**
-     * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
-     * range of [0, 1]. View animations should begin and end within a subset of this range. This
-     * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
-     */
-    fun transitionStepAnimation(
-        flow: Flow<TransitionStep>,
-        params: AnimationParams,
-        totalDuration: Duration,
-    ): Flow<Float> {
-        val start = (params.startTime / totalDuration).toFloat()
-        val chunks = (totalDuration / params.duration).toFloat()
-        return flow
-            // When starting, emit a value of 0f to give animations a chance to set initial state
-            .map { step ->
-                if (step.transitionState == STARTED) {
-                    0f
-                } else {
-                    (step.value - start) * chunks
-                }
-            }
-            .filter { value -> value >= 0f && value <= 1f }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index a59c407..833eda7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -86,7 +86,8 @@
                 KeyguardState.DOZING -> false
                 KeyguardState.AOD -> false
                 KeyguardState.DREAMING -> true
-                KeyguardState.BOUNCER -> true
+                KeyguardState.ALTERNATE_BOUNCER -> true
+                KeyguardState.PRIMARY_BOUNCER -> true
                 KeyguardState.LOCKSCREEN -> true
                 KeyguardState.GONE -> true
                 KeyguardState.OCCLUDED -> true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
index c5e49c6..3099a49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
@@ -18,27 +18,29 @@
 
 import android.view.View
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
 import com.android.systemui.util.ListenerSet
 import javax.inject.Inject
 
 /** Interactor to add and remove callbacks for the bouncer. */
 @SysUISingleton
 class PrimaryBouncerCallbackInteractor @Inject constructor() {
-    private var resetCallbacks = ListenerSet<KeyguardBouncer.KeyguardResetCallback>()
-    private var expansionCallbacks = ArrayList<KeyguardBouncer.PrimaryBouncerExpansionCallback>()
+    private var resetCallbacks = ListenerSet<KeyguardResetCallback>()
+    private var expansionCallbacks = ArrayList<PrimaryBouncerExpansionCallback>()
+
     /** Add a KeyguardResetCallback. */
-    fun addKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+    fun addKeyguardResetCallback(callback: KeyguardResetCallback) {
         resetCallbacks.addIfAbsent(callback)
     }
 
     /** Remove a KeyguardResetCallback. */
-    fun removeKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+    fun removeKeyguardResetCallback(callback: KeyguardResetCallback) {
         resetCallbacks.remove(callback)
     }
 
     /** Adds a callback to listen to bouncer expansion updates. */
-    fun addBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) {
+    fun addBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
         if (!expansionCallbacks.contains(callback)) {
             expansionCallbacks.add(callback)
         }
@@ -48,7 +50,7 @@
      * Removes a previously added callback. If the callback was never added, this method does
      * nothing.
      */
-    fun removeBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) {
+    fun removeBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
         expansionCallbacks.remove(callback)
     }
 
@@ -99,4 +101,40 @@
             callback.onKeyguardReset()
         }
     }
+
+    /** Callback updated when the primary bouncer's show and hide states change. */
+    interface PrimaryBouncerExpansionCallback {
+        /**
+         * Invoked when the bouncer expansion reaches [EXPANSION_VISIBLE]. This is NOT called each
+         * time the bouncer is shown, but rather only when the fully shown amount has changed based
+         * on the panel expansion. The bouncer's visibility can still change when the expansion
+         * amount hasn't changed. See [PrimaryBouncerInteractor.isFullyShowing] for the checks for
+         * the bouncer showing state.
+         */
+        fun onFullyShown() {}
+
+        /** Invoked when the bouncer is starting to transition to a hidden state. */
+        fun onStartingToHide() {}
+
+        /** Invoked when the bouncer is starting to transition to a visible state. */
+        fun onStartingToShow() {}
+
+        /** Invoked when the bouncer expansion reaches [EXPANSION_HIDDEN]. */
+        fun onFullyHidden() {}
+
+        /**
+         * From 0f [EXPANSION_VISIBLE] when fully visible to 1f [EXPANSION_HIDDEN] when fully hidden
+         */
+        fun onExpansionChanged(bouncerHideAmount: Float) {}
+
+        /**
+         * Invoked when visibility of KeyguardBouncer has changed. Note the bouncer expansion can be
+         * [EXPANSION_VISIBLE], but the view's visibility can be [View.INVISIBLE].
+         */
+        fun onVisibilityChanged(isVisible: Boolean) {}
+    }
+
+    interface KeyguardResetCallback {
+        fun onKeyguardReset()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index 2cf5fb9..c709fd1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.content.Context
 import android.content.res.ColorStateList
 import android.hardware.biometrics.BiometricSourceType
 import android.os.Handler
@@ -23,20 +24,24 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.view.View
+import android.util.Log
+import com.android.keyguard.KeyguardConstants
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.DejankUtils
+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.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.BouncerView
 import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.statusbar.phone.KeyguardBouncer
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
@@ -62,8 +67,9 @@
     private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
     private val falsingCollector: FalsingCollector,
     private val dismissCallbackRegistry: DismissCallbackRegistry,
+    private val context: Context,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     keyguardBypassController: KeyguardBypassController,
-    keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) {
     /** Whether we want to wait for face auth. */
     private val primaryBouncerFaceDelay =
@@ -90,7 +96,6 @@
     }
 
     val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
-    val screenTurnedOff: Flow<Unit> = repository.onScreenTurnedOff.filter { it }.map {}
     val show: Flow<KeyguardBouncerModel> = repository.primaryBouncerShow.filterNotNull()
     val hide: Flow<Unit> = repository.primaryBouncerHide.filter { it }.map {}
     val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
@@ -113,6 +118,26 @@
                 0f
             }
         }
+    /** Allow for interaction when just about fully visible */
+    val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
+    val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing
+
+    init {
+        keyguardUpdateMonitor.registerCallback(
+            object : KeyguardUpdateMonitorCallback() {
+                override fun onBiometricRunningStateChanged(
+                    running: Boolean,
+                    biometricSourceType: BiometricSourceType?
+                ) {
+                    updateSideFpsVisibility()
+                }
+
+                override fun onStrongAuthStateChanged(userId: Int) {
+                    updateSideFpsVisibility()
+                }
+            }
+        )
+    }
 
     // TODO(b/243685699): Move isScrimmed logic to data layer.
     // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
@@ -120,7 +145,6 @@
     @JvmOverloads
     fun show(isScrimmed: Boolean) {
         // Reset some states as we show the bouncer.
-        repository.setOnScreenTurnedOff(false)
         repository.setKeyguardAuthenticated(null)
         repository.setPrimaryHide(false)
         repository.setPrimaryStartingToHide(false)
@@ -143,7 +167,7 @@
         Trace.beginSection("KeyguardBouncer#show")
         repository.setPrimaryScrimmed(isScrimmed)
         if (isScrimmed) {
-            setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+            setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
         }
 
         if (resumeBouncer) {
@@ -162,7 +186,7 @@
         } else {
             DejankUtils.postAfterTraversal(showRunnable)
         }
-        keyguardStateController.notifyBouncerShowing(true)
+        keyguardStateController.notifyPrimaryBouncerShowing(true)
         primaryBouncerCallbackInteractor.dispatchStartingToShow()
         Trace.endSection()
     }
@@ -179,7 +203,7 @@
         }
 
         falsingCollector.onBouncerHidden()
-        keyguardStateController.notifyBouncerShowing(false /* showing */)
+        keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */)
         cancelShowRunnable()
         repository.setPrimaryShowingSoon(false)
         repository.setPrimaryVisible(false)
@@ -204,14 +228,14 @@
         }
 
         if (
-            expansion == KeyguardBouncer.EXPANSION_VISIBLE &&
-                oldExpansion != KeyguardBouncer.EXPANSION_VISIBLE
+            expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+                oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE
         ) {
             falsingCollector.onBouncerShown()
             primaryBouncerCallbackInteractor.dispatchFullyShown()
         } else if (
-            expansion == KeyguardBouncer.EXPANSION_HIDDEN &&
-                oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN
+            expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN &&
+                oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN
         ) {
             /*
              * There are cases where #hide() was not invoked, such as when
@@ -222,8 +246,8 @@
             DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
             primaryBouncerCallbackInteractor.dispatchFullyHidden()
         } else if (
-            expansion != KeyguardBouncer.EXPANSION_VISIBLE &&
-                oldExpansion == KeyguardBouncer.EXPANSION_VISIBLE
+            expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+                oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE
         ) {
             primaryBouncerCallbackInteractor.dispatchStartingToHide()
             repository.setPrimaryStartingToHide(true)
@@ -260,11 +284,6 @@
         repository.setKeyguardAuthenticated(strongAuth)
     }
 
-    /** Tell the bouncer the screen has turned off. */
-    fun onScreenTurnedOff() {
-        repository.setOnScreenTurnedOff(true)
-    }
-
     /** Update the position of the bouncer when showing. */
     fun setKeyguardPosition(position: Float) {
         repository.setKeyguardPosition(position)
@@ -299,11 +318,40 @@
         repository.setPrimaryStartDisappearAnimation(finishRunnable)
     }
 
+    /** Determine whether to show the side fps animation. */
+    fun updateSideFpsVisibility() {
+        val sfpsEnabled: Boolean =
+            context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
+        val fpsDetectionRunning: Boolean = keyguardUpdateMonitor.isFingerprintDetectionRunning
+        val isUnlockingWithFpAllowed: Boolean =
+            keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed
+        val bouncerVisible = repository.primaryBouncerVisible.value
+        val toShow =
+            (repository.primaryBouncerVisible.value &&
+                sfpsEnabled &&
+                fpsDetectionRunning &&
+                isUnlockingWithFpAllowed &&
+                !isAnimatingAway())
+
+        if (KeyguardConstants.DEBUG) {
+            Log.d(
+                TAG,
+                ("sideFpsToShow=$toShow\n" +
+                    "bouncerVisible=$bouncerVisible\n" +
+                    "configEnabled=$sfpsEnabled\n" +
+                    "fpsDetectionRunning=$fpsDetectionRunning\n" +
+                    "isUnlockingWithFpAllowed=$isUnlockingWithFpAllowed\n" +
+                    "isAnimatingAway=${isAnimatingAway()}")
+            )
+        }
+        repository.setSideFpsShowing(toShow)
+    }
+
     /** Returns whether bouncer is fully showing. */
     fun isFullyShowing(): Boolean {
         return (repository.primaryBouncerShowingSoon.value ||
             repository.primaryBouncerVisible.value) &&
-            repository.panelExpansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
+            repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
             repository.primaryBouncerStartingDisappearAnimation.value == null
     }
 
@@ -315,8 +363,8 @@
     /** If bouncer expansion is between 0f and 1f non-inclusive. */
     fun isInTransit(): Boolean {
         return repository.primaryBouncerShowingSoon.value ||
-            repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN &&
-                repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE
+            repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN &&
+                repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE
     }
 
     /** Return whether bouncer is animating away. */
@@ -342,4 +390,8 @@
         DejankUtils.removeCallbacks(showRunnable)
         mainHandler.removeCallbacks(showRunnable)
     }
+
+    companion object {
+        private const val TAG = "PrimaryBouncerInteractor"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 81fa233..d9690b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -32,7 +32,9 @@
 
     @Binds
     @IntoSet
-    abstract fun fromBouncer(impl: FromBouncerTransitionInteractor): TransitionInteractor
+    abstract fun fromPrimaryBouncer(
+        impl: FromPrimaryBouncerTransitionInteractor
+    ): TransitionInteractor
 
     @Binds
     @IntoSet
@@ -53,4 +55,10 @@
     @Binds
     @IntoSet
     abstract fun fromDozing(impl: FromDozingTransitionInteractor): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun fromAlternateBouncer(
+        impl: FromAlternateBouncerTransitionInteractor
+    ): TransitionInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 4d24c14..e3e3527 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -30,7 +30,26 @@
 
     abstract fun start()
 
+    fun <A, B, C> toTriple(a: A, b: B, c: C) = Triple(a, b, c)
+
     fun <A, B, C> toTriple(a: A, bc: Pair<B, C>) = Triple(a, bc.first, bc.second)
 
     fun <A, B, C> toTriple(ab: Pair<A, B>, c: C) = Triple(ab.first, ab.second, c)
+
+    fun <A, B, C, D> toQuad(a: A, b: B, c: C, d: D) = Quad(a, b, c, d)
+
+    fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) = Quad(a, bcd.first, bcd.second, bcd.third)
+
+    fun <A, B, C, D, E> toQuint(a: A, bcde: Quad<B, C, D, E>) =
+        Quint(a, bcde.first, bcde.second, bcde.third, bcde.fourth)
 }
+
+data class Quad<A, B, C, D>(val first: A, val second: B, val third: C, val fourth: D)
+
+data class Quint<A, B, C, D, E>(
+    val first: A,
+    val second: B,
+    val third: C,
+    val fourth: D,
+    val fifth: E
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt
new file mode 100644
index 0000000..7c61e71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.keyguard.domain.model
+
+import com.android.systemui.common.shared.model.Position
+
+/** Models a settings popup menu for the lock screen. */
+data class KeyguardSettingsPopupMenuModel(
+    /** Where the menu should be anchored, roughly in screen space. */
+    val position: Position,
+    /** Callback to invoke when the menu gets clicked by the user. */
+    val onClicked: () -> Unit,
+    /** Callback to invoke when the menu gets dismissed by the user. */
+    val onDismissed: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt
new file mode 100644
index 0000000..8222dd5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.keyguard.shared.constants
+
+object KeyguardBouncerConstants {
+    /**
+     * Values for the bouncer expansion represented as the panel expansion. Panel expansion 1f =
+     * panel fully showing = bouncer fully hidden Panel expansion 0f = panel fully hiding = bouncer
+     * fully showing
+     */
+    const val EXPANSION_HIDDEN = 1f
+    const val EXPANSION_VISIBLE = 0f
+    const val ALPHA_EXPANSION_THRESHOLD = 0.95f
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt
similarity index 63%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt
index 67733e9..19baf77 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -15,11 +15,14 @@
  */
 package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+/** Camera launch sources */
+enum class CameraLaunchSourceModel {
+    /** Device is wiggled */
+    WIGGLE,
+    /** Power button has been double tapped */
+    POWER_DOUBLE_TAP,
+    /** Device has been lifted */
+    LIFT_TRIGGER,
+    /** Quick affordance button has been pressed */
+    QUICK_AFFORDANCE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/DevicePosture.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/DevicePosture.kt
new file mode 100644
index 0000000..fff7cfe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/DevicePosture.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.keyguard.shared.model
+
+import com.android.systemui.statusbar.policy.DevicePostureController
+
+/** Represents the possible posture states of the device. */
+enum class DevicePosture {
+    UNKNOWN,
+    CLOSED,
+    HALF_OPENED,
+    OPENED,
+    FLIPPED;
+
+    companion object {
+        fun toPosture(@DevicePostureController.DevicePostureInt posture: Int): DevicePosture {
+            return when (posture) {
+                DevicePostureController.DEVICE_POSTURE_CLOSED -> CLOSED
+                DevicePostureController.DEVICE_POSTURE_HALF_OPENED -> HALF_OPENED
+                DevicePostureController.DEVICE_POSTURE_OPENED -> OPENED
+                DevicePostureController.DEVICE_POSTURE_FLIPPED -> FLIPPED
+                DevicePostureController.DEVICE_POSTURE_UNKNOWN -> UNKNOWN
+                else -> UNKNOWN
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
new file mode 100644
index 0000000..b1c5f8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.keyguard.shared.model
+
+import android.hardware.face.FaceManager
+
+/** Authentication status provided by [com.android.keyguard.faceauth.KeyguardFaceAuthManager] */
+sealed class AuthenticationStatus
+
+/** Success authentication status. */
+data class SuccessAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
+    AuthenticationStatus()
+
+/** Face authentication help message. */
+data class HelpAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus()
+
+/** Face acquired message. */
+data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationStatus()
+
+/** Face authentication failed message. */
+object FailedAuthenticationStatus : AuthenticationStatus()
+
+/** Face authentication error message */
+data class ErrorAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus() {
+    /**
+     * Method that checks if [msgId] is a lockout error. A lockout error means that face
+     * authentication is locked out.
+     */
+    fun isLockoutError() = msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT
+
+    /**
+     * Method that checks if [msgId] is a cancellation error. This means that face authentication
+     * was cancelled before it completed.
+     */
+    fun isCancellationError() = msgId == FaceManager.FACE_ERROR_CANCELED
+}
+
+/** Face detection success message. */
+data class DetectionStatus(val sensorId: Int, val userId: Int, val isStrongBiometric: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
index 7d13359..e7e9159 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.shared.model
 
+import android.content.Intent
 import androidx.annotation.DrawableRes
 
 /**
@@ -45,4 +46,7 @@
      * user to a destination where they can re-enable it.
      */
     val actionComponentName: String? = null,
+
+    /** Optional [Intent] to use to start an activity to configure this affordance. */
+    val configureIntent: Intent? = null,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index c757986..87b4321 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -42,10 +42,15 @@
      */
     AOD,
     /*
-     * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS
-     * (Fingerprint Sensor) variations, for the user to verify their credentials
+     * The security screen prompt containing UI to prompt the user to use a biometric credential
+     * (ie: fingerprint). When supported, this may show before showing the primary bouncer.
      */
-    BOUNCER,
+    ALTERNATE_BOUNCER,
+    /*
+     * The security screen prompt UI, containing PIN, Password, Pattern for the user to verify their
+     * credentials.
+     */
+    PRIMARY_BOUNCER,
     /*
      * Device is actively displaying keyguard UI and is not in low-power mode. Device may be
      * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt
index 67733e9..4fd14b1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
+
 package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
+/** Represents the trust state */
+data class TrustModel(
+    /** If true, the system believes the environment to be trusted. */
+    val isTrusted: Boolean,
+    /** The user, for which the trust changed. */
+    val userId: Int,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
new file mode 100644
index 0000000..ca1e27c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.keyguard.ui
+
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/**
+ * For the given transition params, construct a flow using [createFlow] for the specified portion of
+ * the overall transition.
+ */
+class KeyguardTransitionAnimationFlow(
+    private val transitionDuration: Duration,
+    private val transitionFlow: Flow<TransitionStep>,
+) {
+    /**
+     * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in
+     * the range of [0, 1]. View animations should begin and end within a subset of this range. This
+     * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+     */
+    fun createFlow(
+        duration: Duration,
+        onStep: (Float) -> Float,
+        startTime: Duration = 0.milliseconds,
+        onCancel: (() -> Float)? = null,
+        onFinish: (() -> Float)? = null,
+        interpolator: Interpolator = LINEAR,
+    ): Flow<Float> {
+        if (!duration.isPositive()) {
+            throw IllegalArgumentException("duration must be a positive number: $duration")
+        }
+        if ((startTime + duration).compareTo(transitionDuration) > 0) {
+            throw IllegalArgumentException(
+                "startTime($startTime) + duration($duration) must be" +
+                    " <= transitionDuration($transitionDuration)"
+            )
+        }
+
+        val start = (startTime / transitionDuration).toFloat()
+        val chunks = (transitionDuration / duration).toFloat()
+        var isComplete = true
+
+        fun stepToValue(step: TransitionStep): Float? {
+            val value = (step.value - start) * chunks
+            return when (step.transitionState) {
+                // When starting, make sure to always emit. If a transition is started from the
+                // middle, it is possible this animation is being skipped but we need to inform
+                // the ViewModels of the last update
+                STARTED -> {
+                    isComplete = false
+                    max(0f, min(1f, value))
+                }
+                // Always send a final value of 1. Because of rounding, [value] may never be
+                // exactly 1.
+                RUNNING ->
+                    if (isComplete) {
+                        null
+                    } else if (value >= 1f) {
+                        isComplete = true
+                        1f
+                    } else if (value >= 0f) {
+                        value
+                    } else {
+                        null
+                    }
+                else -> null
+            }?.let { onStep(interpolator.getInterpolation(it)) }
+        }
+
+        return transitionFlow
+            .map { step ->
+                when (step.transitionState) {
+                    STARTED -> stepToValue(step)
+                    RUNNING -> stepToValue(step)
+                    CANCELED -> onCancel?.invoke()
+                    FINISHED -> onFinish?.invoke()
+                }
+            }
+            .filterNotNull()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 0e4058b..ab009f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -45,11 +45,11 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.kotlin.pairwise
 import kotlin.math.pow
 import kotlin.math.sqrt
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
@@ -67,6 +67,8 @@
 object KeyguardBottomAreaViewBinder {
 
     private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
+    private const val SCALE_SELECTED_BUTTON = 1.23f
+    private const val DIM_ALPHA = 0.3f
 
     /**
      * Defines interface for an object that acts as the binding between the view and its view-model.
@@ -129,18 +131,6 @@
                 }
 
                 launch {
-                    viewModel.startButton
-                        .map { it.isActivated }
-                        .pairwise()
-                        .collect { (prev, next) ->
-                            when {
-                                !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
-                                prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
-                            }
-                        }
-                }
-
-                launch {
                     viewModel.endButton.collect { buttonModel ->
                         updateButton(
                             view = endButton,
@@ -153,18 +143,6 @@
                 }
 
                 launch {
-                    viewModel.endButton
-                        .map { it.isActivated }
-                        .pairwise()
-                        .collect { (prev, next) ->
-                            when {
-                                !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
-                                prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
-                            }
-                        }
-                }
-
-                launch {
                     viewModel.isOverlayContainerVisible.collect { isVisible ->
                         overlayContainer.visibility =
                             if (isVisible) {
@@ -186,12 +164,26 @@
 
                         ambientIndicationArea?.alpha = alpha
                         indicationArea.alpha = alpha
-                        startButton.alpha = alpha
-                        endButton.alpha = alpha
                     }
                 }
 
                 launch {
+                    updateButtonAlpha(
+                        view = startButton,
+                        viewModel = viewModel.startButton,
+                        alphaFlow = viewModel.alpha,
+                    )
+                }
+
+                launch {
+                    updateButtonAlpha(
+                        view = endButton,
+                        viewModel = viewModel.endButton,
+                        alphaFlow = viewModel.alpha,
+                    )
+                }
+
+                launch {
                     viewModel.indicationAreaTranslationX.collect { translationX ->
                         indicationArea.translationX = translationX
                         ambientIndicationArea?.translationX = translationX
@@ -340,6 +332,11 @@
             } else {
                 null
             }
+        view
+            .animate()
+            .scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
+            .scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
+            .start()
 
         view.isClickable = viewModel.isClickable
         if (viewModel.isClickable) {
@@ -358,6 +355,17 @@
         view.isSelected = viewModel.isSelected
     }
 
+    private suspend fun updateButtonAlpha(
+        view: View,
+        viewModel: Flow<KeyguardQuickAffordanceViewModel>,
+        alphaFlow: Flow<Float>,
+    ) {
+        combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha ->
+                if (isDimmed) DIM_ALPHA else alpha
+            }
+            .collect { view.alpha = it }
+    }
+
     private class OnTouchListener(
         private val view: View,
         private val viewModel: KeyguardQuickAffordanceViewModel,
@@ -367,83 +375,93 @@
 
         private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
         private var longPressAnimator: ViewPropertyAnimator? = null
-        private var downTimestamp = 0L
 
         @SuppressLint("ClickableViewAccessibility")
         override fun onTouch(v: View?, event: MotionEvent?): Boolean {
             return when (event?.actionMasked) {
                 MotionEvent.ACTION_DOWN ->
                     if (viewModel.configKey != null) {
-                        downTimestamp = System.currentTimeMillis()
-                        longPressAnimator =
-                            view
-                                .animate()
-                                .scaleX(PRESSED_SCALE)
-                                .scaleY(PRESSED_SCALE)
-                                .setDuration(longPressDurationMs)
-                                .withEndAction {
-                                    view.setOnClickListener {
-                                        viewModel.onClicked(
-                                            KeyguardQuickAffordanceViewModel.OnClickedParameters(
-                                                configKey = viewModel.configKey,
-                                                expandable = Expandable.fromView(view),
-                                            )
-                                        )
+                        if (isUsingAccurateTool(event)) {
+                            // For accurate tool types (stylus, mouse, etc.), we don't require a
+                            // long-press.
+                        } else {
+                            // When not using a stylus, we require a long-press to activate the
+                            // quick affordance, mostly to do "falsing" (e.g. protect from false
+                            // clicks in the pocket/bag).
+                            longPressAnimator =
+                                view
+                                    .animate()
+                                    .scaleX(PRESSED_SCALE)
+                                    .scaleY(PRESSED_SCALE)
+                                    .setDuration(longPressDurationMs)
+                                    .withEndAction {
+                                        dispatchClick(viewModel.configKey)
+                                        cancel()
                                     }
-                                    view.performClick()
-                                    view.setOnClickListener(null)
-                                    cancel()
-                                }
+                        }
                         true
                     } else {
                         false
                     }
                 MotionEvent.ACTION_MOVE -> {
-                    if (event.historySize > 0) {
-                        val distance =
-                            sqrt(
-                                (event.y - event.getHistoricalY(0)).pow(2) +
-                                    (event.x - event.getHistoricalX(0)).pow(2)
-                            )
-                        if (distance > ViewConfiguration.getTouchSlop()) {
+                    if (!isUsingAccurateTool(event)) {
+                        // Moving too far while performing a long-press gesture cancels that
+                        // gesture.
+                        val distanceMoved = distanceMoved(event)
+                        if (distanceMoved > ViewConfiguration.getTouchSlop()) {
                             cancel()
                         }
                     }
                     true
                 }
                 MotionEvent.ACTION_UP -> {
-                    cancel(
-                        onAnimationEnd =
-                            if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
-                                Runnable {
-                                    messageDisplayer.invoke(
-                                        R.string.keyguard_affordance_press_too_short
-                                    )
-                                    val amplitude =
-                                        view.context.resources
-                                            .getDimensionPixelSize(
-                                                R.dimen.keyguard_affordance_shake_amplitude
-                                            )
-                                            .toFloat()
-                                    val shakeAnimator =
-                                        ObjectAnimator.ofFloat(
-                                            view,
-                                            "translationX",
-                                            -amplitude / 2,
-                                            amplitude / 2,
+                    if (isUsingAccurateTool(event)) {
+                        // When using an accurate tool type (stylus, mouse, etc.), we don't require
+                        // a long-press gesture to activate the quick affordance. Therefore, lifting
+                        // the pointer performs a click.
+                        if (
+                            viewModel.configKey != null &&
+                                distanceMoved(event) <= ViewConfiguration.getTouchSlop()
+                        ) {
+                            dispatchClick(viewModel.configKey)
+                        }
+                    } else {
+                        // When not using a stylus, lifting the finger/pointer will actually cancel
+                        // the long-press gesture. Calling cancel after the quick affordance was
+                        // already long-press activated is a no-op, so it's safe to call from here.
+                        cancel(
+                            onAnimationEnd =
+                                if (event.eventTime - event.downTime < longPressDurationMs) {
+                                    Runnable {
+                                        messageDisplayer.invoke(
+                                            R.string.keyguard_affordance_press_too_short
                                         )
-                                    shakeAnimator.duration =
-                                        ShakeAnimationDuration.inWholeMilliseconds
-                                    shakeAnimator.interpolator =
-                                        CycleInterpolator(ShakeAnimationCycles)
-                                    shakeAnimator.start()
+                                        val amplitude =
+                                            view.context.resources
+                                                .getDimensionPixelSize(
+                                                    R.dimen.keyguard_affordance_shake_amplitude
+                                                )
+                                                .toFloat()
+                                        val shakeAnimator =
+                                            ObjectAnimator.ofFloat(
+                                                view,
+                                                "translationX",
+                                                -amplitude / 2,
+                                                amplitude / 2,
+                                            )
+                                        shakeAnimator.duration =
+                                            ShakeAnimationDuration.inWholeMilliseconds
+                                        shakeAnimator.interpolator =
+                                            CycleInterpolator(ShakeAnimationCycles)
+                                        shakeAnimator.start()
 
-                                    vibratorHelper?.vibrate(Vibrations.Shake)
+                                        vibratorHelper?.vibrate(Vibrations.Shake)
+                                    }
+                                } else {
+                                    null
                                 }
-                            } else {
-                                null
-                            }
-                    )
+                        )
+                    }
                     true
                 }
                 MotionEvent.ACTION_CANCEL -> {
@@ -454,8 +472,29 @@
             }
         }
 
+        private fun dispatchClick(
+            configKey: String,
+        ) {
+            view.setOnClickListener {
+                vibratorHelper?.vibrate(
+                    if (viewModel.isActivated) {
+                        Vibrations.Activated
+                    } else {
+                        Vibrations.Deactivated
+                    }
+                )
+                viewModel.onClicked(
+                    KeyguardQuickAffordanceViewModel.OnClickedParameters(
+                        configKey = configKey,
+                        expandable = Expandable.fromView(view),
+                    )
+                )
+            }
+            view.performClick()
+            view.setOnClickListener(null)
+        }
+
         private fun cancel(onAnimationEnd: Runnable? = null) {
-            downTimestamp = 0L
             longPressAnimator?.cancel()
             longPressAnimator = null
             view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
@@ -463,6 +502,40 @@
 
         companion object {
             private const val PRESSED_SCALE = 1.5f
+
+            /**
+             * Returns `true` if the tool type at the given pointer index is an accurate tool (like
+             * stylus or mouse), which means we can trust it to not be a false click; `false`
+             * otherwise.
+             */
+            private fun isUsingAccurateTool(
+                event: MotionEvent,
+                pointerIndex: Int = 0,
+            ): Boolean {
+                return when (event.getToolType(pointerIndex)) {
+                    MotionEvent.TOOL_TYPE_STYLUS -> true
+                    MotionEvent.TOOL_TYPE_MOUSE -> true
+                    else -> false
+                }
+            }
+
+            /**
+             * Returns the amount of distance the pointer moved since the historical record at the
+             * [since] index.
+             */
+            private fun distanceMoved(
+                event: MotionEvent,
+                since: Int = 0,
+            ): Float {
+                return if (event.historySize > 0) {
+                    sqrt(
+                        (event.y - event.getHistoricalY(since)).pow(2) +
+                            (event.x - event.getHistoricalX(since)).pow(2)
+                    )
+                } else {
+                    0f
+                }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index f772b17..2337ffc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -19,19 +19,23 @@
 import android.view.KeyEvent
 import android.view.View
 import android.view.ViewGroup
+import android.window.OnBackAnimationCallback
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.internal.policy.SystemBarUtils
-import com.android.keyguard.KeyguardHostViewController
+import com.android.keyguard.KeyguardSecurityContainerController
 import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityView
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.settingslib.Utils
 import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
@@ -41,50 +45,57 @@
     fun bind(
         view: ViewGroup,
         viewModel: KeyguardBouncerViewModel,
+        primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
         componentFactory: KeyguardBouncerComponent.Factory
     ) {
-        // Builds the KeyguardHostViewController from bouncer view group.
-        val hostViewController: KeyguardHostViewController =
-            componentFactory.create(view).keyguardHostViewController
-        hostViewController.init()
+        // Builds the KeyguardSecurityContainerController from bouncer view group.
+        val securityContainerController: KeyguardSecurityContainerController =
+            componentFactory.create(view).securityContainerController
+        securityContainerController.init()
         val delegate =
             object : BouncerViewDelegate {
                 override fun isFullScreenBouncer(): Boolean {
-                    val mode = hostViewController.currentSecurityMode
+                    val mode = securityContainerController.currentSecurityMode
                     return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
                         mode == KeyguardSecurityModel.SecurityMode.SimPuk
                 }
 
+                override fun getBackCallback(): OnBackAnimationCallback {
+                    return securityContainerController.backCallback
+                }
+
                 override fun shouldDismissOnMenuPressed(): Boolean {
-                    return hostViewController.shouldEnableMenuKey()
+                    return securityContainerController.shouldEnableMenuKey()
                 }
 
                 override fun interceptMediaKey(event: KeyEvent?): Boolean {
-                    return hostViewController.interceptMediaKey(event)
+                    return securityContainerController.interceptMediaKey(event)
                 }
 
                 override fun dispatchBackKeyEventPreIme(): Boolean {
-                    return hostViewController.dispatchBackKeyEventPreIme()
+                    return securityContainerController.dispatchBackKeyEventPreIme()
                 }
 
                 override fun showNextSecurityScreenOrFinish(): Boolean {
-                    return hostViewController.dismiss(KeyguardUpdateMonitor.getCurrentUser())
+                    return securityContainerController.dismiss(
+                        KeyguardUpdateMonitor.getCurrentUser()
+                    )
                 }
 
                 override fun resume() {
-                    hostViewController.showPrimarySecurityScreen()
-                    hostViewController.onResume()
+                    securityContainerController.showPrimarySecurityScreen(/* isTurningOff= */ false)
+                    securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
                 }
 
                 override fun setDismissAction(
                     onDismissAction: ActivityStarter.OnDismissAction?,
                     cancelAction: Runnable?
                 ) {
-                    hostViewController.setOnDismissAction(onDismissAction, cancelAction)
+                    securityContainerController.setOnDismissAction(onDismissAction, cancelAction)
                 }
 
                 override fun willDismissWithActions(): Boolean {
-                    return hostViewController.hasDismissActions()
+                    return securityContainerController.hasDismissActions()
                 }
             }
         view.repeatWhenAttached {
@@ -93,46 +104,51 @@
                     viewModel.setBouncerViewDelegate(delegate)
                     launch {
                         viewModel.show.collect {
-                            hostViewController.showPromptReason(it.promptReason)
+                            // Reset Security Container entirely.
+                            securityContainerController.reinflateViewFlipper()
+                            securityContainerController.showPromptReason(it.promptReason)
                             it.errorMessage?.let { errorMessage ->
-                                hostViewController.showErrorMessage(errorMessage)
+                                securityContainerController.showMessage(
+                                    errorMessage,
+                                    Utils.getColorError(view.context)
+                                )
                             }
-                            hostViewController.showPrimarySecurityScreen()
-                            hostViewController.appear(
-                                SystemBarUtils.getStatusBarHeight(view.context)
+                            securityContainerController.showPrimarySecurityScreen(
+                                /* turningOff= */ false
                             )
-                        }
-                    }
-
-                    launch {
-                        viewModel.showWithFullExpansion.collect { model ->
-                            hostViewController.resetSecurityContainer()
-                            hostViewController.showPromptReason(model.promptReason)
-                            hostViewController.onResume()
+                            securityContainerController.appear()
+                            securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
                         }
                     }
 
                     launch {
                         viewModel.hide.collect {
-                            hostViewController.cancelDismissAction()
-                            hostViewController.cleanUp()
-                            hostViewController.resetSecurityContainer()
+                            securityContainerController.cancelDismissAction()
+                            securityContainerController.reset()
                         }
                     }
 
                     launch {
-                        viewModel.startingToHide.collect { hostViewController.onStartingToHide() }
+                        viewModel.startingToHide.collect {
+                            securityContainerController.onStartingToHide()
+                        }
                     }
 
                     launch {
                         viewModel.startDisappearAnimation.collect {
-                            hostViewController.startDisappearAnimation(it)
+                            securityContainerController.startDisappearAnimation(it)
                         }
                     }
 
                     launch {
                         viewModel.bouncerExpansionAmount.collect { expansion ->
-                            hostViewController.setExpansion(expansion)
+                            securityContainerController.setExpansion(expansion)
+                        }
+                    }
+
+                    launch {
+                        primaryBouncerToGoneTransitionViewModel.bouncerAlpha.collect { alpha ->
+                            securityContainerController.setAlpha(alpha)
                         }
                     }
 
@@ -140,53 +156,56 @@
                         viewModel.bouncerExpansionAmount
                             .filter { it == EXPANSION_VISIBLE }
                             .collect {
-                                hostViewController.onResume()
-                                view.announceForAccessibility(
-                                    hostViewController.accessibilityTitleForCurrentMode
-                                )
+                                securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
+                                view.announceForAccessibility(securityContainerController.title)
                             }
                     }
 
                     launch {
                         viewModel.isBouncerVisible.collect { isVisible ->
-                            val visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
-                            view.visibility = visibility
-                            hostViewController.onBouncerVisibilityChanged(visibility)
+                            view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
+                            securityContainerController.onBouncerVisibilityChanged(isVisible)
                         }
                     }
 
                     launch {
                         viewModel.isBouncerVisible
                             .filter { !it }
-                            .collect {
-                                // Remove existing input for security reasons.
-                                hostViewController.resetSecurityContainer()
-                            }
+                            .collect { securityContainerController.onPause() }
+                    }
+
+                    launch {
+                        viewModel.isInteractable.collect { isInteractable ->
+                            securityContainerController.setInteractable(isInteractable)
+                        }
                     }
 
                     launch {
                         viewModel.keyguardPosition.collect { position ->
-                            hostViewController.updateKeyguardPosition(position)
+                            securityContainerController.updateKeyguardPosition(position)
                         }
                     }
 
                     launch {
                         viewModel.updateResources.collect {
-                            hostViewController.updateResources()
+                            securityContainerController.updateResources()
                             viewModel.notifyUpdateResources()
                         }
                     }
 
                     launch {
                         viewModel.bouncerShowMessage.collect {
-                            hostViewController.showMessage(it.message, it.colorStateList)
+                            securityContainerController.showMessage(it.message, it.colorStateList)
                             viewModel.onMessageShown()
                         }
                     }
 
                     launch {
                         viewModel.keyguardAuthenticated.collect {
-                            hostViewController.finish(it, KeyguardUpdateMonitor.getCurrentUser())
+                            securityContainerController.finish(
+                                it,
+                                KeyguardUpdateMonitor.getCurrentUser()
+                            )
                             viewModel.notifyKeyguardAuthenticated()
                         }
                     }
@@ -198,10 +217,14 @@
                     }
 
                     launch {
-                        viewModel.screenTurnedOff.collect {
-                            if (view.visibility == View.VISIBLE) {
-                                hostViewController.onPause()
-                            }
+                        viewModel.shouldUpdateSideFps.collect {
+                            viewModel.updateSideFpsVisibility()
+                        }
+                    }
+
+                    launch {
+                        viewModel.sideFpsShowing.collect {
+                            securityContainerController.updateSideFpsVisibility(it)
                         }
                     }
                     awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt
new file mode 100644
index 0000000..d85682b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.keyguard.ui.binder
+
+import android.annotation.SuppressLint
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import android.widget.PopupWindow
+import com.android.systemui.R
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.common.ui.binder.TextViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsPopupMenuViewModel
+
+object KeyguardLongPressPopupViewBinder {
+    @SuppressLint("InflateParams") // We don't care that the parent is null.
+    fun createAndShow(
+        container: View,
+        viewModel: KeyguardSettingsPopupMenuViewModel,
+        onDismissed: () -> Unit,
+    ): () -> Unit {
+        val contentView: View =
+            LayoutInflater.from(container.context)
+                .inflate(
+                    R.layout.keyguard_settings_popup_menu,
+                    null,
+                )
+
+        contentView.setOnClickListener { viewModel.onClicked() }
+        IconViewBinder.bind(
+            icon = viewModel.icon,
+            view = contentView.requireViewById(R.id.icon),
+        )
+        TextViewBinder.bind(
+            view = contentView.requireViewById(R.id.text),
+            viewModel = viewModel.text,
+        )
+
+        val popupWindow =
+            PopupWindow(container.context).apply {
+                windowLayoutType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
+                setBackgroundDrawable(null)
+                animationStyle = com.android.internal.R.style.Animation_Dialog
+                isOutsideTouchable = true
+                isFocusable = true
+                setContentView(contentView)
+                setOnDismissListener { onDismissed() }
+                contentView.measure(
+                    View.MeasureSpec.makeMeasureSpec(
+                        0,
+                        View.MeasureSpec.UNSPECIFIED,
+                    ),
+                    View.MeasureSpec.makeMeasureSpec(
+                        0,
+                        View.MeasureSpec.UNSPECIFIED,
+                    ),
+                )
+                showAtLocation(
+                    container,
+                    Gravity.NO_GRAVITY,
+                    viewModel.position.x - contentView.measuredWidth / 2,
+                    viewModel.position.y -
+                        contentView.measuredHeight -
+                        container.context.resources.getDimensionPixelSize(
+                            R.dimen.keyguard_long_press_settings_popup_vertical_offset
+                        ),
+                )
+            }
+
+        return { popupWindow.dismiss() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
new file mode 100644
index 0000000..ef3f242
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.keyguard.ui.binder
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.view.LongPressHandlingView
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.FalsingManager
+import kotlinx.coroutines.launch
+
+object KeyguardLongPressViewBinder {
+    /**
+     * Drives UI for the lock screen long-press feature.
+     *
+     * @param view The view that listens for long-presses.
+     * @param viewModel The view-model that models the UI state.
+     * @param onSingleTap A callback to invoke when the system decides that there was a single tap.
+     * @param falsingManager [FalsingManager] for making sure the long-press didn't just happen in
+     * the user's pocket.
+     */
+    @JvmStatic
+    fun bind(
+        view: LongPressHandlingView,
+        viewModel: KeyguardLongPressViewModel,
+        onSingleTap: () -> Unit,
+        falsingManager: FalsingManager,
+    ) {
+        view.listener =
+            object : LongPressHandlingView.Listener {
+                override fun onLongPressDetected(view: View, x: Int, y: Int) {
+                    if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                        return
+                    }
+
+                    viewModel.onLongPress(
+                        x = x,
+                        y = y,
+                    )
+                }
+
+                override fun onSingleTapDetected(view: View) {
+                    if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        return
+                    }
+
+                    onSingleTap()
+                }
+            }
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.isLongPressHandlingEnabled.collect { isEnabled ->
+                        view.setLongPressHandlingEnabled(isEnabled)
+                    }
+                }
+
+                launch {
+                    var dismissMenu: (() -> Unit)? = null
+
+                    viewModel.menu.collect { menuOrNull ->
+                        if (menuOrNull != null) {
+                            dismissMenu =
+                                KeyguardLongPressPopupViewBinder.createAndShow(
+                                    container = view,
+                                    viewModel = menuOrNull,
+                                    onDismissed = menuOrNull.onDismissed,
+                                )
+                        } else {
+                            dismissMenu?.invoke()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index a5ae8ba5..72b317c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -24,7 +24,6 @@
 import android.hardware.display.DisplayManager
 import android.os.Bundle
 import android.os.IBinder
-import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.SurfaceControlViewHost
 import android.view.View
@@ -39,6 +38,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
 import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
 import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import dagger.assisted.Assisted
@@ -65,6 +65,13 @@
     val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
     private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
     private val height: Int = bundle.getInt(KEY_VIEW_HEIGHT)
+    private val shouldHighlightSelectedAffordance: Boolean =
+        bundle.getBoolean(
+            KeyguardQuickAffordancePreviewConstants.KEY_HIGHLIGHT_QUICK_AFFORDANCES,
+            false,
+        )
+    private val shouldHideClock: Boolean =
+        bundle.getBoolean(ClockPreviewConstants.KEY_HIDE_CLOCK, false)
 
     private var host: SurfaceControlViewHost
 
@@ -82,6 +89,7 @@
                 bundle.getString(
                     KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
                 ),
+            shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
         )
         runBlocking(mainDispatcher) {
             host =
@@ -99,7 +107,9 @@
             val rootView = FrameLayout(context)
 
             setUpBottomArea(rootView)
-            setUpClock(rootView)
+            if (!shouldHideClock) {
+                setUpClock(rootView)
+            }
 
             rootView.measure(
                 View.MeasureSpec.makeMeasureSpec(
@@ -154,14 +164,18 @@
             bottomAreaView,
             FrameLayout.LayoutParams(
                 FrameLayout.LayoutParams.MATCH_PARENT,
-                FrameLayout.LayoutParams.WRAP_CONTENT,
-                Gravity.BOTTOM,
+                FrameLayout.LayoutParams.MATCH_PARENT,
             ),
         )
     }
 
     private fun setUpClock(parentView: ViewGroup) {
-        val clockChangeListener = ClockRegistry.ClockChangeListener { onClockChanged(parentView) }
+        val clockChangeListener =
+            object : ClockRegistry.ClockChangeListener {
+                override fun onCurrentClockChanged() {
+                    onClockChanged(parentView)
+                }
+            }
         clockRegistry.registerClockChangeListener(clockChangeListener)
         disposables.add(
             DisposableHandle { clockRegistry.unregisterClockChangeListener(clockChangeListener) }
@@ -173,7 +187,8 @@
         val receiver =
             object : BroadcastReceiver() {
                 override fun onReceive(context: Context?, intent: Intent?) {
-                    clockController.clock?.events?.onTimeTick()
+                    clockController.clock?.smallClock?.events?.onTimeTick()
+                    clockController.clock?.largeClock?.events?.onTimeTick()
                 }
             }
         broadcastDispatcher.registerReceiver(
@@ -195,7 +210,13 @@
             ?.events
             ?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
         clockView?.let { parentView.removeView(it) }
-        clockView = clockController.clock?.largeClock?.view?.apply { parentView.addView(this) }
+        clockView =
+            clockController.clock?.largeClock?.view?.apply {
+                if (shouldHighlightSelectedAffordance) {
+                    alpha = DIM_ALPHA
+                }
+                parentView.addView(this)
+            }
     }
 
     companion object {
@@ -203,5 +224,7 @@
         private const val KEY_VIEW_WIDTH = "width"
         private const val KEY_VIEW_HEIGHT = "height"
         private const val KEY_DISPLAY_ID = "display_id"
+
+        private const val DIM_ALPHA = 0.3f
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index e164f5d..8d6545a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -21,11 +21,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -37,33 +36,46 @@
 constructor(
     private val interactor: KeyguardTransitionInteractor,
 ) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_LOCKSCREEN_DURATION,
+            transitionFlow = interactor.dreamingToLockscreenTransition,
+        )
 
     /** Dream overlay y-translation on exit */
     fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
-            EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
-        }
+        return transitionAnimation.createFlow(
+            duration = 600.milliseconds,
+            onStep = { it * translatePx },
+            interpolator = EMPHASIZED_ACCELERATE,
+        )
     }
     /** Dream overlay views alpha - fade out */
-    val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+    val dreamOverlayAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
+        )
 
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-            -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
-        }
+        return transitionAnimation.createFlow(
+            duration = TO_LOCKSCREEN_DURATION,
+            onStep = { value -> -translatePx + value * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_DECELERATE,
+        )
     }
 
     /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.dreamingToLockscreenTransition,
-            params,
-            totalDuration = TO_LOCKSCREEN_DURATION
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onStep = { it },
         )
-    }
 
     companion object {
         /* Length of time before ending the dream activity, in order to start unoccluding */
@@ -71,11 +83,5 @@
         @JvmField
         val LOCKSCREEN_ANIMATION_DURATION_MS =
             (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
-
-        val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
-        val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
-        val LOCKSCREEN_ALPHA =
-            AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
new file mode 100644
index 0000000..f16827d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
+@SysUISingleton
+class GoneToDreamingTransitionViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardTransitionInteractor,
+) {
+
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_DREAMING_DURATION,
+            transitionFlow = interactor.goneToDreamingTransition,
+        )
+
+    /** Lockscreen views y-translation */
+    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+        return transitionAnimation.createFlow(
+            duration = 500.milliseconds,
+            onStep = { it * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_ACCELERATE,
+        )
+    }
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 5d85680..e7184f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -45,12 +45,17 @@
     private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
     private val burnInHelperWrapper: BurnInHelperWrapper,
 ) {
+    data class PreviewMode(
+        val isInPreviewMode: Boolean = false,
+        val shouldHighlightSelectedAffordance: Boolean = false,
+    )
+
     /**
      * Whether this view-model instance is powering the preview experience that renders exclusively
      * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
      * experience.
      */
-    private val isInPreviewMode = MutableStateFlow(false)
+    private val previewMode = MutableStateFlow(PreviewMode())
 
     /**
      * ID of the slot that's currently selected in the preview that renders exclusively in the
@@ -87,8 +92,8 @@
         keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
     /** An observable for the alpha level for the entire bottom area. */
     val alpha: Flow<Float> =
-        isInPreviewMode.flatMapLatest { isInPreviewMode ->
-            if (isInPreviewMode) {
+        previewMode.flatMapLatest {
+            if (it.isInPreviewMode) {
                 flowOf(1f)
             } else {
                 bottomAreaInteractor.alpha.distinctUntilChanged()
@@ -129,9 +134,18 @@
      * lock screen.
      *
      * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
+     * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be
+     * highlighted (while all others are dimmed to make the selected one stand out).
      */
-    fun enablePreviewMode(initiallySelectedSlotId: String?) {
-        isInPreviewMode.value = true
+    fun enablePreviewMode(
+        initiallySelectedSlotId: String?,
+        shouldHighlightSelectedAffordance: Boolean,
+    ) {
+        previewMode.value =
+            PreviewMode(
+                isInPreviewMode = true,
+                shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
+            )
         onPreviewSlotSelected(
             initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
         )
@@ -150,9 +164,9 @@
     private fun button(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceViewModel> {
-        return isInPreviewMode.flatMapLatest { isInPreviewMode ->
+        return previewMode.flatMapLatest { previewMode ->
             combine(
-                    if (isInPreviewMode) {
+                    if (previewMode.isInPreviewMode) {
                         quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
                     } else {
                         quickAffordanceInteractor.quickAffordance(position = position)
@@ -161,11 +175,19 @@
                     areQuickAffordancesFullyOpaque,
                     selectedPreviewSlotId,
                 ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId ->
+                    val isSelected = selectedPreviewSlotId == position.toSlotId()
                     model.toViewModel(
-                        animateReveal = !isInPreviewMode && animateReveal,
-                        isClickable = isFullyOpaque && !isInPreviewMode,
+                        animateReveal = !previewMode.isInPreviewMode && animateReveal,
+                        isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
                         isSelected =
-                            (isInPreviewMode && selectedPreviewSlotId == position.toSlotId()),
+                            previewMode.isInPreviewMode &&
+                                previewMode.shouldHighlightSelectedAffordance &&
+                                isSelected,
+                        isDimmed =
+                            previewMode.isInPreviewMode &&
+                                previewMode.shouldHighlightSelectedAffordance &&
+                                !isSelected,
+                        forceInactive = previewMode.isInPreviewMode
                     )
                 }
                 .distinctUntilChanged()
@@ -176,6 +198,8 @@
         animateReveal: Boolean,
         isClickable: Boolean,
         isSelected: Boolean,
+        isDimmed: Boolean,
+        forceInactive: Boolean,
     ): KeyguardQuickAffordanceViewModel {
         return when (this) {
             is KeyguardQuickAffordanceModel.Visible ->
@@ -191,9 +215,10 @@
                         )
                     },
                     isClickable = isClickable,
-                    isActivated = activationState is ActivationState.Active,
+                    isActivated = !forceInactive && activationState is ActivationState.Active,
                     isSelected = isSelected,
                     useLongPress = quickAffordanceInteractor.useLongPress,
+                    isDimmed = isDimmed,
                 )
             is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index e5d4e49..97e94d8f3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -22,11 +22,11 @@
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 /** Models UI state for the lock screen bouncer; handles user input. */
 class KeyguardBouncerViewModel
@@ -41,13 +41,12 @@
     /** Observe on bouncer visibility. */
     val isBouncerVisible: Flow<Boolean> = interactor.isVisible
 
+    /** Can the user interact with the view? */
+    val isInteractable: Flow<Boolean> = interactor.isInteractable
+
     /** Observe whether bouncer is showing. */
     val show: Flow<KeyguardBouncerModel> = interactor.show
 
-    /** Observe visible expansion when bouncer is showing. */
-    val showWithFullExpansion: Flow<KeyguardBouncerModel> =
-        interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
-
     /** Observe whether bouncer is hiding. */
     val hide: Flow<Unit> = interactor.hide
 
@@ -69,8 +68,16 @@
     /** Observe whether keyguard is authenticated already. */
     val keyguardAuthenticated: Flow<Boolean> = interactor.keyguardAuthenticated
 
-    /** Observe whether screen is turned off. */
-    val screenTurnedOff: Flow<Unit> = interactor.screenTurnedOff
+    /** Observe whether the side fps is showing. */
+    val sideFpsShowing: Flow<Boolean> = interactor.sideFpsShowing
+
+    /** Observe whether we should update fps is showing. */
+    val shouldUpdateSideFps: Flow<Unit> =
+        merge(
+            interactor.startingToHide,
+            interactor.isVisible.map {},
+            interactor.startingDisappearAnimation.filterNotNull().map {}
+        )
 
     /** Observe whether we want to update resources. */
     fun notifyUpdateResources() {
@@ -87,6 +94,10 @@
         interactor.onMessageShown()
     }
 
+    fun updateSideFpsVisibility() {
+        interactor.updateSideFpsVisibility()
+    }
+
     /** Observe whether back button is enabled. */
     fun observeOnIsBackButtonEnabled(systemUiVisibility: () -> Int): Flow<Int> {
         return interactor.isBackButtonEnabled.map { enabled ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
new file mode 100644
index 0000000..d896390
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Models UI state to support the lock screen long-press feature. */
+class KeyguardLongPressViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardLongPressInteractor,
+) {
+
+    /** Whether the long-press handling feature should be enabled. */
+    val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
+
+    /** View-model for a menu that should be shown; `null` when no menu should be shown. */
+    val menu: Flow<KeyguardSettingsPopupMenuViewModel?> =
+        interactor.menu.map { model ->
+            model?.let {
+                KeyguardSettingsPopupMenuViewModel(
+                    icon =
+                        Icon.Resource(
+                            res = R.drawable.ic_settings,
+                            contentDescription = null,
+                        ),
+                    text =
+                        Text.Resource(
+                            res = R.string.lock_screen_settings,
+                        ),
+                    position = model.position,
+                    onClicked = model.onClicked,
+                    onDismissed = model.onDismissed,
+                )
+            }
+        }
+
+    /** Notifies that the user has long-pressed on the lock screen. */
+    fun onLongPress(
+        x: Int,
+        y: Int,
+    ) {
+        interactor.onLongPress(
+            x = x,
+            y = y,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index cf3a6da..cb68a82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -31,6 +31,7 @@
     val isActivated: Boolean = false,
     val isSelected: Boolean = false,
     val useLongPress: Boolean = false,
+    val isDimmed: Boolean = false,
 ) {
     data class OnClickedParameters(
         val configKey: String,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt
new file mode 100644
index 0000000..0571b05
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Position
+import com.android.systemui.common.shared.model.Text
+
+/** Models the UI state of a keyguard settings popup menu. */
+data class KeyguardSettingsPopupMenuViewModel(
+    val icon: Icon,
+    val text: Text,
+    /** Where the menu should be anchored, roughly in screen space. */
+    val position: Position,
+    /** Callback to invoke when the menu gets clicked by the user. */
+    val onClicked: () -> Unit,
+    /** Callback to invoke when the menu gets dismissed by the user. */
+    val onDismissed: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
new file mode 100644
index 0000000..bc9dc4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class LockscreenToDreamingTransitionViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardTransitionInteractor,
+) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_DREAMING_DURATION,
+            transitionFlow = interactor.lockscreenToDreamingTransition,
+        )
+
+    /** Lockscreen views y-translation */
+    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+        return transitionAnimation.createFlow(
+            duration = 500.milliseconds,
+            onStep = { it * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_ACCELERATE,
+        )
+    }
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
+        )
+
+    companion object {
+        @JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
new file mode 100644
index 0000000..a60665a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class LockscreenToOccludedTransitionViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardTransitionInteractor,
+) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_OCCLUDED_DURATION,
+            transitionFlow = interactor.lockscreenToOccludedTransition,
+        )
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
+        )
+
+    /** Lockscreen views y-translation */
+    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+        return transitionAnimation.createFlow(
+            duration = TO_OCCLUDED_DURATION,
+            onStep = { value -> value * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_ACCELERATE,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index e804562..5770f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -20,11 +20,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -36,28 +35,26 @@
 constructor(
     private val interactor: KeyguardTransitionInteractor,
 ) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_LOCKSCREEN_DURATION,
+            transitionFlow = interactor.occludedToLockscreenTransition,
+        )
+
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-            -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
-        }
-    }
-
-    /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.occludedToLockscreenTransition,
-            params,
-            totalDuration = TO_LOCKSCREEN_DURATION
+        return transitionAnimation.createFlow(
+            duration = TO_LOCKSCREEN_DURATION,
+            onStep = { value -> -translatePx + value * translatePx },
+            interpolator = EMPHASIZED_DECELERATE,
         )
     }
 
-    companion object {
-        @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
-        val LOCKSCREEN_ALPHA =
-            AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
-    }
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onStep = { it },
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..0890791
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down PRIMARY_BOUNCER->GONE transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class PrimaryBouncerToGoneTransitionViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardTransitionInteractor,
+) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_GONE_DURATION,
+            transitionFlow = interactor.primaryBouncerToGoneTransition,
+        )
+
+    /** Bouncer container alpha */
+    val bouncerAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 200.milliseconds,
+            onStep = { 1f - it },
+        )
+
+    /** Scrim alpha */
+    val scrimAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = TO_GONE_DURATION,
+            interpolator = EMPHASIZED_ACCELERATE,
+            onStep = { 1f - it },
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index e364918..d69ac7f 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -145,7 +145,7 @@
  * └───────────────┴───────────────────┴──────────────┴─────────────────┘
  * ```
  */
-private class ViewLifecycleOwner(
+class ViewLifecycleOwner(
     private val view: View,
 ) : LifecycleOwner {
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
new file mode 100644
index 0000000..f7349a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -0,0 +1,181 @@
+package com.android.systemui.log
+
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import com.android.keyguard.FaceAuthUiEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.FaceAuthLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "KeyguardFaceAuthManagerLog"
+
+/**
+ * Helper class for logging for [com.android.keyguard.faceauth.KeyguardFaceAuthManager]
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ *   adb shell settings put global systemui/buffer/KeyguardFaceAuthManagerLog <logLevel>
+ *
+ * ```
+ */
+@SysUISingleton
+class FaceAuthenticationLogger
+@Inject
+constructor(
+    @FaceAuthLog private val logBuffer: LogBuffer,
+) {
+    fun ignoredFaceAuthTrigger(uiEvent: FaceAuthUiEvent) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = uiEvent.reason },
+            {
+                "Ignoring trigger because face auth is currently running. " +
+                    "Trigger reason: $str1"
+            }
+        )
+    }
+
+    fun queuingRequestWhileCancelling(
+        alreadyQueuedRequest: FaceAuthUiEvent?,
+        newRequest: FaceAuthUiEvent
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = alreadyQueuedRequest?.reason
+                str2 = newRequest.reason
+            },
+            {
+                "Face auth requested while previous request is being cancelled, " +
+                    "already queued request: $str1 queueing the new request: $str2"
+            }
+        )
+    }
+
+    fun authenticating(uiEvent: FaceAuthUiEvent) {
+        logBuffer.log(TAG, DEBUG, { str1 = uiEvent.reason }, { "Running authenticate for $str1" })
+    }
+
+    fun detectionNotSupported(
+        faceManager: FaceManager?,
+        sensorPropertiesInternal: MutableList<FaceSensorPropertiesInternal>?
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                bool1 = faceManager == null
+                bool2 = sensorPropertiesInternal.isNullOrEmpty()
+                bool2 = sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false
+            },
+            {
+                "skipping detection request because it is not supported, " +
+                    "faceManager isNull: $bool1, " +
+                    "sensorPropertiesInternal isNullOrEmpty: $bool2, " +
+                    "supportsFaceDetection: $bool3"
+            }
+        )
+    }
+
+    fun skippingBecauseAlreadyRunning(@CompileTimeConstant operation: String) {
+        logBuffer.log(TAG, DEBUG, "isAuthRunning is true, skipping $operation")
+    }
+
+    fun faceDetectionStarted() {
+        logBuffer.log(TAG, DEBUG, "Face detection started.")
+    }
+
+    fun faceDetected() {
+        logBuffer.log(TAG, DEBUG, "Face detected")
+    }
+
+    fun cancelSignalNotReceived(
+        isAuthRunning: Boolean,
+        isLockedOut: Boolean,
+        cancellationInProgress: Boolean,
+        faceAuthRequestedWhileCancellation: FaceAuthUiEvent?
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                bool1 = isAuthRunning
+                bool2 = isLockedOut
+                bool3 = cancellationInProgress
+                str1 = "${faceAuthRequestedWhileCancellation?.reason}"
+            },
+            {
+                "Cancel signal was not received, running timeout handler to reset state. " +
+                    "State before reset: " +
+                    "isAuthRunning: $bool1, " +
+                    "isLockedOut: $bool2, " +
+                    "cancellationInProgress: $bool3, " +
+                    "faceAuthRequestedWhileCancellation: $str1"
+            }
+        )
+    }
+
+    fun authenticationFailed() {
+        logBuffer.log(TAG, DEBUG, "Face authentication failed")
+    }
+
+    fun authenticationAcquired(acquireInfo: Int) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { int1 = acquireInfo },
+            { "Face acquired during face authentication: acquireInfo: $int1 " }
+        )
+    }
+
+    fun authenticationError(
+        errorCode: Int,
+        errString: CharSequence?,
+        lockoutError: Boolean,
+        cancellationError: Boolean
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = errorCode
+                str1 = "$errString"
+                bool1 = lockoutError
+                bool2 = cancellationError
+            },
+            {
+                "Received authentication error: errorCode: $int1, " +
+                    "errString: $str1, " +
+                    "isLockoutError: $bool1, " +
+                    "isCancellationError: $bool2"
+            }
+        )
+    }
+
+    fun launchingQueuedFaceAuthRequest(faceAuthRequestedWhileCancellation: FaceAuthUiEvent?) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = "${faceAuthRequestedWhileCancellation?.reason}" },
+            { "Received cancellation error and starting queued face auth request: $str1" }
+        )
+    }
+
+    fun faceAuthSuccess(result: FaceManager.AuthenticationResult) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = result.userId
+                bool1 = result.isStrongBiometric
+            },
+            { "Face authenticated successfully: userId: $int1, isStrongBiometric: $bool1" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
new file mode 100644
index 0000000..5acaa46
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.log
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.RectF
+import androidx.core.graphics.toRectF
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.ScreenDecorationsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import javax.inject.Inject
+
+private const val TAG = "ScreenDecorationsLog"
+
+/**
+ * Helper class for logging for [com.android.systemui.ScreenDecorations]
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ *   adb shell settings put global systemui/buffer/ScreenDecorationsLog <logLevel>
+ *
+ * ```
+ */
+@SysUISingleton
+class ScreenDecorationsLogger
+@Inject
+constructor(
+    @ScreenDecorationsLog private val logBuffer: LogBuffer,
+) {
+    fun cameraProtectionBoundsForScanningOverlay(bounds: Rect) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = bounds.toShortString() },
+            { "Face scanning overlay present camera protection bounds: $str1" }
+        )
+    }
+
+    fun hwcLayerCameraProtectionBounds(bounds: Rect) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = bounds.toShortString() },
+            { "Hwc layer present camera protection bounds: $str1" }
+        )
+    }
+
+    fun dcvCameraBounds(id: Int, bounds: Rect) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = bounds.toShortString()
+                int1 = id
+            },
+            { "DisplayCutoutView id=$int1 present, camera protection bounds: $str1" }
+        )
+    }
+
+    fun cutoutViewNotInitialized() {
+        logBuffer.log(TAG, ERROR, "CutoutView not initialized showCameraProtection")
+    }
+
+    fun boundingRect(boundingRectangle: RectF, context: String) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = context
+                str2 = boundingRectangle.toShortString()
+            },
+            { "Bounding rect $str1 : $str2" }
+        )
+    }
+
+    fun boundingRect(boundingRectangle: Rect, context: String) {
+        boundingRect(boundingRectangle.toRectF(), context)
+    }
+
+    fun onMeasureDimensions(
+        widthMeasureSpec: Int,
+        heightMeasureSpec: Int,
+        measuredWidth: Int,
+        measuredHeight: Int
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                long1 = widthMeasureSpec.toLong()
+                long2 = heightMeasureSpec.toLong()
+                int1 = measuredWidth
+                int2 = measuredHeight
+            },
+            {
+                "Face scanning animation: widthMeasureSpec: $long1 measuredWidth: $int1, " +
+                    "heightMeasureSpec: $long2 measuredHeight: $int2"
+            }
+        )
+    }
+
+    fun faceSensorLocation(faceSensorLocation: Point?) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = faceSensorLocation?.y?.times(2) ?: 0
+                str1 = "$faceSensorLocation"
+            },
+            { "Reinflating view: Face sensor location: $str1, faceScanningHeight: $int1" }
+        )
+    }
+
+    fun onSensorLocationChanged() {
+        logBuffer.log(TAG, DEBUG, "AuthControllerCallback in ScreenDecorations triggered")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/FaceAuthLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/FaceAuthLog.kt
new file mode 100644
index 0000000..b97e3a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/FaceAuthLog.kt
@@ -0,0 +1,6 @@
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for Face authentication triggered by SysUI. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class FaceAuthLog()
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
index 0645236..9f563fe4 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -23,3 +23,15 @@
 @MustBeDocumented
 @Retention(AnnotationRetention.RUNTIME)
 annotation class KeyguardClockLog
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for small keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardSmallClockLog
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for large keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardLargeClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index bc29858..642c9f7 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -62,6 +62,15 @@
         return factory.create("NotifLog", maxSize, false /* systrace */);
     }
 
+    /** Provides a logging buffer for all logs related to notifications on the lockscreen. */
+    @Provides
+    @SysUISingleton
+    @NotificationLockscreenLog
+    public static LogBuffer provideNotificationLockScreenLogBuffer(
+            LogBufferFactory factory) {
+        return factory.create("NotifLockscreenLog", 50, false /* systrace */);
+    }
+
     /** Provides a logging buffer for logs related to heads up presentation of notifications. */
     @Provides
     @SysUISingleton
@@ -191,37 +200,12 @@
                 false /* systrace */);
     }
 
-    /**
-     * Provides a logging buffer for logs related to swiping away the status bar while in immersive
-     * mode. See {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
-     */
+    /** Provides a logging buffer for logs related to swipe up gestures. */
     @Provides
     @SysUISingleton
-    @SwipeStatusBarAwayLog
-    public static LogBuffer provideSwipeAwayGestureLogBuffer(LogBufferFactory factory) {
-        return factory.create("SwipeStatusBarAwayLog", 30);
-    }
-
-    /**
-     * Provides a logging buffer for logs related to the media tap-to-transfer chip on the sender
-     * device. See {@link com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger}.
-     */
-    @Provides
-    @SysUISingleton
-    @MediaTttSenderLogBuffer
-    public static LogBuffer provideMediaTttSenderLogBuffer(LogBufferFactory factory) {
-        return factory.create("MediaTttSender", 20);
-    }
-
-    /**
-     * Provides a logging buffer for logs related to the media tap-to-transfer chip on the receiver
-     * device. See {@link com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger}.
-     */
-    @Provides
-    @SysUISingleton
-    @MediaTttReceiverLogBuffer
-    public static LogBuffer provideMediaTttReceiverLogBuffer(LogBufferFactory factory) {
-        return factory.create("MediaTttReceiver", 20);
+    @SwipeUpLog
+    public static LogBuffer provideSwipeUpLogBuffer(LogBufferFactory factory) {
+        return factory.create("SwipeUpLog", 30);
     }
 
     /**
@@ -290,16 +274,6 @@
         return factory.create("MediaCarouselCtlrLog", 20);
     }
 
-    /**
-     * Provides a {@link LogBuffer} for use in the status bar connectivity pipeline
-     */
-    @Provides
-    @SysUISingleton
-    @StatusBarConnectivityLog
-    public static LogBuffer provideStatusBarConnectivityBuffer(LogBufferFactory factory) {
-        return factory.create("SbConnectivity", 64);
-    }
-
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
@@ -335,13 +309,33 @@
     }
 
     /**
-     * Provides a {@link LogBuffer} for keyguard clock logs.
+     * Provides a {@link LogBuffer} for general keyguard clock logs.
      */
     @Provides
     @SysUISingleton
     @KeyguardClockLog
     public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
-        return factory.create("KeyguardClockLog", 500);
+        return factory.create("KeyguardClockLog", 100);
+    }
+
+    /**
+     * Provides a {@link LogBuffer} for keyguard small clock logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardSmallClockLog
+    public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) {
+        return factory.create("KeyguardSmallClockLog", 100);
+    }
+
+    /**
+     * Provides a {@link LogBuffer} for keyguard large clock logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardLargeClockLog
+    public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) {
+        return factory.create("KeyguardLargeClockLog", 100);
     }
 
     /**
@@ -355,6 +349,27 @@
     }
 
     /**
+     * Provides a {@link LogBuffer} for use by {@link com.android.systemui.ScreenDecorations}.
+     */
+    @Provides
+    @SysUISingleton
+    @ScreenDecorationsLog
+    public static LogBuffer provideScreenDecorationsLog(LogBufferFactory factory) {
+        return factory.create("ScreenDecorationsLog", 200);
+    }
+
+    /**
+     * Provides a {@link LogBuffer} for use by
+     *  {@link com.android.keyguard.faceauth.KeyguardFaceAuthManagerImpl}.
+     */
+    @Provides
+    @SysUISingleton
+    @FaceAuthLog
+    public static LogBuffer provideFaceAuthLog(LogBufferFactory factory) {
+        return factory.create("KeyguardFaceAuthManagerLog", 300);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for bluetooth-related logs.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLockscreenLog.java
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
rename to packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLockscreenLog.java
index 67cdb72..a2d381e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLockscreenLog.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -25,11 +25,9 @@
 
 import javax.inject.Qualifier;
 
-/**
- * A {@link LogBuffer} for status bar connectivity events.
- */
+/** A {@link LogBuffer} for notification & lockscreen related messages. */
 @Qualifier
 @Documented
 @Retention(RUNTIME)
-public @interface StatusBarConnectivityLog {
+public @interface NotificationLockscreenLog {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
index 67733e9..de2a8b6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.log.dagger
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for ScreenDecorations added by SysUI. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ScreenDecorationsLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeUpLog.java
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
rename to packages/SystemUI/src/com/android/systemui/log/dagger/SwipeUpLog.java
index 4c276e2..d58b538 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeUpLog.java
@@ -27,10 +27,10 @@
 
 /**
  * A {@link LogBuffer} for
- * {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
+ * {@link com.android.systemui.statusbar.gesture.SwipeUpGestureLogger}.
  */
 @Qualifier
 @Documented
 @Retention(RUNTIME)
-public @interface SwipeStatusBarAwayLog {
+public @interface SwipeUpLog {
 }
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/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
index 7a90a74..7ccc43c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
@@ -29,6 +29,18 @@
     private val dumpManager: DumpManager,
     private val systemClock: SystemClock,
 ) {
+    private val existingBuffers = mutableMapOf<String, TableLogBuffer>()
+
+    /**
+     * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where
+     * it is guaranteed only to be created one time. See [getOrCreate] for a cache-aware method of
+     * obtaining a buffer.
+     *
+     * @param name a unique table name
+     * @param maxSize the buffer max size. See [adjustMaxSize]
+     *
+     * @return a new [TableLogBuffer] registered with [DumpManager]
+     */
     fun create(
         name: String,
         maxSize: Int,
@@ -37,4 +49,23 @@
         dumpManager.registerNormalDumpable(name, tableBuffer)
         return tableBuffer
     }
+
+    /**
+     * Log buffers are retained indefinitely by [DumpManager], so that they can be represented in
+     * bugreports. Because of this, many of them are created statically in the Dagger graph.
+     *
+     * In the case where you have to create a logbuffer with a name only known at runtime, this
+     * method can be used to lazily create a table log buffer which is then cached for reuse.
+     *
+     * @return a [TableLogBuffer] suitable for reuse
+     */
+    fun getOrCreate(
+        name: String,
+        maxSize: Int,
+    ): TableLogBuffer =
+        existingBuffers.getOrElse(name) {
+            val buffer = create(name, maxSize)
+            existingBuffers[name] = buffer
+            buffer
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index ceb4845..52d4171 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -18,6 +18,7 @@
 import android.app.ActivityOptions
 import android.content.Intent
 import android.content.res.Configuration
+import android.content.res.Resources
 import android.media.projection.IMediaProjection
 import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
 import android.os.Binder
@@ -27,11 +28,15 @@
 import android.os.UserHandle
 import android.view.ViewGroup
 import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
 import com.android.internal.app.ChooserActivity
 import com.android.internal.app.ResolverListController
 import com.android.internal.app.chooser.NotSelectableTargetInfo
 import com.android.internal.app.chooser.TargetInfo
 import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -45,6 +50,7 @@
 class MediaProjectionAppSelectorActivity(
     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
     private val activityLauncher: AsyncActivityLauncher,
+    private val featureFlags: FeatureFlags,
     /** This is used to override the dependency in a screenshot test */
     @VisibleForTesting
     private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
@@ -54,21 +60,18 @@
     constructor(
         componentFactory: MediaProjectionAppSelectorComponent.Factory,
         activityLauncher: AsyncActivityLauncher,
-    ) : this(componentFactory, activityLauncher, null)
+        featureFlags: FeatureFlags
+    ) : this(componentFactory, activityLauncher, featureFlags, listControllerFactory = null)
 
     private lateinit var configurationController: ConfigurationController
     private lateinit var controller: MediaProjectionAppSelectorController
     private lateinit var recentsViewController: MediaProjectionRecentsViewController
+    private lateinit var component: MediaProjectionAppSelectorComponent
 
     override fun getLayoutResource() = R.layout.media_projection_app_selector
 
     public override fun onCreate(bundle: Bundle?) {
-        val component =
-            componentFactory.create(
-                activity = this,
-                view = this,
-                resultHandler = this
-            )
+        component = componentFactory.create(activity = this, view = this, resultHandler = this)
 
         // Create a separate configuration controller for this activity as the configuration
         // might be different from the global one
@@ -76,11 +79,12 @@
         controller = component.controller
         recentsViewController = component.recentsViewController
 
-        val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
-        intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
+        intent.configureChooserIntent(
+            resources,
+            component.hostUserHandle,
+            component.personalProfileUserHandle
+        )
 
-        val title = getString(R.string.media_projection_permission_app_selector_title)
-        intent.putExtra(Intent.EXTRA_TITLE, title)
         super.onCreate(bundle)
         controller.init()
     }
@@ -92,6 +96,13 @@
 
     override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
 
+    override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
+        if (featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+            component.emptyStateProvider
+        } else {
+            super.createBlockerEmptyStateProvider()
+        }
+
     override fun createListController(userHandle: UserHandle): ResolverListController =
         listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
 
@@ -183,6 +194,13 @@
 
     override fun shouldShowContentPreview() = true
 
+    override fun shouldShowContentPreviewWhenEmpty(): Boolean = true
+
+    override fun createMyUserIdProvider(): MyUserIdProvider =
+        object : MyUserIdProvider() {
+            override fun getMyUserId(): Int = component.hostUserHandle.identifier
+        }
+
     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
         recentsViewController.createView(parent)
 
@@ -193,6 +211,34 @@
          * instance through activity result.
          */
         const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
+
+        /** UID of the app that originally launched the media projection flow (host app user) */
+        const val EXTRA_HOST_APP_USER_HANDLE = "launched_from_user_handle"
         const val KEY_CAPTURE_TARGET = "capture_region"
+
+        /** Set up intent for the [ChooserActivity] */
+        private fun Intent.configureChooserIntent(
+            resources: Resources,
+            hostUserHandle: UserHandle,
+            personalProfileUserHandle: UserHandle
+        ) {
+            // Specify the query intent to show icons for all apps on the chooser screen
+            val queryIntent =
+                Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
+            putExtra(Intent.EXTRA_INTENT, queryIntent)
+
+            // Update the title of the chooser
+            val title = resources.getString(R.string.media_projection_permission_app_selector_title)
+            putExtra(Intent.EXTRA_TITLE, title)
+
+            // Select host app's profile tab by default
+            val selectedProfile =
+                if (hostUserHandle == personalProfileUserHandle) {
+                    PROFILE_PERSONAL
+                } else {
+                    PROFILE_WORK
+                }
+            putExtra(EXTRA_SELECTED_PROFILE, selectedProfile)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index bfa67a8..c4e76b2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP;
 
 import android.app.Activity;
+import android.app.ActivityManager;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -35,6 +36,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 import android.text.BidiFormatter;
 import android.text.SpannableString;
 import android.text.TextPaint;
@@ -43,33 +45,46 @@
 import android.util.Log;
 import android.view.Window;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
 import com.android.systemui.screenrecord.MediaProjectionPermissionDialog;
 import com.android.systemui.screenrecord.ScreenShareOption;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.Utils;
 
+import javax.inject.Inject;
+
+import dagger.Lazy;
+
 public class MediaProjectionPermissionActivity extends Activity
         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
     private static final String TAG = "MediaProjectionPermissionActivity";
     private static final float MAX_APP_NAME_SIZE_PX = 500f;
     private static final String ELLIPSIS = "\u2026";
 
+    private final FeatureFlags mFeatureFlags;
+    private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver;
+
     private String mPackageName;
     private int mUid;
     private IMediaProjectionManager mService;
-    private FeatureFlags mFeatureFlags;
 
     private AlertDialog mDialog;
 
+    @Inject
+    public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
+            Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
+        mFeatureFlags = featureFlags;
+        mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
+    }
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        mFeatureFlags = Dependency.get(FeatureFlags.class);
         mPackageName = getCallingPackage();
         IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
         mService = IMediaProjectionManager.Stub.asInterface(b);
@@ -102,6 +117,12 @@
             return;
         }
 
+        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+            if (showScreenCaptureDisabledDialogIfNeeded()) {
+                return;
+            }
+        }
+
         TextPaint paint = new TextPaint();
         paint.setTextSize(42);
 
@@ -169,16 +190,7 @@
             mDialog = dialogBuilder.create();
         }
 
-        SystemUIDialog.registerDismissListener(mDialog);
-        SystemUIDialog.applyFlags(mDialog);
-        SystemUIDialog.setDialogSize(mDialog);
-
-        mDialog.setOnCancelListener(this);
-        mDialog.create();
-        mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
-
-        final Window w = mDialog.getWindow();
-        w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+        setUpDialog(mDialog);
 
         mDialog.show();
     }
@@ -198,6 +210,32 @@
         }
     }
 
+    private void setUpDialog(AlertDialog dialog) {
+        SystemUIDialog.registerDismissListener(dialog);
+        SystemUIDialog.applyFlags(dialog);
+        SystemUIDialog.setDialogSize(dialog);
+
+        dialog.setOnCancelListener(this);
+        dialog.create();
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+
+        final Window w = dialog.getWindow();
+        w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+    }
+
+    private boolean showScreenCaptureDisabledDialogIfNeeded() {
+        final UserHandle hostUserHandle = getHostUserHandle();
+        if (mScreenCaptureDevicePolicyResolver.get()
+                .isScreenCaptureCompletelyDisabled(hostUserHandle)) {
+            AlertDialog dialog = new ScreenCaptureDisabledDialog(this);
+            setUpDialog(dialog);
+            dialog.show();
+            return true;
+        }
+
+        return false;
+    }
+
     private void grantMediaProjectionPermission(int screenShareMode) {
         try {
             if (screenShareMode == ENTIRE_SCREEN) {
@@ -208,8 +246,14 @@
                 final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class);
                 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
                         projection.asBinder());
+                intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+                        getHostUserHandle());
                 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
-                startActivity(intent);
+
+                // Start activity from the current foreground user to avoid creating a separate
+                // SystemUI process without access to recent tasks because it won't have
+                // WM Shell running inside.
+                startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser()));
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error granting projection permission", e);
@@ -222,6 +266,10 @@
         }
     }
 
+    private UserHandle getHostUserHandle() {
+        return UserHandle.getUserHandleForUid(getLaunchedFromUid());
+    }
+
     private IMediaProjection createProjection(int uid, String packageName) throws RemoteException {
         return mService.createProjection(uid, packageName,
                 MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index f006442..b7a2522 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -88,7 +88,13 @@
     val instanceId: InstanceId,
 
     /** The UID of the app, used for logging */
-    val appUid: Int
+    val appUid: Int,
+
+    /** Whether explicit indicator exists */
+    val isExplicit: Boolean = false,
+
+    /** Track progress (0 - 1) to display for players where [resumption] is true */
+    val resumeProgress: Double? = null,
 ) {
     companion object {
         /** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
index a8f39fa9a..1c8bfd1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
@@ -24,6 +24,7 @@
 import android.widget.SeekBar
 import android.widget.TextView
 import androidx.constraintlayout.widget.Barrier
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.R
 import com.android.systemui.media.controls.models.GutsViewHolder
 import com.android.systemui.surfaceeffects.ripple.MultiRippleView
@@ -44,6 +45,7 @@
     val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
     val titleText = itemView.requireViewById<TextView>(R.id.header_title)
     val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
+    val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator)
 
     // Output switcher
     val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
@@ -123,6 +125,7 @@
                 R.id.app_name,
                 R.id.header_title,
                 R.id.header_artist,
+                R.id.media_explicit_indicator,
                 R.id.media_seamless,
                 R.id.media_progress_bar,
                 R.id.actionPlayPause,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index bba5f35..a057c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -238,6 +238,24 @@
     }
 
     /**
+     * Set the progress to a fixed percentage value that cannot be changed by the user.
+     *
+     * @param percent value between 0 and 1
+     */
+    fun updateStaticProgress(percent: Double) {
+        val position = (percent * 100).toInt()
+        _data =
+            Progress(
+                enabled = true,
+                seekAvailable = false,
+                playing = false,
+                scrubbing = false,
+                elapsedTime = position,
+                duration = 100,
+            )
+    }
+
+    /**
      * Puts the seek bar into a resumption state.
      *
      * This should be called when the media session behind the controller has been destroyed.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
index 1a10b18..70f2dee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -20,7 +20,9 @@
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
+import android.widget.SeekBar
 import android.widget.TextView
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.R
 import com.android.systemui.media.controls.models.GutsViewHolder
 import com.android.systemui.media.controls.ui.IlluminationDrawable
@@ -29,18 +31,16 @@
 private const val TAG = "RecommendationViewHolder"
 
 /** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View) {
+class RecommendationViewHolder private constructor(itemView: View, updatedView: Boolean) {
 
     val recommendations = itemView as TransitionLayout
 
     // Recommendation screen
-    val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
-    val mediaCoverItems =
-        listOf<ImageView>(
-            itemView.requireViewById(R.id.media_cover1),
-            itemView.requireViewById(R.id.media_cover2),
-            itemView.requireViewById(R.id.media_cover3)
-        )
+    lateinit var cardIcon: ImageView
+    lateinit var mediaAppIcons: List<CachingIconView>
+    lateinit var mediaProgressBars: List<SeekBar>
+    lateinit var cardTitle: TextView
+
     val mediaCoverContainers =
         listOf<ViewGroup>(
             itemView.requireViewById(R.id.media_cover1_container),
@@ -48,21 +48,52 @@
             itemView.requireViewById(R.id.media_cover3_container)
         )
     val mediaTitles: List<TextView> =
-        listOf(
-            itemView.requireViewById(R.id.media_title1),
-            itemView.requireViewById(R.id.media_title2),
-            itemView.requireViewById(R.id.media_title3)
-        )
+        if (updatedView) {
+            mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
+        } else {
+            listOf(
+                itemView.requireViewById(R.id.media_title1),
+                itemView.requireViewById(R.id.media_title2),
+                itemView.requireViewById(R.id.media_title3)
+            )
+        }
     val mediaSubtitles: List<TextView> =
-        listOf(
-            itemView.requireViewById(R.id.media_subtitle1),
-            itemView.requireViewById(R.id.media_subtitle2),
-            itemView.requireViewById(R.id.media_subtitle3)
-        )
+        if (updatedView) {
+            mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
+        } else {
+            listOf(
+                itemView.requireViewById(R.id.media_subtitle1),
+                itemView.requireViewById(R.id.media_subtitle2),
+                itemView.requireViewById(R.id.media_subtitle3)
+            )
+        }
 
+    val mediaCoverItems: List<ImageView> =
+        if (updatedView) {
+            mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
+        } else {
+            listOf(
+                itemView.requireViewById(R.id.media_cover1),
+                itemView.requireViewById(R.id.media_cover2),
+                itemView.requireViewById(R.id.media_cover3)
+            )
+        }
     val gutsViewHolder = GutsViewHolder(itemView)
 
     init {
+        if (updatedView) {
+            mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
+            cardTitle = itemView.requireViewById(R.id.media_rec_title)
+            mediaProgressBars =
+                mediaCoverContainers.map {
+                    it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
+                        // Media playback is in the direction of tape, not time, so it stays LTR
+                        layoutDirection = View.LAYOUT_DIRECTION_LTR
+                    }
+                }
+        } else {
+            cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
+        }
         (recommendations.background as IlluminationDrawable).let { background ->
             mediaCoverContainers.forEach { background.registerLightSource(it) }
             background.registerLightSource(gutsViewHolder.cancel)
@@ -83,36 +114,52 @@
          * @param parent Parent of inflated view.
          */
         @JvmStatic
-        fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
+        fun create(
+            inflater: LayoutInflater,
+            parent: ViewGroup,
+            updatedView: Boolean,
+        ): RecommendationViewHolder {
             val itemView =
-                inflater.inflate(
-                    R.layout.media_smartspace_recommendations,
-                    parent,
-                    false /* attachToRoot */
-                )
+                if (updatedView) {
+                    inflater.inflate(
+                        R.layout.media_recommendations,
+                        parent,
+                        false /* attachToRoot */
+                    )
+                } else {
+                    inflater.inflate(
+                        R.layout.media_smartspace_recommendations,
+                        parent,
+                        false /* attachToRoot */
+                    )
+                }
             // Because this media view (a TransitionLayout) is used to measure and layout the views
             // in various states before being attached to its parent, we can't depend on the default
             // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
             itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-            return RecommendationViewHolder(itemView)
+            return RecommendationViewHolder(itemView, updatedView)
         }
 
         // Res Ids for the control components on the recommendation view.
         val controlsIds =
             setOf(
                 R.id.recommendation_card_icon,
+                R.id.media_rec_title,
                 R.id.media_cover1,
                 R.id.media_cover2,
                 R.id.media_cover3,
+                R.id.media_cover,
                 R.id.media_cover1_container,
                 R.id.media_cover2_container,
                 R.id.media_cover3_container,
                 R.id.media_title1,
                 R.id.media_title2,
                 R.id.media_title3,
+                R.id.media_title,
                 R.id.media_subtitle1,
                 R.id.media_subtitle2,
-                R.id.media_subtitle3
+                R.id.media_subtitle3,
+                R.id.media_subtitle,
             )
 
         val mediaTitlesAndSubtitlesIds =
@@ -120,9 +167,11 @@
                 R.id.media_title1,
                 R.id.media_title2,
                 R.id.media_title3,
+                R.id.media_title,
                 R.id.media_subtitle1,
                 R.id.media_subtitle2,
-                R.id.media_subtitle3
+                R.id.media_subtitle3,
+                R.id.media_subtitle,
             )
 
         val mediaContainersIds =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
index 1df42c6..0b57175 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
@@ -41,10 +41,12 @@
     val recommendations: List<SmartspaceAction>,
     /** Intent for the user's initiated dismissal. */
     val dismissIntent: Intent?,
-    /** The timestamp in milliseconds that headphone is connected. */
+    /** The timestamp in milliseconds that the card was generated */
     val headphoneConnectionTimeMillis: Long,
     /** Instance ID for [MediaUiEventLogger] */
-    val instanceId: InstanceId
+    val instanceId: InstanceId,
+    /** The timestamp in milliseconds indicating when the card should be removed */
+    val expiryTimeMs: Long,
 ) {
     /**
      * Indicates if all the data is valid.
@@ -86,5 +88,14 @@
     }
 }
 
+/** Key to indicate whether this card should be used to re-show recent media */
+const val EXTRA_KEY_TRIGGER_RESUME = "SHOULD_TRIGGER_RESUME"
+/** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */
+const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE"
+/** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */
+const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION"
+/** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */
+const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER"
+
 const val NUM_REQUIRED_RECOMMENDATIONS = 3
 private val TAG = SmartspaceMediaData::class.simpleName!!
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index cf71d67..97717a6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -23,7 +23,9 @@
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_RESUME
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -66,7 +68,8 @@
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     @Main private val executor: Executor,
     private val systemClock: SystemClock,
-    private val logger: MediaUiEventLogger
+    private val logger: MediaUiEventLogger,
+    private val mediaFlags: MediaFlags,
 ) : MediaDataManager.Listener {
     private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
     internal val listeners: Set<MediaDataManager.Listener>
@@ -121,7 +124,9 @@
         data: SmartspaceMediaData,
         shouldPrioritize: Boolean
     ) {
-        if (!data.isActive) {
+        // With persistent recommendation card, we could get a background update while inactive
+        // Otherwise, consider it an invalid update
+        if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) {
             Log.d(TAG, "Inactive recommendation data. Skip triggering.")
             return
         }
@@ -134,14 +139,23 @@
         val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
         val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
         var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
-        data.cardAction?.let {
-            val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
+        data.cardAction?.extras?.let {
+            val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
             if (smartspaceMaxAgeSeconds > 0) {
                 smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
             }
         }
 
-        val shouldReactivate = !hasActiveMedia() && hasAnyMedia()
+        // Check if smartspace has explicitly specified whether to re-activate resumable media.
+        // The default behavior is to trigger if the smartspace data is active.
+        val shouldTriggerResume =
+            if (data.cardAction?.extras?.containsKey(EXTRA_KEY_TRIGGER_RESUME) == true) {
+                data.cardAction.extras.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true)
+            } else {
+                true
+            }
+        val shouldReactivate =
+            shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive
 
         if (timeSinceActive < smartspaceMaxAgeMillis) {
             // It could happen there are existing active media resume cards, then we don't need to
@@ -169,7 +183,7 @@
                     )
                 }
             }
-        } else {
+        } else if (data.isActive) {
             // Mark to prioritize Smartspace card if no recent media.
             shouldPrioritizeMutable = true
         }
@@ -252,7 +266,7 @@
             if (dismissIntent == null) {
                 Log.w(
                     TAG,
-                    "Cannot create dismiss action click action: " + "extras missing dismiss_intent."
+                    "Cannot create dismiss action click action: extras missing dismiss_intent."
                 )
             } else if (
                 dismissIntent.getComponent() != null &&
@@ -264,15 +278,21 @@
             } else {
                 broadcastSender.sendBroadcast(dismissIntent)
             }
-            smartspaceMediaData =
-                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                    targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId
+
+            if (mediaFlags.isPersistentSsCardEnabled()) {
+                smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+                mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
+            } else {
+                smartspaceMediaData =
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = smartspaceMediaData.targetId,
+                        instanceId = smartspaceMediaData.instanceId,
+                    )
+                mediaDataManager.dismissSmartspaceRecommendation(
+                    smartspaceMediaData.targetId,
+                    delay = 0L,
                 )
-            mediaDataManager.dismissSmartspaceRecommendation(
-                smartspaceMediaData.targetId,
-                delay = 0L
-            )
+            }
         }
     }
 
@@ -283,8 +303,15 @@
                 (smartspaceMediaData.isValid() || reactivatedKey != null))
 
     /** Are there any media entries we should display? */
-    fun hasAnyMediaOrRecommendation() =
-        userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
+    fun hasAnyMediaOrRecommendation(): Boolean {
+        val hasSmartspace =
+            if (mediaFlags.isPersistentSsCardEnabled()) {
+                smartspaceMediaData.isValid()
+            } else {
+                smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+            }
+        return userEntries.isNotEmpty() || hasSmartspace
+    }
 
     /** Are there any media notifications active (excluding the recommendation)? */
     fun hasActiveMedia() = userEntries.any { it.value.active }
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 2dd339d..e70a2f3 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
@@ -45,11 +45,12 @@
 import android.os.UserHandle
 import android.provider.Settings
 import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
 import android.text.TextUtils
 import android.util.Log
 import androidx.media.utils.MediaConstants
-import com.android.internal.annotations.VisibleForTesting
 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
@@ -62,10 +63,14 @@
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.player.MediaDeviceData
 import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
 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
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.plugins.ActivityStarter
@@ -117,7 +122,6 @@
         appUid = Process.INVALID_UID
     )
 
-@VisibleForTesting
 internal val EMPTY_SMARTSPACE_MEDIA_DATA =
     SmartspaceMediaData(
         targetId = "INVALID",
@@ -127,7 +131,8 @@
         recommendations = emptyList(),
         dismissIntent = null,
         headphoneConnectionTimeMillis = 0,
-        instanceId = InstanceId.fakeInstanceId(-1)
+        instanceId = InstanceId.fakeInstanceId(-1),
+        expiryTimeMs = 0,
     )
 
 fun isMediaNotification(sbn: StatusBarNotification): Boolean {
@@ -173,6 +178,7 @@
     private val mediaFlags: MediaFlags,
     private val logger: MediaUiEventLogger,
     private val smartspaceManager: SmartspaceManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
 
     companion object {
@@ -237,6 +243,7 @@
         mediaFlags: MediaFlags,
         logger: MediaUiEventLogger,
         smartspaceManager: SmartspaceManager,
+        keyguardUpdateMonitor: KeyguardUpdateMonitor,
     ) : this(
         context,
         backgroundExecutor,
@@ -260,6 +267,7 @@
         mediaFlags,
         logger,
         smartspaceManager,
+        keyguardUpdateMonitor,
     )
 
     private val appChangeReceiver =
@@ -302,6 +310,7 @@
         mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
             updateState(key, state)
         }
+        mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
         mediaResumeListener.setManager(this)
         mediaDataFilter.mediaDataManager = this
 
@@ -545,6 +554,11 @@
             if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
             onMediaDataLoaded(key, key, it)
         }
+
+        if (key == smartspaceMediaData.targetId) {
+            if (DEBUG) Log.d(TAG, "smartspace card expired")
+            dismissSmartspaceRecommendation(key, delay = 0L)
+        }
     }
 
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
@@ -602,8 +616,8 @@
     }
 
     /**
-     * Called whenever the recommendation has been expired, or swiped from QQS. This will make the
-     * recommendation view to not be shown anymore during this headphone connection session.
+     * Called whenever the recommendation has been expired or removed by the user. This will remove
+     * the recommendation card entirely from the carousel.
      */
     fun dismissSmartspaceRecommendation(key: String, delay: Long) {
         if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
@@ -625,6 +639,23 @@
         )
     }
 
+    /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+    fun setRecommendationInactive(key: String) {
+        if (!mediaFlags.isPersistentSsCardEnabled()) {
+            Log.e(TAG, "Only persistent recommendation can be inactive!")
+            return
+        }
+        if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+            // If this doesn't match, or we've already invalidated the data, no action needed
+            return
+        }
+
+        smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+        notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+    }
+
     private fun loadMediaDataInBgForResumption(
         userId: Int,
         desc: MediaDescription,
@@ -634,7 +665,7 @@
         appIntent: PendingIntent,
         packageName: String
     ) {
-        if (TextUtils.isEmpty(desc.title)) {
+        if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
             // Delete the placeholder entry
             mediaEntries.remove(packageName)
@@ -660,6 +691,15 @@
         val currentEntry = mediaEntries.get(packageName)
         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
         val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+        val isExplicit =
+            desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
+                mediaFlags.isExplicitIndicatorEnabled()
+
+        val progress =
+            if (mediaFlags.isResumeProgressEnabled()) {
+                MediaDataUtils.getDescriptionProgress(desc.extras)
+            } else null
 
         val mediaAction = getResumeMediaAction(resumeAction)
         val lastActive = systemClock.elapsedRealtime()
@@ -689,7 +729,9 @@
                     hasCheckedForResume = true,
                     lastActive = lastActive,
                     instanceId = instanceId,
-                    appUid = appUid
+                    appUid = appUid,
+                    isExplicit = isExplicit,
+                    resumeProgress = progress,
                 )
             )
         }
@@ -750,6 +792,15 @@
             song = HybridGroupManager.resolveTitle(notif)
         }
 
+        // Explicit Indicator
+        var isExplicit = false
+        if (mediaFlags.isExplicitIndicatorEnabled()) {
+            val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+            isExplicit =
+                mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+        }
+
         // Artist name
         var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
         if (artist == null) {
@@ -848,10 +899,11 @@
                     notificationKey = key,
                     hasCheckedForResume = hasCheckedForResume,
                     isPlaying = isPlaying,
-                    isClearable = sbn.isClearable(),
+                    isClearable = !sbn.isOngoing,
                     lastActive = lastActive,
                     instanceId = instanceId,
-                    appUid = appUid
+                    appUid = appUid,
+                    isExplicit = isExplicit,
                 )
             )
         }
@@ -1241,12 +1293,25 @@
                 if (DEBUG) {
                     Log.d(TAG, "Set Smartspace media to be inactive for the data update")
                 }
-                smartspaceMediaData =
-                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                        targetId = smartspaceMediaData.targetId,
-                        instanceId = smartspaceMediaData.instanceId
+                if (mediaFlags.isPersistentSsCardEnabled()) {
+                    // Smartspace uses this signal to hide the card (e.g. when it expires or user
+                    // disconnects headphones), so treat as setting inactive when flag is on
+                    smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+                    notifySmartspaceMediaDataLoaded(
+                        smartspaceMediaData.targetId,
+                        smartspaceMediaData,
                     )
-                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
+                } else {
+                    smartspaceMediaData =
+                        EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                            targetId = smartspaceMediaData.targetId,
+                            instanceId = smartspaceMediaData.instanceId,
+                        )
+                    notifySmartspaceMediaDataRemoved(
+                        smartspaceMediaData.targetId,
+                        immediately = false,
+                    )
+                }
             }
             1 -> {
                 val newMediaTarget = mediaTargets.get(0)
@@ -1255,7 +1320,7 @@
                     return
                 }
                 if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
-                smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
+                smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
                 notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
             }
             else -> {
@@ -1264,7 +1329,7 @@
                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
                 notifySmartspaceMediaDataRemoved(
                     smartspaceMediaData.targetId,
-                    false /* immediately */
+                    immediately = false,
                 )
                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
             }
@@ -1273,45 +1338,136 @@
 
     fun onNotificationRemoved(key: String) {
         Assert.isMainThread()
-        val removed = mediaEntries.remove(key)
-        if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
-            Log.d(TAG, "Not removing $key because resumable")
-            // Move to resume key (aka package name) if that key doesn't already exist.
-            val resumeAction = getResumeMediaAction(removed.resumeAction!!)
-            val updated =
-                removed.copy(
-                    token = null,
-                    actions = listOf(resumeAction),
-                    semanticActions = MediaButton(playOrPause = resumeAction),
-                    actionsToShowInCompact = listOf(0),
-                    active = false,
-                    resumption = true,
-                    isPlaying = false,
-                    isClearable = true
-                )
-            val pkg = removed.packageName
-            val migrate = mediaEntries.put(pkg, updated) == null
-            // Notify listeners of "new" controls when migrating or removed and update when not
-            if (migrate) {
-                notifyMediaDataLoaded(pkg, key, updated)
-            } else {
-                // Since packageName is used for the key of the resumption controls, it is
-                // possible that another notification has already been reused for the resumption
-                // controls of this package. In this case, rather than renaming this player as
-                // packageName, just remove it and then send a update to the existing resumption
-                // controls.
-                notifyMediaDataRemoved(key)
-                notifyMediaDataLoaded(pkg, pkg, updated)
-            }
-            logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
-            return
-        }
-        if (removed != null) {
+        val removed = mediaEntries.remove(key) ?: return
+        val isEligibleForResume =
+            removed.isLocalSession() ||
+                (mediaFlags.isRemoteResumeAllowed() &&
+                    removed.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
+        if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else if (useMediaResumption && removed.resumeAction != null && isEligibleForResume) {
+            convertToResumePlayer(key, removed)
+        } else if (mediaFlags.isRetainingPlayersEnabled()) {
+            handlePossibleRemoval(key, removed, notificationRemoved = true)
+        } else {
             notifyMediaDataRemoved(key)
             logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
         }
     }
 
+    private fun onSessionDestroyed(key: String) {
+        if (!mediaFlags.isRetainingPlayersEnabled()) return
+
+        if (DEBUG) Log.d(TAG, "session destroyed for $key")
+        val entry = mediaEntries.remove(key) ?: return
+        // Clear token since the session is no longer valid
+        val updated = entry.copy(token = null)
+        handlePossibleRemoval(key, updated)
+    }
+
+    /**
+     * Convert to resume state if the player is no longer valid and active, then notify listeners
+     * that the data was updated. Does not convert to resume state if the player is still valid, or
+     * if it was removed before becoming inactive. (Assumes that [removed] was removed from
+     * [mediaEntries] before this function was called)
+     */
+    private fun handlePossibleRemoval(
+        key: String,
+        removed: MediaData,
+        notificationRemoved: Boolean = false
+    ) {
+        val hasSession = removed.token != null
+        if (hasSession && removed.semanticActions != null) {
+            // The app was using session actions, and the session is still valid: keep player
+            if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
+            mediaEntries.put(key, removed)
+            notifyMediaDataLoaded(key, key, removed)
+        } else if (!notificationRemoved && removed.semanticActions == null) {
+            // The app was using notification actions, and notif wasn't removed yet: keep player
+            if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
+            mediaEntries.put(key, removed)
+            notifyMediaDataLoaded(key, key, removed)
+        } else if (removed.active) {
+            // This player was still active - it didn't last long enough to time out: remove
+            if (DEBUG) Log.d(TAG, "Removing still-active player $key")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else {
+            // Convert to resume
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "Notification ($notificationRemoved) and/or session " +
+                        "($hasSession) gone for inactive player $key"
+                )
+            }
+            convertToResumePlayer(key, removed)
+        }
+    }
+
+    /** Set the given [MediaData] as a resume state player and notify listeners */
+    private fun convertToResumePlayer(key: String, data: MediaData) {
+        if (DEBUG) Log.d(TAG, "Converting $key to resume")
+        // Resumption controls must have a title.
+        if (data.song.isNullOrBlank()) {
+            Log.e(TAG, "Description incomplete")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+            return
+        }
+        // Move to resume key (aka package name) if that key doesn't already exist.
+        val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
+        val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
+        val launcherIntent =
+            context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
+                PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+            }
+        val updated =
+            data.copy(
+                token = null,
+                actions = actions,
+                semanticActions = MediaButton(playOrPause = resumeAction),
+                actionsToShowInCompact = listOf(0),
+                active = false,
+                resumption = true,
+                isPlaying = false,
+                isClearable = true,
+                clickIntent = launcherIntent,
+            )
+        val pkg = data.packageName
+        val migrate = mediaEntries.put(pkg, updated) == null
+        // Notify listeners of "new" controls when migrating or removed and update when not
+        Log.d(TAG, "migrating? $migrate from $key -> $pkg")
+        if (migrate) {
+            notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
+        } else {
+            // Since packageName is used for the key of the resumption controls, it is
+            // possible that another notification has already been reused for the resumption
+            // controls of this package. In this case, rather than renaming this player as
+            // packageName, just remove it and then send a update to the existing resumption
+            // controls.
+            notifyMediaDataRemoved(key)
+            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) {
         if (useMediaResumption == isEnabled) {
             return
@@ -1409,21 +1565,28 @@
     }
 
     /**
-     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
+     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
      *
      * @return An empty SmartspaceMediaData with the valid target Id is returned if the
      * SmartspaceTarget's data is invalid.
      */
-    private fun toSmartspaceMediaData(
-        target: SmartspaceTarget,
-        isActive: Boolean
-    ): SmartspaceMediaData {
+    private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
         var dismissIntent: Intent? = null
         if (target.baseAction != null && target.baseAction.extras != null) {
             dismissIntent =
                 target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
                     as Intent?
         }
+
+        val isActive =
+            when {
+                !mediaFlags.isPersistentSsCardEnabled() -> true
+                target.baseAction == null -> true
+                else ->
+                    target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) !=
+                        EXTRA_VALUE_TRIGGER_PERIODIC
+            }
+
         packageName(target)?.let {
             return SmartspaceMediaData(
                 targetId = target.smartspaceTargetId,
@@ -1433,7 +1596,8 @@
                 recommendations = target.iconGrid,
                 dismissIntent = dismissIntent,
                 headphoneConnectionTimeMillis = target.creationTimeMillis,
-                instanceId = logger.getNewInstanceId()
+                instanceId = logger.getNewInstanceId(),
+                expiryTimeMs = target.expiryTimeMillis,
             )
         }
         return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
@@ -1441,7 +1605,8 @@
             isActive = isActive,
             dismissIntent = dismissIntent,
             headphoneConnectionTimeMillis = target.creationTimeMillis,
-            instanceId = logger.getNewInstanceId()
+            instanceId = logger.getNewInstanceId(),
+            expiryTimeMs = target.expiryTimeMillis,
         )
     }
 
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 7f5c82f..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
@@ -23,7 +23,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -38,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
@@ -49,10 +51,12 @@
     @Main private val mainExecutor: DelayableExecutor,
     private val logger: MediaTimeoutLogger,
     statusBarStateController: SysuiStatusBarStateController,
-    private val systemClock: SystemClock
+    private val systemClock: SystemClock,
+    private val mediaFlags: MediaFlags,
 ) : MediaDataManager.Listener {
 
     private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
+    private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf()
 
     /**
      * Callback representing that a media object is now expired:
@@ -71,6 +75,12 @@
      */
     lateinit var stateCallback: (String, PlaybackState) -> Unit
 
+    /**
+     * Callback representing that the [MediaSession] for an active control has been destroyed
+     * @param key Media control unique identifier
+     */
+    lateinit var sessionCallback: (String) -> Unit
+
     init {
         statusBarStateController.addCallback(
             object : StatusBarStateController.StateListener {
@@ -87,6 +97,16 @@
                                 listener.doTimeout()
                             }
                         }
+
+                        recommendationListeners.forEach { (key, listener) ->
+                            if (
+                                listener.cancellation != null &&
+                                    listener.expiration <= systemClock.currentTimeMillis()
+                            ) {
+                                logger.logTimeoutCancelled(key, "Timed out while dozing")
+                                listener.doTimeout()
+                            }
+                        }
                     }
                 }
             }
@@ -149,6 +169,30 @@
         mediaListeners.remove(key)?.destroy()
     }
 
+    override fun onSmartspaceMediaDataLoaded(
+        key: String,
+        data: SmartspaceMediaData,
+        shouldPrioritize: Boolean
+    ) {
+        if (!mediaFlags.isPersistentSsCardEnabled()) return
+
+        // First check if we already have a listener
+        recommendationListeners.get(key)?.let {
+            if (!it.destroyed) {
+                it.recommendationData = data
+                return
+            }
+        }
+
+        // Otherwise, create a new one
+        recommendationListeners[key] = RecommendationListener(key, data)
+    }
+
+    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        if (!mediaFlags.isPersistentSsCardEnabled()) return
+        recommendationListeners.remove(key)?.destroy()
+    }
+
     fun isTimedOut(key: String): Boolean {
         return mediaListeners[key]?.timedOut ?: false
     }
@@ -211,6 +255,7 @@
             } else {
                 // For active controls, if the session is destroyed, clean up everything since we
                 // will need to recreate it if this key is updated later
+                sessionCallback.invoke(key)
                 destroy()
             }
         }
@@ -328,4 +373,53 @@
         }
         return true
     }
+
+    /** Listens to changes in recommendation card data and schedules a timeout for its expiration */
+    private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
+        private var timedOut = false
+        var destroyed = false
+        var expiration = Long.MAX_VALUE
+            private set
+        var cancellation: Runnable? = null
+            private set
+
+        var recommendationData: SmartspaceMediaData = data
+            set(value) {
+                destroyed = false
+                field = value
+                processUpdate()
+            }
+
+        init {
+            recommendationData = data
+        }
+
+        fun destroy() {
+            cancellation?.run()
+            cancellation = null
+            destroyed = true
+        }
+
+        private fun processUpdate() {
+            if (recommendationData.expiryTimeMs != expiration) {
+                // The expiry time changed - cancel and reschedule
+                val timeout =
+                    recommendationData.expiryTimeMs -
+                        recommendationData.headphoneConnectionTimeMillis
+                logger.logRecommendationTimeoutScheduled(key, timeout)
+                cancellation?.run()
+                cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
+                expiration = recommendationData.expiryTimeMs
+            }
+        }
+
+        fun doTimeout() {
+            cancellation?.run()
+            cancellation = null
+            logger.logTimeout(key)
+            timedOut = true
+            expiration = Long.MAX_VALUE
+            timeoutCallback(key, timedOut)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
index 8f3f054..f731dc0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
@@ -107,6 +107,17 @@
                 str1 = key
                 str2 = reason
             },
-            { "media timeout cancelled for $str1, reason: $str2" }
+            { "timeout cancelled for $str1, reason: $str2" }
+        )
+
+    fun logRecommendationTimeoutScheduled(key: String, timeout: Long) =
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = key
+                long1 = timeout
+            },
+            { "recommendation timeout scheduled for $str1 in $long1 ms" }
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index 2d10b82..2af21c4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.Utils
@@ -63,7 +64,8 @@
     private val tunerService: TunerService,
     private val mediaBrowserFactory: ResumeMediaBrowserFactory,
     dumpManager: DumpManager,
-    private val systemClock: SystemClock
+    private val systemClock: SystemClock,
+    private val mediaFlags: MediaFlags,
 ) : MediaDataManager.Listener, Dumpable {
 
     private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
@@ -231,7 +233,11 @@
                 mediaBrowser = null
             }
             // If we don't have a resume action, check if we haven't already
-            if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
+            val isEligibleForResume =
+                data.isLocalSession() ||
+                    (mediaFlags.isRemoteResumeAllowed() &&
+                        data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
+            if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) {
                 // TODO also check for a media button receiver intended for restarting (b/154127084)
                 Log.d(TAG, "Checking for service component for " + data.packageName)
                 val pm = context.packageManager
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
index 93be6a7..4827a16 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
@@ -162,8 +162,8 @@
                     context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
                         UI_MODE_NIGHT_YES
                 )
-                    colorScheme.accent1[2]
-                else colorScheme.accent1[3]
+                    colorScheme.accent1.s100
+                else colorScheme.accent1.s200
             },
             { seamlessColor: Int ->
                 val accentColorList = ColorStateList.valueOf(seamlessColor)
@@ -230,7 +230,14 @@
 
     fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
         var anyChanged = false
-        colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged }
+        colorTransitions.forEach {
+            val isChanged = it.updateColorScheme(colorScheme)
+
+            // Ignore changes to colorSeamless, since that is expected when toggling dark mode
+            if (it == colorSeamless) return@forEach
+
+            anyChanged = isChanged || anyChanged
+        }
         colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
         return anyChanged
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index 899148b..8f1c904 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -130,7 +130,12 @@
     private var splitShadeContainer: ViewGroup? = null
 
     /** Track the media player setting status on lock screen. */
-    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private var allowMediaPlayerOnLockScreen: Boolean =
+        secureSettings.getBoolForUser(
+            Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+            true,
+            UserHandle.USER_CURRENT
+        )
     private val lockScreenMediaPlayerUri =
         secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
 
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 1fdbc99..6cf051a 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,19 +30,28 @@
 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.Background
 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
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.media.controls.util.SmallHash
 import com.android.systemui.plugins.ActivityStarter
@@ -60,8 +69,13 @@
 import com.android.systemui.util.traceSection
 import java.io.PrintWriter
 import java.util.TreeMap
+import java.util.concurrent.Executor
 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)
@@ -81,20 +95,24 @@
     private val mediaHostStatesManager: MediaHostStatesManager,
     private val activityStarter: ActivityStarter,
     private val systemClock: SystemClock,
-    @Main executor: DelayableExecutor,
+    @Main private val mainExecutor: DelayableExecutor,
+    @Background private val backgroundExecutor: Executor,
     private val mediaManager: MediaDataManager,
     configurationController: ConfigurationController,
     falsingCollector: FalsingCollector,
     falsingManager: FalsingManager,
     dumpManager: DumpManager,
     private val logger: MediaUiEventLogger,
-    private val debugLogger: MediaCarouselControllerLogger
+    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
 
     /** The current height of the carousel */
-    private var currentCarouselHeight: Int = 0
+    @VisibleForTesting var currentCarouselHeight: Int = 0
 
     /** Are we currently showing only active players */
     private var currentlyShowingOnlyActive: Boolean = false
@@ -128,14 +146,14 @@
     /** The measured height of the carousel */
     private var carouselMeasureHeight: Int = 0
     private var desiredHostState: MediaHostState? = null
-    private val mediaCarousel: MediaScrollView
+    @VisibleForTesting var mediaCarousel: MediaScrollView
     val mediaCarouselScrollHandler: MediaCarouselScrollHandler
     val mediaFrame: ViewGroup
     @VisibleForTesting
     lateinit var settingsButton: View
         private set
     private val mediaContent: ViewGroup
-    @VisibleForTesting val pageIndicator: PageIndicator
+    @VisibleForTesting var pageIndicator: PageIndicator
     private val visualStabilityCallback: OnReorderingAllowedListener
     private var needsReordering: Boolean = false
     private var keysNeedRemoval = mutableSetOf<String>()
@@ -149,41 +167,37 @@
                 mediaCarouselScrollHandler.scrollToStart()
             }
         }
-    private var currentlyExpanded = true
+
+    @VisibleForTesting
+    var currentlyExpanded = true
         set(value) {
             if (field != value) {
                 field = value
-                for (player in MediaPlayerData.players()) {
-                    player.setListening(field)
-                }
+                updateSeekbarListening(mediaCarouselScrollHandler.visibleToUser)
             }
         }
 
     companion object {
-        const val ANIMATION_BASE_DURATION = 2200f
-        const val DURATION = 167f
-        const val DETAILS_DELAY = 1067f
-        const val CONTROLS_DELAY = 1400f
-        const val PAGINATION_DELAY = 1900f
-        const val MEDIATITLES_DELAY = 1000f
-        const val MEDIACONTAINERS_DELAY = 967f
         val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F)
-        val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F)
 
-        fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
-            val transformStartFraction = delay / ANIMATION_BASE_DURATION
-            val transformDurationFraction = duration / ANIMATION_BASE_DURATION
-            val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
-            return MathUtils.constrain(
-                (squishinessToTime - transformStartFraction) / transformDurationFraction,
-                0F,
-                1F
-            )
+        fun calculateAlpha(
+            squishinessFraction: Float,
+            startPosition: Float,
+            endPosition: Float
+        ): Float {
+            val transformFraction =
+                MathUtils.constrain(
+                    (squishinessFraction - startPosition) / (endPosition - startPosition),
+                    0F,
+                    1F
+                )
+            return TRANSFORM_BEZIER.getInterpolation(transformFraction)
         }
     }
 
     private val configListener =
         object : ConfigurationController.ConfigurationListener {
+            var lastOrientation = -1
 
             override fun onDensityOrFontScaleChanged() {
                 // System font changes should only happen when UMO is offscreen or a flicker may
@@ -200,7 +214,13 @@
             override fun onConfigChanged(newConfig: Configuration?) {
                 if (newConfig == null) return
                 isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
-                updatePlayers(recreateMedia = true)
+                val newOrientation = newConfig.orientation
+                if (lastOrientation != newOrientation) {
+                    // The players actually depend on the orientation possibly, so we have to
+                    // recreate them (at least on large screen devices)
+                    lastOrientation = newOrientation
+                    updatePlayers(recreateMedia = true)
+                }
             }
 
             override fun onUiModeChanged() {
@@ -209,6 +229,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.
@@ -228,9 +259,10 @@
             MediaCarouselScrollHandler(
                 mediaCarousel,
                 pageIndicator,
-                executor,
+                mainExecutor,
                 this::onSwipeToDismiss,
                 this::updatePageIndicatorLocation,
+                this::updateSeekbarListening,
                 this::closeGuts,
                 falsingCollector,
                 falsingManager,
@@ -364,7 +396,7 @@
                     data: SmartspaceMediaData,
                     shouldPrioritize: Boolean
                 ) {
-                    debugLogger.logRecommendationLoaded(key)
+                    debugLogger.logRecommendationLoaded(key, data.isActive)
                     // Log the case where the hidden media carousel with the existed inactive resume
                     // media is shown by the Smartspace signal.
                     if (data.isActive) {
@@ -438,7 +470,12 @@
                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
                         }
                     } else {
-                        onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+                        if (!mediaFlags.isPersistentSsCardEnabled()) {
+                            // Handle update to inactive as a removal
+                            onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+                        } else {
+                            addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
+                        }
                     }
                 }
 
@@ -478,6 +515,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() {
@@ -507,6 +551,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
@@ -533,6 +594,17 @@
                     ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
             }
         }
+        // Check postcondition: mediaContent should have the same number of children as there
+        // are
+        // elements in mediaPlayers.
+        if (MediaPlayerData.players().size != mediaContent.childCount) {
+            Log.e(
+                TAG,
+                "Size of players list and number of views in carousel are out of sync. " +
+                    "Players size is ${MediaPlayerData.players().size}. " +
+                    "View count is ${mediaContent.childCount}."
+            )
+        }
     }
 
     // Returns true if new player is added
@@ -549,37 +621,7 @@
                 MediaPlayerData.visiblePlayerKeys()
                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
             if (existingPlayer == null) {
-                val newPlayer = mediaControlPanelFactory.get()
-                newPlayer.attachPlayer(
-                    MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
-                )
-                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-                val lp =
-                    LinearLayout.LayoutParams(
-                        ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT
-                    )
-                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
-                newPlayer.bindPlayer(data, key)
-                newPlayer.setListening(currentlyExpanded)
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    newPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger
-                )
-                updatePlayerToState(newPlayer, noAnimation = true)
-                // Media data added from a recommendation card should starts playing.
-                if (
-                    (shouldScrollToKey && data.isPlaying == true) ||
-                        (!shouldScrollToKey && data.active)
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
-                } else {
-                    needsReordering = true
-                }
+                setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey)
             } else {
                 existingPlayer.bindPlayer(data, key)
                 MediaPlayerData.addMediaPlayer(
@@ -604,24 +646,65 @@
                 } else {
                     needsReordering = true
                 }
-            }
-            updatePageIndicator()
-            mediaCarouselScrollHandler.onPlayersChanged()
-            mediaFrame.requiresRemeasuring = true
-            // Check postcondition: mediaContent should have the same number of children as there
-            // are
-            // elements in mediaPlayers.
-            if (MediaPlayerData.players().size != mediaContent.childCount) {
-                Log.e(
-                    TAG,
-                    "Size of players list and number of views in carousel are out of sync. " +
-                        "Players size is ${MediaPlayerData.players().size}. " +
-                        "View count is ${mediaContent.childCount}."
-                )
+                updatePageIndicator()
+                mediaCarouselScrollHandler.onPlayersChanged()
+                mediaFrame.requiresRemeasuring = true
             }
             return existingPlayer == null
         }
 
+    private fun setupNewPlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+    ) {
+        backgroundExecutor.execute {
+            val mediaViewHolder = createMediaViewHolderInBg()
+            // Add the new player in the main thread.
+            mainExecutor.execute {
+                val newPlayer = mediaControlPanelFactory.get()
+                newPlayer.attachPlayer(mediaViewHolder)
+                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
+                val lp =
+                    LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                    )
+                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
+                newPlayer.bindPlayer(data, key)
+                newPlayer.setListening(
+                    mediaCarouselScrollHandler.visibleToUser && currentlyExpanded
+                )
+                MediaPlayerData.addMediaPlayer(
+                    key,
+                    data,
+                    newPlayer,
+                    systemClock,
+                    isSsReactivated,
+                    debugLogger
+                )
+                updatePlayerToState(newPlayer, noAnimation = true)
+                // Media data added from a recommendation card should starts playing.
+                if (
+                    (shouldScrollToKey && data.isPlaying == true) ||
+                        (!shouldScrollToKey && data.active)
+                ) {
+                    reorderAllPlayers(curVisibleMediaKey, key)
+                } else {
+                    needsReordering = true
+                }
+                updatePageIndicator()
+                mediaCarouselScrollHandler.onPlayersChanged()
+                mediaFrame.requiresRemeasuring = true
+            }
+        }
+    }
+
+    private fun createMediaViewHolderInBg(): MediaViewHolder {
+        return MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+    }
+
     private fun addSmartspaceMediaRecommendations(
         key: String,
         data: SmartspaceMediaData,
@@ -629,7 +712,19 @@
     ) =
         traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
             if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
-            if (MediaPlayerData.getMediaPlayer(key) != null) {
+            MediaPlayerData.getMediaPlayer(key)?.let {
+                if (mediaFlags.isPersistentSsCardEnabled()) {
+                    // The card exists, but could have changed active state, so update for sorting
+                    MediaPlayerData.addMediaRecommendation(
+                        key,
+                        data,
+                        it,
+                        shouldPrioritize,
+                        systemClock,
+                        debugLogger,
+                        update = true,
+                    )
+                }
                 Log.w(TAG, "Skip adding smartspace target in carousel")
                 return
             }
@@ -642,11 +737,14 @@
                     debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey)
                 }
             }
-
             val newRecs = mediaControlPanelFactory.get()
-            newRecs.attachRecommendation(
-                RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
-            )
+            val recommendationViewHolder =
+                RecommendationViewHolder.create(
+                    LayoutInflater.from(context),
+                    mediaContent,
+                    mediaFlags.isRecommendationCardUpdateEnabled()
+                )
+            newRecs.attachRecommendation(recommendationViewHolder)
             newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
             val lp =
                 LinearLayout.LayoutParams(
@@ -664,23 +762,12 @@
                 newRecs,
                 shouldPrioritize,
                 systemClock,
-                debugLogger
+                debugLogger,
             )
             updatePlayerToState(newRecs, noAnimation = true)
             reorderAllPlayers(curVisibleMediaKey)
             updatePageIndicator()
             mediaFrame.requiresRemeasuring = true
-            // Check postcondition: mediaContent should have the same number of children as there
-            // are
-            // elements in mediaPlayers.
-            if (MediaPlayerData.players().size != mediaContent.childCount) {
-                Log.e(
-                    TAG,
-                    "Size of players list and number of views in carousel are out of sync. " +
-                        "Players size is ${MediaPlayerData.players().size}. " +
-                        "View count is ${mediaContent.childCount}."
-                )
-            }
         }
 
     fun removePlayer(
@@ -717,6 +804,9 @@
     private fun updatePlayers(recreateMedia: Boolean) {
         pageIndicator.tintList =
             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+        val previousVisibleKey =
+            MediaPlayerData.visiblePlayerKeys()
+                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
 
         MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
             if (isSsMediaRec) {
@@ -741,6 +831,9 @@
                     isSsReactivated = isSsReactivated
                 )
             }
+            if (recreateMedia) {
+                reorderAllPlayers(previousVisibleKey)
+            }
         }
     }
 
@@ -800,7 +893,12 @@
         val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
         val endAlpha =
             (if (endIsVisible) 1.0f else 0.0f) *
-                calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
+                calculateAlpha(
+                    squishFraction,
+                    (pageIndicator.translationY + pageIndicator.height) /
+                        mediaCarousel.measuredHeight,
+                    1F
+                )
         var alpha = 1.0f
         if (!endIsVisible || !startIsVisible) {
             var progress = currentTransitionProgress
@@ -826,7 +924,15 @@
         pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
         val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
         pageIndicator.translationY =
-            (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat()
+            (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin)
+                .toFloat()
+    }
+
+    /** Update listening to seekbar. */
+    private fun updateSeekbarListening(visibleToUser: Boolean) {
+        for (player in MediaPlayerData.players()) {
+            player.setListening(visibleToUser && currentlyExpanded)
+        }
     }
 
     /** Update the dimension of this carousel. */
@@ -1205,17 +1311,18 @@
         player: MediaControlPanel,
         shouldPrioritize: Boolean,
         clock: SystemClock,
-        debugLogger: MediaCarouselControllerLogger? = null
+        debugLogger: MediaCarouselControllerLogger? = null,
+        update: Boolean = false
     ) {
         shouldPrioritizeSs = shouldPrioritize
         val removedPlayer = removeMediaPlayer(key)
-        if (removedPlayer != null && removedPlayer != player) {
+        if (!update && removedPlayer != null && removedPlayer != player) {
             debugLogger?.logPotentialMemoryLeak(key)
         }
         val sortKey =
             MediaSortKey(
                 isSsMediaRec = true,
-                EMPTY.copy(isPlaying = false),
+                EMPTY.copy(active = data.isActive, isPlaying = false),
                 key,
                 clock.currentTimeMillis(),
                 isSsReactivated = true
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
index eed1bd7..35bda15 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
@@ -48,8 +48,16 @@
     fun logMediaRemoved(key: String) =
         buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
 
-    fun logRecommendationLoaded(key: String) =
-        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
+    fun logRecommendationLoaded(key: String, isActive: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = isActive
+            },
+            { "add recommendation $str1, active $bool1" }
+        )
 
     fun logRecommendationRemoved(key: String, immediately: Boolean) =
         buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
index 36b2eda..1ace316 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
@@ -57,6 +57,7 @@
     private val mainExecutor: DelayableExecutor,
     val dismissCallback: () -> Unit,
     private var translationChangedListener: () -> Unit,
+    private var seekBarUpdateListener: (visibleToUser: Boolean) -> Unit,
     private val closeGuts: (immediate: Boolean) -> Unit,
     private val falsingCollector: FalsingCollector,
     private val falsingManager: FalsingManager,
@@ -177,6 +178,12 @@
 
     /** Whether the media card is visible to user if any */
     var visibleToUser: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                seekBarUpdateListener.invoke(field)
+            }
+        }
 
     /** Whether the quick setting is expanded or not */
     var qsExpanded: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
index 82abf9b..2a8362b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
@@ -19,28 +19,28 @@
 import com.android.systemui.monet.ColorScheme
 
 /** Returns the surface color for media controls based on the scheme. */
-internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2[9] // A2-800
+internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2.s800 // A2-800
 
 /** Returns the primary accent color for media controls based on the scheme. */
-internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1[2] // A1-100
+internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1.s100 // A1-100
 
 /** Returns the secondary accent color for media controls based on the scheme. */
-internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1[3] // A1-200
+internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1.s200 // A1-200
 
 /** Returns the primary text color for media controls based on the scheme. */
-internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1[1] // N1-50
+internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1.s50 // N1-50
 
 /** Returns the inverse of the primary text color for media controls based on the scheme. */
-internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1[10] // N1-900
+internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1.s900 // N1-900
 
 /** Returns the secondary text color for media controls based on the scheme. */
-internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2[3] // N2-200
+internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s200 // N2-200
 
 /** Returns the tertiary text color for media controls based on the scheme. */
-internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2[5] // N2-400
+internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s400 // N2-400
 
 /** Returns the color for the start of the background gradient based on the scheme. */
-internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2[8] // A2-700
+internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2.s700 // A2-700
 
 /** Returns the color for the end of the background gradient based on the scheme. */
-internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1[8] // A1-700
+internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1.s700 // A1-700
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 15c3443..4ab93da 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
@@ -50,24 +50,26 @@
 import android.os.Trace;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.Pair;
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Interpolator;
 import android.widget.ImageButton;
 import android.widget.ImageView;
+import android.widget.SeekBar;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
+import androidx.appcompat.content.res.AppCompatResources;
 import androidx.constraintlayout.widget.ConstraintSet;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.InstanceId;
+import com.android.internal.widget.CachingIconView;
 import com.android.settingslib.widget.AdaptiveIcon;
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.R;
@@ -111,6 +113,7 @@
 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController;
 import com.android.systemui.util.ColorUtilKt;
 import com.android.systemui.util.animation.TransitionLayout;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.time.SystemClock;
 
 import java.net.URISyntaxException;
@@ -121,6 +124,7 @@
 import javax.inject.Inject;
 
 import dagger.Lazy;
+import kotlin.Triple;
 import kotlin.Unit;
 
 /**
@@ -166,10 +170,13 @@
             R.id.action1
     );
 
+    // Time in millis for playing turbulence noise that is played after a touch ripple.
+    @VisibleForTesting static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
+
     private final SeekBarViewModel mSeekBarViewModel;
     private SeekBarObserver mSeekBarObserver;
     protected final Executor mBackgroundExecutor;
-    private final Executor mMainExecutor;
+    private final DelayableExecutor mMainExecutor;
     private final ActivityStarter mActivityStarter;
     private final BroadcastSender mBroadcastSender;
 
@@ -219,13 +226,13 @@
     private final BroadcastDialogController mBroadcastDialogController;
     private boolean mIsCurrentBroadcastedApp = false;
     private boolean mShowBroadcastDialogButton = false;
-    private String mSwitchBroadcastApp;
+    private String mCurrentBroadcastApp;
     private MultiRippleController mMultiRippleController;
     private TurbulenceNoiseController mTurbulenceNoiseController;
-    private FeatureFlags mFeatureFlags;
-    private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null;
+    private final FeatureFlags mFeatureFlags;
+    private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig;
     @VisibleForTesting
-    MultiRippleController.Companion.RipplesFinishedListener mRipplesFinishedListener = null;
+    MultiRippleController.Companion.RipplesFinishedListener mRipplesFinishedListener;
 
     /**
      * Initialize a new control panel
@@ -239,7 +246,7 @@
     public MediaControlPanel(
             Context context,
             @Background Executor backgroundExecutor,
-            @Main Executor mainExecutor,
+            @Main DelayableExecutor mainExecutor,
             ActivityStarter activityStarter,
             BroadcastSender broadcastSender,
             MediaViewController mediaViewController,
@@ -339,6 +346,11 @@
         mSeekBarViewModel.setListening(listening);
     }
 
+    @VisibleForTesting
+    public boolean getListening() {
+        return mSeekBarViewModel.getListening();
+    }
+
     /** Sets whether the user is touching the seek bar to change the track position. */
     private void setIsScrubbing(boolean isScrubbing) {
         if (mMediaData == null || mMediaData.getSemanticActions() == null) {
@@ -398,10 +410,11 @@
 
         TextView titleText = mMediaViewHolder.getTitleText();
         TextView artistText = mMediaViewHolder.getArtistText();
+        CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator();
         AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
-                Interpolators.EMPHASIZED_DECELERATE, titleText, artistText);
+                Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator);
         AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
-                Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
+                Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator);
 
         MultiRippleView multiRippleView = vh.getMultiRippleView();
         mMultiRippleController = new MultiRippleController(multiRippleView);
@@ -409,10 +422,12 @@
         if (mFeatureFlags.isEnabled(Flags.UMO_TURBULENCE_NOISE)) {
             mRipplesFinishedListener = () -> {
                 if (mTurbulenceNoiseAnimationConfig == null) {
-                    mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation();
+                    mTurbulenceNoiseAnimationConfig = createTurbulenceNoiseAnimation();
                 }
                 // Color will be correctly updated in ColorSchemeTransition.
                 mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig);
+                mMainExecutor.executeDelayed(
+                        mTurbulenceNoiseController::finish, TURBULENCE_NOISE_PLAY_DURATION);
             };
             mMultiRippleController.addRipplesFinishedListener(mRipplesFinishedListener);
         }
@@ -513,8 +528,13 @@
         }
 
         // Seek Bar
-        final MediaController controller = getController();
-        mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+        if (data.getResumption() && data.getResumeProgress() != null) {
+            double progress = data.getResumeProgress();
+            mSeekBarViewModel.updateStaticProgress(progress);
+        } else {
+            final MediaController controller = getController();
+            mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+        }
 
         // Show the broadcast dialog button only when the le audio is enabled.
         mShowBroadcastDialogButton =
@@ -552,9 +572,8 @@
             // TODO(b/233698402): Use the package name instead of app label to avoid the
             // unexpected result.
             mIsCurrentBroadcastedApp = device != null
-                    && TextUtils.equals(device.getName(),
-                    MediaDataUtils.getAppLabel(mContext, mPackageName, mContext.getString(
-                            R.string.bt_le_audio_broadcast_dialog_unknown_name)));
+                && TextUtils.equals(device.getName(),
+                    mContext.getString(R.string.broadcasting_description_is_broadcasting));
             useDisabledAlpha = !mIsCurrentBroadcastedApp;
             // Always be enabled if the broadcast button is shown
             isTapEnabled = true;
@@ -609,8 +628,8 @@
                         // media output dialog.
                         if (!mIsCurrentBroadcastedApp) {
                             mLogger.logOpenBroadcastDialog(mUid, mPackageName, mInstanceId);
-                            mSwitchBroadcastApp = device.getName().toString();
-                            mBroadcastDialogController.createBroadcastDialog(mSwitchBroadcastApp,
+                            mCurrentBroadcastApp = device.getName().toString();
+                            mBroadcastDialogController.createBroadcastDialog(mCurrentBroadcastApp,
                                     mPackageName, true, mMediaViewHolder.getSeamlessButton());
                         } else {
                             mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
@@ -664,11 +683,15 @@
     private boolean bindSongMetadata(MediaData data) {
         TextView titleText = mMediaViewHolder.getTitleText();
         TextView artistText = mMediaViewHolder.getArtistText();
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
         return mMetadataAnimationHandler.setNext(
-            Pair.create(data.getSong(), data.getArtist()),
+            new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
             () -> {
                 titleText.setText(data.getSong());
                 artistText.setText(data.getArtist());
+                setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit());
+                setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit());
 
                 // refreshState is required here to resize the text views (and prevent ellipsis)
                 mMediaViewController.refreshState();
@@ -715,9 +738,14 @@
             contentDescription =
                     mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
         } else if (data != null) {
-            contentDescription = mContext.getString(
-                    R.string.controls_media_smartspace_rec_description,
-                    data.getAppName(mContext));
+            if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+                contentDescription = mContext.getString(
+                        R.string.controls_media_smartspace_rec_header);
+            } else {
+                contentDescription = mContext.getString(
+                        R.string.controls_media_smartspace_rec_description,
+                        data.getAppName(mContext));
+            }
         } else {
             contentDescription = null;
         }
@@ -739,43 +767,16 @@
         int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
         int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
 
-        // WallpaperColors.fromBitmap takes a good amount of time. We do that work
-        // on the background executor to avoid stalling animations on the UI Thread.
         mBackgroundExecutor.execute(() -> {
             // Album art
             ColorScheme mutableColorScheme = null;
             Drawable artwork;
             boolean isArtworkBound;
             Icon artworkIcon = data.getArtwork();
-            WallpaperColors wallpaperColors = null;
-            if (artworkIcon != null) {
-                if (artworkIcon.getType() == Icon.TYPE_BITMAP
-                        || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
-                    // Avoids extra processing if this is already a valid bitmap
-                    wallpaperColors = WallpaperColors
-                            .fromBitmap(artworkIcon.getBitmap());
-                } else {
-                    Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
-                    if (artworkDrawable != null) {
-                        wallpaperColors = WallpaperColors
-                                .fromDrawable(artworkIcon.loadDrawable(mContext));
-                    }
-                }
-            }
+            WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
             if (wallpaperColors != null) {
                 mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
-                Drawable albumArt = getScaledBackground(artworkIcon, width, height);
-                GradientDrawable gradient = (GradientDrawable) mContext
-                        .getDrawable(R.drawable.qs_media_scrim);
-                gradient.setColors(new int[] {
-                        ColorUtilKt.getColorWithAlpha(
-                                MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
-                                0.25f),
-                        ColorUtilKt.getColorWithAlpha(
-                                MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
-                                0.9f),
-                });
-                artwork = new LayerDrawable(new Drawable[] { albumArt, gradient });
+                artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
                 isArtworkBound = true;
             } else {
                 // If there's no artwork, use colors from the app icon
@@ -854,6 +855,96 @@
         });
     }
 
+    private void bindRecommendationArtwork(
+            SmartspaceAction recommendation,
+            String packageName,
+            int itemIndex
+    ) {
+        final int traceCookie = recommendation.hashCode();
+        final String traceName =
+                "MediaControlPanel#bindRecommendationArtwork<" + packageName + ">";
+        Trace.beginAsyncSection(traceName, traceCookie);
+
+        // Capture width & height from views in foreground for artwork scaling in background
+        int width = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredWidth();
+        int height = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredHeight();
+
+        mBackgroundExecutor.execute(() -> {
+            // Album art
+            ColorScheme mutableColorScheme = null;
+            Drawable artwork;
+            Icon artworkIcon = recommendation.getIcon();
+            WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
+            if (wallpaperColors != null) {
+                mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
+                artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
+            } else {
+                artwork = new ColorDrawable(Color.TRANSPARENT);
+            }
+
+            mMainExecutor.execute(() -> {
+                // Bind the artwork drawable to media cover.
+                ImageView mediaCover =
+                        mRecommendationViewHolder.getMediaCoverItems().get(itemIndex);
+                mediaCover.setImageDrawable(artwork);
+
+                // Set up the app icon.
+                ImageView appIconView = mRecommendationViewHolder.getMediaAppIcons().get(itemIndex);
+                appIconView.clearColorFilter();
+                try {
+                    Drawable icon = mContext.getPackageManager()
+                            .getApplicationIcon(packageName);
+                    appIconView.setImageDrawable(icon);
+                } catch (PackageManager.NameNotFoundException e) {
+                    Log.w(TAG, "Cannot find icon for package " + packageName, e);
+                    appIconView.setImageResource(R.drawable.ic_music_note);
+                }
+                Trace.endAsyncSection(traceName, traceCookie);
+            });
+        });
+    }
+
+    // This method should be called from a background thread. WallpaperColors.fromBitmap takes a
+    // good amount of time. We do that work on the background executor to avoid stalling animations
+    // on the UI Thread.
+    private WallpaperColors getWallpaperColor(Icon artworkIcon) {
+        if (artworkIcon != null) {
+            if (artworkIcon.getType() == Icon.TYPE_BITMAP
+                    || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
+                // Avoids extra processing if this is already a valid bitmap
+                return WallpaperColors
+                        .fromBitmap(artworkIcon.getBitmap());
+            } else {
+                Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
+                if (artworkDrawable != null) {
+                    return WallpaperColors
+                            .fromDrawable(artworkIcon.loadDrawable(mContext));
+                }
+            }
+        }
+        return null;
+    }
+
+    private LayerDrawable addGradientToIcon(
+            Icon artworkIcon,
+            ColorScheme mutableColorScheme,
+            int width,
+            int height
+    ) {
+        Drawable albumArt = getScaledBackground(artworkIcon, width, height);
+        GradientDrawable gradient = (GradientDrawable) AppCompatResources
+                .getDrawable(mContext, R.drawable.qs_media_scrim);
+        gradient.setColors(new int[] {
+                ColorUtilKt.getColorWithAlpha(
+                        MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
+                        0.25f),
+                ColorUtilKt.getColorWithAlpha(
+                        MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
+                        0.9f),
+        });
+        return new LayerDrawable(new Drawable[] { albumArt, gradient });
+    }
+
     private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
             int targetWidth, int targetHeight) {
         Drawable drawable = transitionDrawable.getDrawable(layer);
@@ -1049,14 +1140,16 @@
                         /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
                         mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
                         /* opacity= */ 100,
-                        /* shouldFillRipple= */ false,
                         /* sparkleStrength= */ 0f,
+                        /* baseRingFadeParams= */ null,
+                        /* sparkleRingFadeParams= */ null,
+                        /* centerFillFadeParams= */ null,
                         /* shouldDistort= */ false
                 )
         );
     }
 
-    private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() {
+    private TurbulenceNoiseAnimationConfig createTurbulenceNoiseAnimation() {
         return new TurbulenceNoiseAnimationConfig(
                 TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT,
                 TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
@@ -1071,7 +1164,9 @@
                 /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(),
                 /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(),
                 TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
+                /* easeInDuration= */
                 TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS,
+                /* easeOutDuration= */
                 TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS,
                 this.getContext().getResources().getDisplayMetrics().density,
                 BlendMode.PLUS,
@@ -1209,8 +1304,10 @@
         PackageManager packageManager = mContext.getPackageManager();
         // Set up media source app's logo.
         Drawable icon = packageManager.getApplicationIcon(applicationInfo);
-        ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
-        headerLogoImageView.setImageDrawable(icon);
+        if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+            ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
+            headerLogoImageView.setImageDrawable(icon);
+        }
         fetchAndUpdateRecommendationColors(icon);
 
         // Set up media rec card's tap action if applicable.
@@ -1230,7 +1327,15 @@
 
             // Set up media item cover.
             ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
-            mediaCoverImageView.setImageIcon(recommendation.getIcon());
+            if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+                bindRecommendationArtwork(
+                        recommendation,
+                        data.getPackageName(),
+                        itemIndex
+                );
+            } else {
+                mediaCoverImageView.setImageIcon(recommendation.getIcon());
+            }
 
             // Set up the media item's click listener if applicable.
             ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
@@ -1260,7 +1365,6 @@
                                 recommendation.getTitle(), artistName, appName));
             }
 
-
             // Set up title
             CharSequence title = recommendation.getTitle();
             hasTitle |= !TextUtils.isEmpty(title);
@@ -1274,6 +1378,24 @@
             hasSubtitle |= !TextUtils.isEmpty(subtitle);
             TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
             subtitleView.setText(subtitle);
+
+            // Set up progress bar
+            if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+                SeekBar mediaProgressBar =
+                        mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
+                TextView mediaSubtitle =
+                        mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
+                // show progress bar if the recommended album is played.
+                Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
+                if (progress == null || progress <= 0.0) {
+                    mediaProgressBar.setVisibility(View.GONE);
+                    mediaSubtitle.setVisibility(View.VISIBLE);
+                } else {
+                    mediaProgressBar.setProgress((int) (progress * 100));
+                    mediaProgressBar.setVisibility(View.VISIBLE);
+                    mediaSubtitle.setVisibility(View.GONE);
+                }
+            }
         }
         mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
 
@@ -1338,12 +1460,22 @@
         int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
         int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
 
+        if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+            mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
+        }
+
         mRecommendationViewHolder.getRecommendations()
                 .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
         mRecommendationViewHolder.getMediaTitles().forEach(
                 (title) -> title.setTextColor(textPrimaryColor));
         mRecommendationViewHolder.getMediaSubtitles().forEach(
                 (subtitle) -> subtitle.setTextColor(textSecondaryColor));
+        if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+            mRecommendationViewHolder.getMediaProgressBars().forEach(
+                    (progressBar) -> progressBar.setProgressTintList(
+                            ColorStateList.valueOf(textPrimaryColor))
+            );
+        }
 
         mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index f7a9bc7..45bc425 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -28,7 +28,6 @@
 import android.os.Handler
 import android.os.UserHandle
 import android.provider.Settings
-import android.util.Log
 import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
@@ -41,6 +40,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.dream.MediaDreamComplication
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeStateEvents
@@ -93,6 +93,7 @@
     private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
     private val mediaCarouselController: MediaCarouselController,
+    private val mediaManager: MediaDataManager,
     private val keyguardViewController: KeyguardViewController,
     private val dreamOverlayStateController: DreamOverlayStateController,
     configurationController: ConfigurationController,
@@ -224,9 +225,9 @@
 
     private var inSplitShade = false
 
-    /** Is there any active media in the carousel? */
-    private var hasActiveMedia: Boolean = false
-        get() = mediaHosts.get(LOCATION_QQS)?.visible == true
+    /** Is there any active media or recommendation in the carousel? */
+    private var hasActiveMediaOrRecommendation: Boolean = false
+        get() = mediaManager.hasActiveMediaOrRecommendation()
 
     /** Are we currently waiting on an animation to start? */
     private var animationPending: Boolean = false
@@ -582,12 +583,8 @@
         val viewHost = createUniqueObjectHost()
         mediaObject.hostView = viewHost
         mediaObject.addVisibilityChangeListener {
-            // If QQS changes visibility, we need to force an update to ensure the transition
-            // goes into the correct state
-            val stateUpdate = mediaObject.location == LOCATION_QQS
-
             // Never animate because of a visibility change, only state changes should do that
-            updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
+            updateDesiredLocation(forceNoAnimation = true)
         }
         mediaHosts[mediaObject.location] = mediaObject
         if (mediaObject.location == desiredLocation) {
@@ -908,7 +905,7 @@
     fun isCurrentlyInGuidedTransformation(): Boolean {
         return hasValidStartAndEndLocations() &&
             getTransformationProgress() >= 0 &&
-            areGuidedTransitionHostsVisible()
+            (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation)
     }
 
     private fun hasValidStartAndEndLocations(): Boolean {
@@ -965,7 +962,7 @@
     private fun getQSTransformationProgress(): Float {
         val currentHost = getHost(desiredLocation)
         val previousHost = getHost(previousLocation)
-        if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
+        if (currentHost?.location == LOCATION_QS && !inSplitShade) {
             if (previousHost?.location == LOCATION_QQS) {
                 if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
                     return qsExpansion
@@ -1028,7 +1025,8 @@
     private fun updateHostAttachment() =
         traceSection("MediaHierarchyManager#updateHostAttachment") {
             var newLocation = resolveLocationForFading()
-            var canUseOverlay = !isCurrentlyFading()
+            // Don't use the overlay when fading or when we don't have active media
+            var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation
             if (isCrossFadeAnimatorRunning) {
                 if (
                     getHost(newLocation)?.visible == true &&
@@ -1056,17 +1054,6 @@
                     // This will either do a full layout pass and remeasure, or it will bypass
                     // that and directly set the mediaFrame's bounds within the premeasured host.
                     targetHost.addView(mediaFrame)
-
-                    if (mediaFrame.childCount > 0) {
-                        val child = mediaFrame.getChildAt(0)
-                        if (mediaFrame.height < child.height) {
-                            Log.wtf(
-                                TAG,
-                                "mediaFrame height is too small for child: " +
-                                    "${mediaFrame.height} vs ${child.height}"
-                            )
-                        }
-                    }
                 }
                 if (isCrossFadeAnimatorRunning) {
                     // When cross-fading with an animation, we only notify the media carousel of the
@@ -1122,7 +1109,6 @@
                 dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
                 (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
                 qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
-                !hasActiveMedia -> LOCATION_QS
                 onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
                 onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
                 onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 3224213..b9b0459 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -24,18 +24,16 @@
 import com.android.systemui.media.controls.models.GutsViewHolder
 import com.android.systemui.media.controls.models.player.MediaViewHolder
 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
 import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.MeasurementOutput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionLayoutController
 import com.android.systemui.util.animation.TransitionViewState
 import com.android.systemui.util.traceSection
+import java.lang.Float.max
+import java.lang.Float.min
 import javax.inject.Inject
 
 /**
@@ -48,7 +46,8 @@
     private val context: Context,
     private val configurationController: ConfigurationController,
     private val mediaHostStatesManager: MediaHostStatesManager,
-    private val logger: MediaViewLogger
+    private val logger: MediaViewLogger,
+    private val mediaFlags: MediaFlags,
 ) {
 
     /**
@@ -80,6 +79,7 @@
             setOf(
                 R.id.header_title,
                 R.id.header_artist,
+                R.id.media_explicit_indicator,
                 R.id.actionPlayPause,
             )
 
@@ -304,42 +304,108 @@
         val squishedViewState = viewState.copy()
         val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt()
         squishedViewState.height = squishedHeight
-        controlIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
-            }
-        }
-
-        detailIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
-            }
-        }
-
         // We are not overriding the squishedViewStates height but only the children to avoid
         // them remeasuring the whole view. Instead it just remains as the original size
         backgroundIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.height = squishedHeight
-            }
+            squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight }
         }
 
-        RecommendationViewHolder.mediaContainersIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
-            }
-        }
-
-        RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
-            }
-        }
-
+        // media player
+        calculateWidgetGroupAlphaForSquishiness(
+            controlIds,
+            squishedViewState.measureHeight.toFloat(),
+            squishedViewState,
+            squishFraction
+        )
+        calculateWidgetGroupAlphaForSquishiness(
+            detailIds,
+            squishedViewState.measureHeight.toFloat(),
+            squishedViewState,
+            squishFraction
+        )
+        // recommendation card
+        val titlesTop =
+            calculateWidgetGroupAlphaForSquishiness(
+                RecommendationViewHolder.mediaTitlesAndSubtitlesIds,
+                squishedViewState.measureHeight.toFloat(),
+                squishedViewState,
+                squishFraction
+            )
+        calculateWidgetGroupAlphaForSquishiness(
+            RecommendationViewHolder.mediaContainersIds,
+            titlesTop,
+            squishedViewState,
+            squishFraction
+        )
         return squishedViewState
     }
 
     /**
+     * This function is to make each widget in UMO disappear before being clipped by squished UMO
+     *
+     * The general rule is that widgets in UMO has been divided into several groups, and widgets in
+     * one group have the same alpha during squishing It will change from alpha 0.0 when the visible
+     * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible
+     * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause
+     * button will change alpha together.
+     * ```
+     *     And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls,
+     *     including progress bar, next button, previous button
+     * ```
+     * widgetGroupIds: a group of widgets have same state during UMO is squished,
+     * ```
+     *     e.g. Album title, artist title and play-pause button
+     * ```
+     * groupEndPosition: the height of UMO, when the height reaches this value,
+     * ```
+     *     widgets in this group should have 1.0 as alpha
+     *     e.g., the group of album title, artist title and play-pause button will become fully
+     *         visible when the height of UMO reaches the top of controls group
+     *         (progress bar, previous button and next button)
+     * ```
+     * squishedViewState: hold the widgetState of each widget, which will be modified
+     * squishFraction: the squishFraction of UMO
+     */
+    private fun calculateWidgetGroupAlphaForSquishiness(
+        widgetGroupIds: Set<Int>,
+        groupEndPosition: Float,
+        squishedViewState: TransitionViewState,
+        squishFraction: Float
+    ): Float {
+        val nonsquishedHeight = squishedViewState.measureHeight
+        var groupTop = squishedViewState.measureHeight.toFloat()
+        var groupBottom = 0F
+        widgetGroupIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                groupTop = min(groupTop, state.y)
+                groupBottom = max(groupBottom, state.y + state.height)
+            }
+        }
+        // startPosition means to the height of squished UMO where the widget alpha should start
+        // changing from 0.0
+        // generally, it equals to the bottom of widgets, so that we can meet the requirement that
+        // widget should not go beyond the bounds of background
+        // endPosition means to the height of squished UMO where the widget alpha should finish
+        // changing alpha to 1.0
+        var startPosition = groupBottom
+        val endPosition = groupEndPosition
+        if (startPosition == endPosition) {
+            startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat()
+        }
+        widgetGroupIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha =
+                    calculateAlpha(
+                        squishFraction,
+                        startPosition / nonsquishedHeight,
+                        endPosition / nonsquishedHeight
+                    )
+            }
+        }
+        return groupTop // used for the widget group above this group
+    }
+
+    /**
      * Obtain a new viewState for a given media state. This usually returns a cached state, but if
      * it's not available, it will recreate one by measuring, which may be expensive.
      */
@@ -544,11 +610,13 @@
         overrideSize?.let {
             // To be safe we're using a maximum here. The override size should always be set
             // properly though.
-            if (result.measureHeight != it.measuredHeight
-                    || result.measureWidth != it.measuredWidth) {
+            if (
+                result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth
+            ) {
                 result.measureHeight = Math.max(it.measuredHeight, result.measureHeight)
                 result.measureWidth = Math.max(it.measuredWidth, result.measureWidth)
-                // The measureHeight and the shown height should both be set to the overridden height
+                // The measureHeight and the shown height should both be set to the overridden
+                // height
                 result.height = result.measureHeight
                 result.width = result.measureWidth
                 // Make sure all background views are also resized such that their size is correct
@@ -579,8 +647,13 @@
                 expandedLayout.load(context, R.xml.media_session_expanded)
             }
             TYPE.RECOMMENDATION -> {
-                collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
-                expandedLayout.load(context, R.xml.media_recommendation_expanded)
+                if (mediaFlags.isRecommendationCardUpdateEnabled()) {
+                    collapsedLayout.load(context, R.xml.media_recommendations_view_collapsed)
+                    expandedLayout.load(context, R.xml.media_recommendations_view_expanded)
+                } else {
+                    collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
+                    expandedLayout.load(context, R.xml.media_recommendation_expanded)
+                }
             }
         }
         refreshState()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index bcfceaa..e95106e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -16,11 +16,16 @@
 
 package com.android.systemui.media.controls.util;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.Bundle;
 import android.text.TextUtils;
 
+import androidx.core.math.MathUtils;
+import androidx.media.utils.MediaConstants;
+
 /**
  * Utility class with common methods for media controls
  */
@@ -50,4 +55,36 @@
                         : unknownName);
         return applicationName;
     }
+
+    /**
+     * Check the bundle for extras indicating the progress percentage
+     *
+     * @param extras
+     * @return the progress value between 0-1 inclusive if prsent, otherwise null
+     */
+    public static Double getDescriptionProgress(@Nullable Bundle extras) {
+        if (extras == null
+                || !extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS)) {
+            return null;
+        }
+
+        int status = extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS);
+        switch (status) {
+            case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED:
+                return 0.0;
+            case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED:
+                return 1.0;
+            case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED: {
+                if (extras
+                        .containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE)) {
+                    double percent = extras
+                            .getDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE);
+                    return MathUtils.clamp(percent, 0.0, 1.0);
+                } else {
+                    return 0.5;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 8d4931a..9bc66f6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -42,4 +42,26 @@
      * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
      */
     fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
+
+    /** Check whether we show explicit indicator on UMO */
+    fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR)
+
+    /**
+     * If true, keep active media controls for the lifetime of the MediaSession, regardless of
+     * whether the underlying notification was dismissed
+     */
+    fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS)
+
+    /** Check whether we show the updated recommendation card. */
+    fun isRecommendationCardUpdateEnabled() =
+        featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
+
+    /** Check whether to get progress information for resume players */
+    fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
+
+    /** If true, do not automatically dismiss the recommendation card */
+    fun isPersistentSsCardEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_RECOMMENDATIONS)
+
+    /** Check whether we allow remote media to generate resume controls */
+    fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index bb833df..9ae4577 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -17,8 +17,7 @@
 package com.android.systemui.media.dagger;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
-import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
+import com.android.systemui.log.LogBufferFactory;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.media.controls.ui.MediaHost;
@@ -29,12 +28,9 @@
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
 import com.android.systemui.media.taptotransfer.MediaTttFlags;
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
-import com.android.systemui.media.taptotransfer.receiver.ChipReceiverInfo;
-import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
+import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogBuffer;
+import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogBuffer;
 import com.android.systemui.plugins.log.LogBuffer;
-import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo;
 
 import java.util.Optional;
 
@@ -94,22 +90,22 @@
         return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager);
     }
 
+    /** Provides a logging buffer related to the media tap-to-transfer chip on the sender device. */
     @Provides
     @SysUISingleton
-    @MediaTttSenderLogger
-    static MediaTttLogger<ChipbarInfo> providesMediaTttSenderLogger(
-            @MediaTttSenderLogBuffer LogBuffer buffer
-    ) {
-        return new MediaTttLogger<>("Sender", buffer);
+    @MediaTttSenderLogBuffer
+    static LogBuffer provideMediaTttSenderLogBuffer(LogBufferFactory factory) {
+        return factory.create("MediaTttSender", 30);
     }
 
+    /**
+     * Provides a logging buffer related to the media tap-to-transfer chip on the receiver device.
+     */
     @Provides
     @SysUISingleton
-    @MediaTttReceiverLogger
-    static MediaTttLogger<ChipReceiverInfo> providesMediaTttReceiverLogger(
-            @MediaTttReceiverLogBuffer LogBuffer buffer
-    ) {
-        return new MediaTttLogger<>("Receiver", buffer);
+    @MediaTttReceiverLogBuffer
+    static LogBuffer provideMediaTttReceiverLogBuffer(LogBufferFactory factory) {
+        return factory.create("MediaTttReceiver", 20);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 51b5a3d..769e0c8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,17 +16,21 @@
 
 package com.android.systemui.media.dialog;
 
+import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CheckBox;
 import android.widget.TextView;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.core.widget.CompoundButtonCompat;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -186,6 +190,17 @@
                     mCurrentActivePosition = position;
                     updateFullItemClickListener(v -> onItemClick(v, device));
                     setSingleLineLayout(getItemTitle(device));
+                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                        && mController.isSubStatusSupported() && device.hasDisabledReason()) {
+                    //update to subtext with device status
+                    setUpDeviceIcon(device);
+                    mSubTitleText.setText(
+                            Api34Impl.composeDisabledReason(device.getDisableReason(), mContext));
+                    updateConnectionFailedStatusIcon();
+                    updateFullItemClickListener(null);
+                    setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
+                            false /* showProgressBar */, true /* showSubtitle */,
+                            true /* showStatus */);
                 } else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
                     setUpDeviceIcon(device);
                     updateConnectionFailedStatusIcon();
@@ -389,4 +404,12 @@
             mTitleText.setText(groupDividerTitle);
         }
     }
+
+    @RequiresApi(34)
+    private static class Api34Impl {
+        @DoNotInline
+        static String composeDisabledReason(int reason, Context context) {
+            return "";
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 4e08050..dc75538 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -45,6 +45,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.settingslib.Utils;
@@ -142,11 +143,12 @@
         final TextView mVolumeValueText;
         final ImageView mTitleIcon;
         final ProgressBar mProgressBar;
-        final MediaOutputSeekbar mSeekBar;
         final LinearLayout mTwoLineLayout;
         final ImageView mStatusIcon;
         final CheckBox mCheckBox;
         final ViewGroup mEndTouchArea;
+        @VisibleForTesting
+        MediaOutputSeekbar mSeekBar;
         private String mDeviceId;
         private ValueAnimator mCornerAnimator;
         private ValueAnimator mVolumeAnimator;
@@ -390,6 +392,7 @@
                         mTitleIcon.setVisibility(View.VISIBLE);
                         mVolumeValueText.setVisibility(View.GONE);
                     }
+                    mController.logInteractionAdjustVolume(device);
                     mIsDragging = false;
                 }
             });
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index a9e1a4d..4803371 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -95,6 +95,7 @@
     private RecyclerView mDevicesRecyclerView;
     private LinearLayout mDeviceListLayout;
     private LinearLayout mCastAppLayout;
+    private LinearLayout mMediaMetadataSectionLayout;
     private Button mDoneButton;
     private Button mStopButton;
     private Button mAppButton;
@@ -240,6 +241,7 @@
         mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
         mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
         mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+        mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section);
         mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
         mDoneButton = mDialogView.requireViewById(R.id.done);
         mStopButton = mDialogView.requireViewById(R.id.stop);
@@ -255,21 +257,17 @@
         mDevicesRecyclerView.setLayoutManager(mLayoutManager);
         mDevicesRecyclerView.setAdapter(mAdapter);
         mDevicesRecyclerView.setHasFixedSize(false);
-        // Init header icon
-        mHeaderIcon.setOnClickListener(v -> onHeaderIconClick());
         // Init bottom buttons
         mDoneButton.setOnClickListener(v -> dismiss());
         mStopButton.setOnClickListener(v -> {
             mMediaOutputController.releaseSession();
             dismiss();
         });
-        mAppButton.setOnClickListener(v -> {
-            mBroadcastSender.closeSystemDialogs();
-            if (mMediaOutputController.getAppLaunchIntent() != null) {
-                mContext.startActivity(mMediaOutputController.getAppLaunchIntent());
-            }
-            dismiss();
-        });
+        mAppButton.setOnClickListener(v -> mMediaOutputController.tryToLaunchMediaApplication());
+        if (mMediaOutputController.isAdvancedLayoutSupported()) {
+            mMediaMetadataSectionLayout.setOnClickListener(
+                    v -> mMediaOutputController.tryToLaunchMediaApplication());
+        }
     }
 
     @Override
@@ -560,7 +558,7 @@
 
     @Override
     public void dismissDialog() {
-        dismiss();
+        mBroadcastSender.closeSystemDialogs();
     }
 
     void onHeaderIconClick() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 8eb25c4..5f5c686 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -141,7 +141,7 @@
     @VisibleForTesting
     LocalMediaManager mLocalMediaManager;
     @VisibleForTesting
-    private MediaOutputMetricLogger mMetricLogger;
+    MediaOutputMetricLogger mMetricLogger;
     private int mCurrentState;
 
     private int mColorItemContent;
@@ -382,6 +382,15 @@
         return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName);
     }
 
+    void tryToLaunchMediaApplication() {
+        Intent launchIntent = getAppLaunchIntent();
+        if (launchIntent != null) {
+            launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mCallback.dismissDialog();
+            mContext.startActivity(launchIntent);
+        }
+    }
+
     CharSequence getHeaderTitle() {
         if (mMediaController != null) {
             final MediaMetadata metadata = mMediaController.getMetadata();
@@ -493,20 +502,20 @@
         ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors,
                 isDarkTheme);
         if (isDarkTheme) {
-            mColorItemContent = mCurrentColorScheme.getAccent1().get(2); // A1-100
-            mColorSeekbarProgress = mCurrentColorScheme.getAccent2().get(7); // A2-600
-            mColorButtonBackground = mCurrentColorScheme.getAccent1().get(4); // A1-300
-            mColorItemBackground = mCurrentColorScheme.getNeutral2().get(9); // N2-800
-            mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().get(9); // A2-800
-            mColorPositiveButtonText = mCurrentColorScheme.getAccent2().get(9); // A2-800
-            mColorDialogBackground = mCurrentColorScheme.getNeutral1().get(10); // N1-900
+            mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100
+            mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600
+            mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300
+            mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800
+            mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800
+            mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800
+            mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900
         } else {
-            mColorItemContent = mCurrentColorScheme.getAccent1().get(9); // A1-800
-            mColorSeekbarProgress = mCurrentColorScheme.getAccent1().get(4); // A1-300
-            mColorButtonBackground = mCurrentColorScheme.getAccent1().get(7); // A1-600
-            mColorItemBackground = mCurrentColorScheme.getAccent2().get(1); // A2-50
-            mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().get(2); // A1-100
-            mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().get(1); // N1-50
+            mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800
+            mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300
+            mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600
+            mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50
+            mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100
+            mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50
             mColorDialogBackground = mCurrentColorScheme.getBackgroundColor();
         }
     }
@@ -630,50 +639,28 @@
 
     private void buildMediaItems(List<MediaDevice> devices) {
         synchronized (mMediaDevicesLock) {
-            //TODO(b/257851968): do the organization only when there's no suggested sorted order
-            // we get from application
-            attachRangeInfo(devices);
-            Collections.sort(devices, Comparator.naturalOrder());
+            if (!isRouteProcessSupported() || (isRouteProcessSupported()
+                    && !mLocalMediaManager.isPreferenceRouteListingExist())) {
+                attachRangeInfo(devices);
+                Collections.sort(devices, Comparator.naturalOrder());
+            }
             // For the first time building list, to make sure the top device is the connected
             // device.
+            boolean needToHandleMutingExpectedDevice =
+                    hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote();
+            final MediaDevice connectedMediaDevice =
+                    needToHandleMutingExpectedDevice ? null
+                            : getCurrentConnectedMediaDevice();
             if (mMediaItemList.isEmpty()) {
-                boolean needToHandleMutingExpectedDevice =
-                        hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote();
-                final MediaDevice connectedMediaDevice =
-                        needToHandleMutingExpectedDevice ? null
-                                : getCurrentConnectedMediaDevice();
                 if (connectedMediaDevice == null) {
                     if (DEBUG) {
                         Log.d(TAG, "No connected media device or muting expected device exist.");
                     }
-                    if (needToHandleMutingExpectedDevice) {
-                        for (MediaDevice device : devices) {
-                            if (device.isMutingExpectedDevice()) {
-                                mMediaItemList.add(0, new MediaItem(device));
-                                mMediaItemList.add(1, new MediaItem(mContext.getString(
-                                        R.string.media_output_group_title_speakers_and_displays),
-                                        MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
-                            } else {
-                                mMediaItemList.add(new MediaItem(device));
-                            }
-                        }
-                        mMediaItemList.add(new MediaItem());
-                    } else {
-                        mMediaItemList.addAll(
-                                devices.stream().map(MediaItem::new).collect(Collectors.toList()));
-                        categorizeMediaItems(null);
-                    }
+                    categorizeMediaItems(null, devices, needToHandleMutingExpectedDevice);
                     return;
                 }
                 // selected device exist
-                for (MediaDevice device : devices) {
-                    if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
-                        mMediaItemList.add(0, new MediaItem(device));
-                    } else {
-                        mMediaItemList.add(new MediaItem(device));
-                    }
-                }
-                categorizeMediaItems(connectedMediaDevice);
+                categorizeMediaItems(connectedMediaDevice, devices, false);
                 return;
             }
             // To keep the same list order
@@ -707,31 +694,46 @@
         }
     }
 
-    private void categorizeMediaItems(MediaDevice connectedMediaDevice) {
+    private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices,
+            boolean needToHandleMutingExpectedDevice) {
         synchronized (mMediaDevicesLock) {
             Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
                     MediaDevice::getId).collect(Collectors.toSet());
             if (connectedMediaDevice != null) {
                 selectedDevicesIds.add(connectedMediaDevice.getId());
             }
-            int latestSelected = 1;
-            for (MediaItem item : mMediaItemList) {
-                if (item.getMediaDevice().isPresent()) {
-                    MediaDevice device = item.getMediaDevice().get();
-                    if (selectedDevicesIds.contains(device.getId())) {
-                        latestSelected = mMediaItemList.indexOf(item) + 1;
-                    } else {
-                        mMediaItemList.add(latestSelected, new MediaItem(mContext.getString(
-                                R.string.media_output_group_title_speakers_and_displays),
-                                MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
-                        break;
+            boolean suggestedDeviceAdded = false;
+            boolean displayGroupAdded = false;
+            for (MediaDevice device : devices) {
+                if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) {
+                    mMediaItemList.add(0, new MediaItem(device));
+                } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains(
+                        device.getId())) {
+                    mMediaItemList.add(0, new MediaItem(device));
+                } else {
+                    if (device.isSuggestedDevice() && !suggestedDeviceAdded) {
+                        attachGroupDivider(mContext.getString(
+                                R.string.media_output_group_title_suggested_device));
+                        suggestedDeviceAdded = true;
+                    } else if (!device.isSuggestedDevice() && !displayGroupAdded) {
+                        attachGroupDivider(mContext.getString(
+                                R.string.media_output_group_title_speakers_and_displays));
+                        displayGroupAdded = true;
                     }
+                    mMediaItemList.add(new MediaItem(device));
                 }
             }
             mMediaItemList.add(new MediaItem());
         }
     }
 
+    private void attachGroupDivider(String title) {
+        synchronized (mMediaDevicesLock) {
+            mMediaItemList.add(
+                    new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
+        }
+    }
+
     private void attachRangeInfo(List<MediaDevice> devices) {
         for (MediaDevice mediaDevice : devices) {
             if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) {
@@ -751,6 +753,14 @@
         return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT);
     }
 
+    public boolean isRouteProcessSupported() {
+        return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING);
+    }
+
+    public boolean isSubStatusSupported() {
+        return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_DEVICE_STATUS);
+    }
+
     List<MediaDevice> getGroupMediaDevices() {
         final List<MediaDevice> selectedDevices = getSelectedMediaDevice();
         final List<MediaDevice> selectableDevices = getSelectableMediaDevice();
@@ -860,12 +870,15 @@
     }
 
     void adjustVolume(MediaDevice device, int volume) {
-        mMetricLogger.logInteractionAdjustVolume(device);
         ThreadUtils.postOnBackgroundThread(() -> {
             device.requestSetVolume(volume);
         });
     }
 
+    void logInteractionAdjustVolume(MediaDevice device) {
+        mMetricLogger.logInteractionAdjustVolume(device);
+    }
+
     String getPackageName() {
         return mPackageName;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
new file mode 100644
index 0000000..e35575b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
@@ -0,0 +1,71 @@
+/*
+ * 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.media.dialog;
+
+import android.annotation.MainThread;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.statusbar.CommandQueue;
+
+import javax.inject.Inject;
+
+/** Controls display of media output switcher. */
+@SysUISingleton
+public class MediaOutputSwitcherDialogUI implements CoreStartable, CommandQueue.Callbacks {
+
+    private static final String TAG = "MediaOutputSwitcherDialogUI";
+
+    private final CommandQueue mCommandQueue;
+    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final FeatureFlags mFeatureFlags;
+
+    @Inject
+    public MediaOutputSwitcherDialogUI(
+            Context context,
+            CommandQueue commandQueue,
+            MediaOutputDialogFactory mediaOutputDialogFactory,
+            FeatureFlags featureFlags) {
+        mCommandQueue = commandQueue;
+        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mFeatureFlags = featureFlags;
+    }
+
+    @Override
+    public void start() {
+        if (mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_SHOW_API_ENABLED)) {
+            mCommandQueue.addCallback(this);
+        } else {
+            Log.w(TAG, "Show media output switcher is not enabled.");
+        }
+    }
+
+    @Override
+    @MainThread
+    public void showMediaOutputSwitcher(String packageName) {
+        if (!TextUtils.isEmpty(packageName)) {
+            mMediaOutputDialogFactory.create(packageName, false, null);
+        } else {
+            Log.e(TAG, "Unable to launch media output dialog. Package name is empty.");
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 8a565fa..60504e4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -30,4 +30,8 @@
     /** Check whether the flag for the receiver success state is enabled. */
     fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
         featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
+
+    /** True if the media transfer chip can be dismissed via a gesture. */
+    fun isMediaTttDismissGestureEnabled(): Boolean =
+        featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
deleted file mode 100644
index 8aef938..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media.taptotransfer.common
-
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
-import com.android.systemui.temporarydisplay.TemporaryViewLogger
-
-/**
- * A logger for media tap-to-transfer events.
- *
- * @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver".
- *
- * TODO(b/245610654): We should de-couple the sender and receiver loggers, since they're vastly
- * different experiences.
- */
-class MediaTttLogger<T : TemporaryViewInfo>(
-    deviceTypeTag: String,
-    buffer: LogBuffer
-) : TemporaryViewLogger<T>(buffer, BASE_TAG + deviceTypeTag) {
-    /** Logs a change in the chip state for the given [mediaRouteId]. */
-    fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
-        buffer.log(
-            tag,
-            LogLevel.DEBUG,
-            {
-                str1 = stateName
-                str2 = mediaRouteId
-                str3 = packageName
-            },
-            { "State changed to $str1 for ID=$str2 package=$str3" }
-        )
-    }
-
-    /**
-     * Logs an error in trying to update to [displayState].
-     *
-     * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or
-     * a [android.app.StatusBarManager.MediaTransferReceiverState].
-     */
-    fun logStateChangeError(displayState: Int) {
-        buffer.log(
-            tag,
-            LogLevel.ERROR,
-            { int1 = displayState },
-            { "Cannot display state=$int1; aborting" }
-        )
-    }
-
-    /**
-     * Logs an invalid sender state transition error in trying to update to [desiredState].
-     *
-     * @param currentState the previous state of the chip.
-     * @param desiredState the new state of the chip.
-     */
-    fun logInvalidStateTransitionError(
-        currentState: String,
-        desiredState: String
-    ) {
-        buffer.log(
-                tag,
-                LogLevel.ERROR,
-                {
-                    str1 = currentState
-                    str2 = desiredState
-                },
-                { "Cannot display state=$str2 after state=$str1; invalid transition" }
-        )
-    }
-
-    /** Logs that we couldn't find information for [packageName]. */
-    fun logPackageNotFound(packageName: String) {
-        buffer.log(
-            tag,
-            LogLevel.DEBUG,
-            { str1 = packageName },
-            { "Package $str1 could not be found" }
-        )
-    }
-
-    /**
-     * Logs that a removal request has been bypassed (ignored).
-     *
-     * @param removalReason the reason that the chip removal was requested.
-     * @param bypassReason the reason that the request was bypassed.
-     */
-    fun logRemovalBypass(removalReason: String, bypassReason: String) {
-        buffer.log(
-            tag,
-            LogLevel.DEBUG,
-            {
-                str1 = removalReason
-                str2 = bypassReason
-            },
-            { "Chip removal requested due to $str1; however, removal was ignored because $str2" })
-    }
-}
-
-private const val BASE_TAG = "MediaTtt"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt
new file mode 100644
index 0000000..0e839c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.taptotransfer.common
+
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+
+/** A helper for logging media tap-to-transfer events. */
+object MediaTttLoggerUtils {
+    fun logStateChange(
+        buffer: LogBuffer,
+        tag: String,
+        stateName: String,
+        mediaRouteId: String,
+        packageName: String?,
+    ) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = stateName
+                str2 = mediaRouteId
+                str3 = packageName
+            },
+            { "State changed to $str1 for ID=$str2 package=$str3" }
+        )
+    }
+
+    fun logStateChangeError(buffer: LogBuffer, tag: String, displayState: Int) {
+        buffer.log(
+            tag,
+            LogLevel.ERROR,
+            { int1 = displayState },
+            { "Cannot display state=$int1; aborting" }
+        )
+    }
+
+    fun logPackageNotFound(buffer: LogBuffer, tag: String, packageName: String) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            { str1 = packageName },
+            { "Package $str1 could not be found" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 066c185..720c44a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.TintedIcon
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 
 /** Utility methods for media tap-to-transfer. */
 class MediaTttUtils {
@@ -43,26 +42,39 @@
          * default name and icon if we can't find the app name/icon.
          *
          * @param appPackageName the package name of the app playing the media.
-         * @param logger the logger to use for any errors.
+         * @param onPackageNotFoundException a function run if a
+         * [PackageManager.NameNotFoundException] occurs.
+         * @param isReceiver indicates whether the icon is displayed in a receiver view.
          */
         fun getIconInfoFromPackageName(
             context: Context,
             appPackageName: String?,
-            logger: MediaTttLogger<out TemporaryViewInfo>
+            isReceiver: Boolean,
+            onPackageNotFoundException: () -> Unit,
         ): IconInfo {
             if (appPackageName != null) {
                 val packageManager = context.packageManager
                 try {
+                    val appName =
+                        packageManager
+                            .getApplicationInfo(
+                                appPackageName,
+                                PackageManager.ApplicationInfoFlags.of(0),
+                            )
+                            .loadLabel(packageManager)
+                            .toString()
                     val contentDescription =
-                        ContentDescription.Loaded(
-                            packageManager
-                                .getApplicationInfo(
-                                    appPackageName,
-                                    PackageManager.ApplicationInfoFlags.of(0)
+                        if (isReceiver) {
+                            ContentDescription.Loaded(
+                                context.getString(
+                                    R.string
+                                        .media_transfer_receiver_content_description_with_app_name,
+                                    appName
                                 )
-                                .loadLabel(packageManager)
-                                .toString()
-                        )
+                            )
+                        } else {
+                            ContentDescription.Loaded(appName)
+                        }
                     return IconInfo(
                         contentDescription,
                         MediaTttIcon.Loaded(packageManager.getApplicationIcon(appPackageName)),
@@ -70,11 +82,19 @@
                         isAppIcon = true
                     )
                 } catch (e: PackageManager.NameNotFoundException) {
-                    logger.logPackageNotFound(appPackageName)
+                    onPackageNotFoundException.invoke()
                 }
             }
             return IconInfo(
-                ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name),
+                if (isReceiver) {
+                    ContentDescription.Resource(
+                        R.string.media_transfer_receiver_content_description_unknown_app
+                    )
+                } else {
+                    ContentDescription.Resource(
+                        R.string.media_output_dialog_unknown_launch_app_name
+                    )
+                },
                 MediaTttIcon.Resource(R.drawable.ic_cast),
                 tintAttr = android.R.attr.textColorPrimary,
                 isAppIcon = 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 889147b..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
@@ -30,9 +32,11 @@
 import android.view.ViewGroup
 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.settingslib.Utils
 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
@@ -40,7 +44,6 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttIcon
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -65,7 +68,7 @@
 open class MediaTttChipControllerReceiver @Inject constructor(
         private val commandQueue: CommandQueue,
         context: Context,
-        @MediaTttReceiverLogger logger: MediaTttLogger<ChipReceiverInfo>,
+        logger: MediaTttReceiverLogger,
         windowManager: WindowManager,
         mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
@@ -78,7 +81,8 @@
         private val viewUtil: ViewUtil,
         wakeLockBuilder: WakeLock.Builder,
         systemClock: SystemClock,
-) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>(
+        private val rippleController: MediaTttReceiverRippleController,
+) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttReceiverLogger>(
         context,
         logger,
         windowManager,
@@ -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,
@@ -114,9 +125,6 @@
         }
     }
 
-    private var maxRippleWidth: Float = 0f
-    private var maxRippleHeight: Float = 0f
-
     private fun updateMediaTapToTransferReceiverDisplay(
         @StatusBarManager.MediaTransferReceiverState displayState: Int,
         routeInfo: MediaRoute2Info,
@@ -175,9 +183,14 @@
     }
 
     override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
+        val packageName = newInfo.routeInfo.clientPackageName
         var iconInfo = MediaTttUtils.getIconInfoFromPackageName(
-            context, newInfo.routeInfo.clientPackageName, logger
-        )
+            context,
+            packageName,
+            isReceiver = true,
+        ) {
+            logger.logPackageNotFound(packageName)
+        }
 
         if (newInfo.appNameOverride != null) {
             iconInfo = iconInfo.copy(
@@ -202,40 +215,51 @@
         val iconView = currentView.getAppIconView()
         iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
         TintedIconViewBinder.bind(iconInfo.toTintedIcon(), iconView)
+
+        val iconContainerView = currentView.getIconContainerView()
+        iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
     }
 
     override fun animateViewIn(view: ViewGroup) {
-        val appIconView = view.getAppIconView()
-        appIconView.animate()
-                .translationYBy(-1 * getTranslationAmount().toFloat())
-                .setDuration(ICON_TRANSLATION_ANIM_DURATION)
-                .start()
-        appIconView.animate()
-                .alpha(1f)
-                .setDuration(ICON_ALPHA_ANIM_DURATION)
-                .start()
-        // Using withEndAction{} doesn't apply a11y focus when screen is unlocked.
-        appIconView.postOnAnimation { view.requestAccessibilityFocus() }
-        expandRipple(view.requireViewById(R.id.ripple))
+        val iconContainerView = view.getIconContainerView()
+        val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
+        val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+        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()
-        appIconView.animate()
-                .translationYBy(getTranslationAmount().toFloat())
-                .setDuration(ICON_TRANSLATION_ANIM_DURATION)
-                .start()
-        appIconView.animate()
-                .alpha(0f)
-                .setDuration(ICON_ALPHA_ANIM_DURATION)
-                .start()
-
+        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()) {
-            expandRippleToFull(rippleView, onAnimationEnd)
+            rippleController.expandToSuccessState(rippleView, onAnimationEnd)
+            animateViewTranslationAndFade(
+                iconContainerView,
+                -1 * translationYBy,
+                0f,
+                translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
+                alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
+            )
         } else {
-            rippleView.collapseRipple(onAnimationEnd)
+            rippleController.collapseRipple(rippleView, onAnimationEnd)
+            animateViewTranslationAndFade(iconContainerView, translationYBy, 0f)
         }
     }
 
@@ -245,74 +269,70 @@
         viewUtil.setRectToViewWindowLocation(view.getAppIconView(), outRect)
     }
 
+    /** Animation of view translation and fading. */
+    private fun animateViewTranslationAndFade(
+        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)
+            .setDuration(alphaDuration)
+            .start()
+    }
+
     /** Returns the amount that the chip will be translated by in its intro animation. */
-    private fun getTranslationAmount(): Int {
-        return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
-    }
-
-    private fun expandRipple(rippleView: ReceiverChipRippleView) {
-        if (rippleView.rippleInProgress()) {
-            // Skip if ripple is still playing
-            return
-        }
-
-        // In case the device orientation changes, we need to reset the layout.
-        rippleView.addOnLayoutChangeListener (
-            View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
-                if (v == null) return@OnLayoutChangeListener
-
-                val layoutChangedRippleView = v as ReceiverChipRippleView
-                layoutRipple(layoutChangedRippleView)
-                layoutChangedRippleView.invalidate()
-            }
-        )
-        rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
-            override fun onViewDetachedFromWindow(view: View?) {}
-
-            override fun onViewAttachedToWindow(view: View?) {
-                if (view == null) {
-                    return
-                }
-                val attachedRippleView = view as ReceiverChipRippleView
-                layoutRipple(attachedRippleView)
-                attachedRippleView.expandRipple()
-                attachedRippleView.removeOnAttachStateChangeListener(this)
-            }
-        })
-    }
-
-    private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
-        val windowBounds = windowManager.currentWindowMetrics.bounds
-        val height = windowBounds.height().toFloat()
-        val width = windowBounds.width().toFloat()
-
-        if (isFullScreen) {
-            maxRippleHeight = height * 2f
-            maxRippleWidth = width * 2f
-        } else {
-            maxRippleHeight = height / 2f
-            maxRippleWidth = width / 2f
-        }
-        rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
-        // Center the ripple on the bottom of the screen in the middle.
-        rippleView.setCenter(width * 0.5f, height)
-        val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
-        rippleView.setColor(color, 70)
+    private fun getTranslationAmount(): Float {
+        return rippleController.getRippleSize() * 0.5f
     }
 
     private fun View.getAppIconView(): CachingIconView {
         return this.requireViewById(R.id.app_icon)
     }
 
-    private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
-        layoutRipple(rippleView, true)
-        rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+    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
     }
 }
 
-val ICON_TRANSLATION_ANIM_DURATION = 30.frames
-val ICON_ALPHA_ANIM_DURATION = 5.frames
-
 data class ChipReceiverInfo(
     val routeInfo: MediaRoute2Info,
     val appIconDrawableOverride: Drawable?,
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogBuffer.java
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
rename to packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogBuffer.java
index 1570d43..67e464c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogBuffer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log.dagger;
+package com.android.systemui.media.taptotransfer.receiver;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@@ -26,8 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for
- * {@link com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger}.
+ * A {@link LogBuffer} for receiver logs.
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
index 54fc48d..b0c6257 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
@@ -13,14 +13,44 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.systemui.media.taptotransfer.receiver
 
-import java.lang.annotation.Documented
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
-import javax.inject.Qualifier
+import android.app.StatusBarManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.taptotransfer.common.MediaTttLoggerUtils
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.temporarydisplay.TemporaryViewLogger
+import javax.inject.Inject
 
-@Qualifier
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-annotation class MediaTttReceiverLogger
+/** A logger for all events related to the media tap-to-transfer receiver experience. */
+@SysUISingleton
+class MediaTttReceiverLogger
+@Inject
+constructor(
+    @MediaTttReceiverLogBuffer buffer: LogBuffer,
+) : TemporaryViewLogger<ChipReceiverInfo>(buffer, TAG) {
+
+    /** Logs a change in the chip state for the given [mediaRouteId]. */
+    fun logStateChange(
+        stateName: String,
+        mediaRouteId: String,
+        packageName: String?,
+    ) {
+        MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, mediaRouteId, packageName)
+    }
+
+    /** Logs an error in trying to update to [displayState]. */
+    fun logStateChangeError(@StatusBarManager.MediaTransferReceiverState displayState: Int) {
+        MediaTttLoggerUtils.logStateChangeError(buffer, TAG, displayState)
+    }
+
+    /** Logs that we couldn't find information for [packageName]. */
+    fun logPackageNotFound(packageName: String) {
+        MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
+    }
+
+    companion object {
+        private const val TAG = "MediaTttReceiver"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
new file mode 100644
index 0000000..5013802
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.media.taptotransfer.receiver
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import android.view.WindowManager
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import javax.inject.Inject
+
+/**
+ * A controller responsible for the animation of the ripples shown in media tap-to-transfer on the
+ * receiving device.
+ */
+class MediaTttReceiverRippleController
+@Inject
+constructor(
+    private val context: Context,
+    private val windowManager: WindowManager,
+) {
+
+    private var maxRippleWidth: Float = 0f
+    private var maxRippleHeight: Float = 0f
+
+    /** Expands the icon and main ripple to in-progress state */
+    fun expandToInProgressState(
+        mainRippleView: ReceiverChipRippleView,
+        iconRippleView: ReceiverChipRippleView,
+    ) {
+        expandRipple(mainRippleView, isIconRipple = false)
+        expandRipple(iconRippleView, isIconRipple = true)
+    }
+
+    private fun expandRipple(rippleView: ReceiverChipRippleView, isIconRipple: Boolean) {
+        if (rippleView.rippleInProgress()) {
+            // Skip if ripple is still playing
+            return
+        }
+
+        // In case the device orientation changes, we need to reset the layout.
+        rippleView.addOnLayoutChangeListener(
+            View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
+                if (v == null) return@OnLayoutChangeListener
+
+                val layoutChangedRippleView = v as ReceiverChipRippleView
+                if (isIconRipple) {
+                    layoutIconRipple(layoutChangedRippleView)
+                } else {
+                    layoutRipple(layoutChangedRippleView)
+                }
+                layoutChangedRippleView.invalidate()
+            }
+        )
+        rippleView.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewDetachedFromWindow(view: View?) {}
+
+                override fun onViewAttachedToWindow(view: View?) {
+                    if (view == null) {
+                        return
+                    }
+                    val attachedRippleView = view as ReceiverChipRippleView
+                    if (isIconRipple) {
+                        layoutIconRipple(attachedRippleView)
+                    } else {
+                        layoutRipple(attachedRippleView)
+                    }
+                    attachedRippleView.expandRipple()
+                    attachedRippleView.removeOnAttachStateChangeListener(this)
+                }
+            }
+        )
+    }
+
+    /** Expands the ripple to cover the screen. */
+    fun expandToSuccessState(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+        layoutRipple(rippleView, isFullScreen = true)
+        rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+    }
+
+    /** Collapses the ripple. */
+    fun collapseRipple(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable? = null) {
+        rippleView.collapseRipple(onAnimationEnd)
+    }
+
+    private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
+        val windowBounds = windowManager.currentWindowMetrics.bounds
+        val height = windowBounds.height().toFloat()
+        val width = windowBounds.width().toFloat()
+
+        if (isFullScreen) {
+            maxRippleHeight = height * 2f
+            maxRippleWidth = width * 2f
+        } else {
+            maxRippleHeight = getRippleSize()
+            maxRippleWidth = getRippleSize()
+        }
+        rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
+        // Center the ripple on the bottom of the screen in the middle.
+        rippleView.setCenter(width * 0.5f, height)
+        rippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
+    }
+
+    private fun layoutIconRipple(iconRippleView: ReceiverChipRippleView) {
+        val windowBounds = windowManager.currentWindowMetrics.bounds
+        val height = windowBounds.height().toFloat()
+        val width = windowBounds.width().toFloat()
+        val radius = getReceiverIconSize().toFloat()
+
+        iconRippleView.setMaxSize(radius * 0.8f, radius * 0.8f)
+        iconRippleView.setCenter(
+            width * 0.5f,
+            height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin()
+        )
+        iconRippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
+    }
+
+    private fun getRippleColor(): Int {
+        var colorStateList =
+            ColorStateList.valueOf(
+                Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
+            )
+        return colorStateList.withLStar(TONE_PERCENT).defaultColor
+    }
+
+    /** Returns the size of the ripple. */
+    internal fun getRippleSize(): Float {
+        return getReceiverIconSize() * 4f
+    }
+
+    /** Returns the size of the icon of the receiver. */
+    internal fun getReceiverIconSize(): Int {
+        return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver)
+    }
+
+    /** Return the bottom margin of the icon of the receiver. */
+    internal fun getReceiverIconBottomMargin(): Int {
+        // Adding a margin to make sure ripple behind the icon is not cut by the screen bounds.
+        return context.resources.getDimensionPixelSize(
+            R.dimen.media_ttt_receiver_icon_bottom_margin
+        )
+    }
+
+    companion object {
+        const val RIPPLE_OPACITY = 70
+        const val TONE_PERCENT = 95f
+    }
+}
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 87b2528..0b0535d 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
@@ -33,14 +33,14 @@
     private var isStarted: Boolean
 
     init {
-        setupShader(RippleShader.RippleShape.ELLIPSE)
-        setRippleFill(true)
+        setupShader(RippleShader.RippleShape.CIRCLE)
+        setupRippleFadeParams()
         setSparkleStrength(0f)
-        duration = 3000L
         isStarted = false
     }
 
     fun expandRipple(onAnimationEnd: Runnable? = null) {
+        duration = DEFAULT_DURATION
         isStarted = true
         super.startRipple(onAnimationEnd)
     }
@@ -50,6 +50,7 @@
         if (!isStarted) {
             return // Ignore if ripple is not started yet.
         }
+        duration = DEFAULT_DURATION
         // Reset all listeners to animator.
         animator.removeAllListeners()
         animator.addListener(object : AnimatorListenerAdapter() {
@@ -71,15 +72,16 @@
         animator.removeAllUpdateListeners()
 
         // Only show the outline as ripple expands and disappears when animation ends.
-        setRippleFill(false)
+        removeRippleFill()
 
         val startingPercentage = calculateStartingPercentage(newHeight)
+        animator.duration = EXPAND_TO_FULL_DURATION
         animator.addUpdateListener { updateListener ->
             val now = updateListener.currentPlayTime
             val progress = updateListener.animatedValue as Float
-            rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
-            rippleShader.distortionStrength = 1 - rippleShader.progress
-            rippleShader.pixelDensity = 1 - rippleShader.progress
+            rippleShader.rawProgress = startingPercentage + (progress * (1 - startingPercentage))
+            rippleShader.distortionStrength = 1 - rippleShader.rawProgress
+            rippleShader.pixelDensity = 1 - rippleShader.rawProgress
             rippleShader.time = now.toFloat()
             invalidate()
         }
@@ -96,8 +98,45 @@
     // Calculates the actual starting percentage according to ripple shader progress set method.
     // Check calculations in [RippleShader.progress]
     fun calculateStartingPercentage(newHeight: Float): Float {
-        val ratio = rippleShader.currentHeight / newHeight
+        val ratio = rippleShader.rippleSize.currentHeight / newHeight
         val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
         return 1 - remainingPercentage
     }
+
+    private fun setupRippleFadeParams() {
+        with(rippleShader) {
+            // No fade out for the base ring.
+            baseRingFadeParams.fadeOutStart = 1f
+            baseRingFadeParams.fadeOutEnd = 1f
+
+            // No fade in and outs for the center fill, as we always draw it.
+            centerFillFadeParams.fadeInStart = 0f
+            centerFillFadeParams.fadeInEnd = 0f
+            centerFillFadeParams.fadeOutStart = 1f
+            centerFillFadeParams.fadeOutEnd = 1f
+        }
+    }
+
+    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
+
+            // 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
+        }
+    }
+
+    companion object {
+        const val DEFAULT_DURATION = 333L
+        const val EXPAND_TO_FULL_DURATION = 1000L
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 1f27582..537dbb9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -136,9 +136,9 @@
         ),
     ) {
         override fun isValidNextState(nextState: ChipStateSender): Boolean {
-            return nextState == FAR_FROM_RECEIVER ||
-                    nextState == ALMOST_CLOSE_TO_START_CAST ||
-                    nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+            // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any
+            // state that represents the beginning of a new sequence.
+            return stateIsStartOfSequence(nextState)
         }
     },
 
@@ -158,9 +158,9 @@
         ),
     ) {
         override fun isValidNextState(nextState: ChipStateSender): Boolean {
-            return nextState == FAR_FROM_RECEIVER ||
-                    nextState == ALMOST_CLOSE_TO_END_CAST ||
-                    nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+            // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any
+            // state that represents the beginning of a new sequence.
+            return stateIsStartOfSequence(nextState)
         }
     },
 
@@ -173,9 +173,9 @@
         endItem = SenderEndItem.Error,
     ) {
         override fun isValidNextState(nextState: ChipStateSender): Boolean {
-            return nextState == FAR_FROM_RECEIVER ||
-                    nextState == ALMOST_CLOSE_TO_START_CAST ||
-                    nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+            // Since _FAILED is the end of a transfer sequence, we should be able to move to any
+            // state that represents the beginning of a new sequence.
+            return stateIsStartOfSequence(nextState)
         }
     },
 
@@ -188,9 +188,9 @@
         endItem = SenderEndItem.Error,
     ) {
         override fun isValidNextState(nextState: ChipStateSender): Boolean {
-            return nextState == FAR_FROM_RECEIVER ||
-                    nextState == ALMOST_CLOSE_TO_END_CAST ||
-                    nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+            // Since _FAILED is the end of a transfer sequence, we should be able to move to any
+            // state that represents the beginning of a new sequence.
+            return stateIsStartOfSequence(nextState)
         }
     },
 
@@ -210,9 +210,9 @@
         }
 
         override fun isValidNextState(nextState: ChipStateSender): Boolean {
-            return nextState == FAR_FROM_RECEIVER ||
-                    nextState.transferStatus == TransferStatus.NOT_STARTED ||
-                    nextState.transferStatus == TransferStatus.IN_PROGRESS
+            // When far away, we can go to any state that represents the start of a transfer
+            // sequence.
+            return stateIsStartOfSequence(nextState)
         }
     };
 
@@ -227,6 +227,20 @@
         return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
     }
 
+    /**
+     * Returns true if moving from this state to [nextState] is a valid transition.
+     *
+     * In general, we expect a media transfer go to through a sequence of states:
+     * For push-to-receiver:
+     *   - ALMOST_CLOSE_TO_START_CAST => TRANSFER_TO_RECEIVER_TRIGGERED =>
+     *     TRANSFER_TO_RECEIVER_(SUCCEEDED|FAILED)
+     *   - ALMOST_CLOSE_TO_END_CAST => TRANSFER_TO_THIS_DEVICE_TRIGGERED =>
+     *     TRANSFER_TO_THIS_DEVICE_(SUCCEEDED|FAILED)
+     *
+     * This method should validate that the states go through approximately that sequence.
+     *
+     * See b/221265848 for more details.
+     */
     abstract fun isValidNextState(nextState: ChipStateSender): Boolean
 
     companion object {
@@ -276,6 +290,18 @@
 
             return currentState.isValidNextState(desiredState)
         }
+
+        /**
+         * Returns true if [state] represents a state at the beginning of a sequence and false
+         * otherwise.
+         */
+        private fun stateIsStartOfSequence(state: ChipStateSender): Boolean {
+            return state == FAR_FROM_RECEIVER ||
+                state.transferStatus == TransferStatus.NOT_STARTED ||
+                // It's possible to skip the NOT_STARTED phase and go immediately into the
+                // IN_PROGRESS phase.
+                state.transferStatus == TransferStatus.IN_PROGRESS
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index 9f44d98..6bb6906 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -23,17 +23,20 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.CoreStartable
+import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
 import com.android.systemui.temporarydisplay.ViewPriority
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
+import java.io.PrintWriter
 import javax.inject.Inject
 
 /**
@@ -47,12 +50,12 @@
     private val chipbarCoordinator: ChipbarCoordinator,
     private val commandQueue: CommandQueue,
     private val context: Context,
-    @MediaTttSenderLogger private val logger: MediaTttLogger<ChipbarInfo>,
+    private val dumpManager: DumpManager,
+    private val logger: MediaTttSenderLogger,
     private val mediaTttFlags: MediaTttFlags,
     private val uiEventLogger: MediaTttSenderUiEventLogger,
-) : CoreStartable {
+) : CoreStartable, Dumpable {
 
-    private var displayedState: ChipStateSender? = null
     // A map to store current chip state per id.
     private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
 
@@ -74,6 +77,7 @@
     override fun start() {
         if (mediaTttFlags.isMediaTttEnabled()) {
             commandQueue.addCallback(commandQueueCallbacks)
+            dumpManager.registerNormalDumpable(this)
         }
     }
 
@@ -91,42 +95,42 @@
             return
         }
 
-        val currentState = stateMap[routeInfo.id]
-        if (!ChipStateSender.isValidStateTransition(currentState, chipState)) {
+        val currentStateForId: ChipStateSender? = stateMap[routeInfo.id]
+        if (!ChipStateSender.isValidStateTransition(currentStateForId, chipState)) {
             // ChipStateSender.FAR_FROM_RECEIVER is the default state when there is no state.
             logger.logInvalidStateTransitionError(
-                currentState = currentState?.name ?: ChipStateSender.FAR_FROM_RECEIVER.name,
+                currentState = currentStateForId?.name ?: ChipStateSender.FAR_FROM_RECEIVER.name,
                 chipState.name
             )
             return
         }
         uiEventLogger.logSenderStateChange(chipState)
 
-        stateMap.put(routeInfo.id, chipState)
         if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
-            // No need to store the state since it is the default state
-            stateMap.remove(routeInfo.id)
-            // Return early if we're not displaying a chip anyway
-            val currentDisplayedState = displayedState ?: return
+            // Return early if we're not displaying a chip for this ID anyway
+            if (currentStateForId == null) return
 
             val removalReason = ChipStateSender.FAR_FROM_RECEIVER.name
             if (
-                currentDisplayedState.transferStatus == TransferStatus.IN_PROGRESS ||
-                    currentDisplayedState.transferStatus == TransferStatus.SUCCEEDED
+                currentStateForId.transferStatus == TransferStatus.IN_PROGRESS ||
+                    currentStateForId.transferStatus == TransferStatus.SUCCEEDED
             ) {
                 // Don't remove the chip if we're in progress or succeeded, since the user should
                 // still be able to see the status of the transfer.
                 logger.logRemovalBypass(
                     removalReason,
-                    bypassReason = "transferStatus=${currentDisplayedState.transferStatus.name}"
+                    bypassReason = "transferStatus=${currentStateForId.transferStatus.name}"
                 )
                 return
             }
 
-            displayedState = null
+            // No need to store the state since it is the default state
+            removeIdFromStore(routeInfo.id, reason = removalReason)
             chipbarCoordinator.removeView(routeInfo.id, removalReason)
         } else {
-            displayedState = chipState
+            stateMap[routeInfo.id] = chipState
+            logger.logStateMap(stateMap)
+            chipbarCoordinator.registerListener(displayListener)
             chipbarCoordinator.displayView(
                 createChipbarInfo(
                     chipState,
@@ -135,7 +139,7 @@
                     context,
                     logger,
                 )
-            ) { stateMap.remove(routeInfo.id) }
+            )
         }
     }
 
@@ -147,16 +151,23 @@
         routeInfo: MediaRoute2Info,
         undoCallback: IUndoMediaTransferCallback?,
         context: Context,
-        logger: MediaTttLogger<ChipbarInfo>,
+        logger: MediaTttSenderLogger,
     ): ChipbarInfo {
         val packageName = routeInfo.clientPackageName
-        val otherDeviceName = routeInfo.name.toString()
+        val otherDeviceName =
+            if (routeInfo.name.isBlank()) {
+                context.getString(R.string.media_ttt_default_device_type)
+            } else {
+                routeInfo.name.toString()
+            }
+        val icon =
+            MediaTttUtils.getIconInfoFromPackageName(context, packageName, isReceiver = false) {
+                logger.logPackageNotFound(packageName)
+            }
 
         return ChipbarInfo(
             // Display the app's icon as the start icon
-            startIcon =
-                MediaTttUtils.getIconInfoFromPackageName(context, packageName, logger)
-                    .toTintedIcon(),
+            startIcon = icon.toTintedIcon(),
             text = chipStateSender.getChipTextString(context, otherDeviceName),
             endItem =
                 when (chipStateSender.endItem) {
@@ -177,6 +188,7 @@
                     }
                 },
             vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+            allowSwipeToDismiss = true,
             windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
             wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
             timeoutMs = chipStateSender.timeout,
@@ -220,4 +232,21 @@
             onClickListener,
         )
     }
+
+    private val displayListener =
+        TemporaryViewDisplayController.Listener { id, reason -> removeIdFromStore(id, reason) }
+
+    private fun removeIdFromStore(id: String, reason: String) {
+        logger.logStateMapRemoval(id, reason)
+        stateMap.remove(id)
+        logger.logStateMap(stateMap)
+        if (stateMap.isEmpty()) {
+            chipbarCoordinator.unregisterListener(displayListener)
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("Current sender states:")
+        pw.println(stateMap.toString())
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogBuffer.java
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
rename to packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogBuffer.java
index bf216c6..a262e97 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogBuffer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log.dagger;
+package com.android.systemui.media.taptotransfer.sender;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@@ -26,8 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for
- * {@link com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger}.
+ * A {@link LogBuffer} for sender logs.
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
index 4393af9..964a95b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
@@ -13,14 +13,102 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.systemui.media.taptotransfer.sender
 
-import java.lang.annotation.Documented
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
-import javax.inject.Qualifier
+import android.app.StatusBarManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.taptotransfer.common.MediaTttLoggerUtils
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
 
-@Qualifier
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-annotation class MediaTttSenderLogger
+/** A logger for all events related to the media tap-to-transfer sender experience. */
+@SysUISingleton
+class MediaTttSenderLogger
+@Inject
+constructor(
+    @MediaTttSenderLogBuffer private val buffer: LogBuffer,
+) {
+    /** Logs a change in the chip state for the given [mediaRouteId]. */
+    fun logStateChange(
+        stateName: String,
+        mediaRouteId: String,
+        packageName: String?,
+    ) {
+        MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, mediaRouteId, packageName)
+    }
+
+    /** Logs an error in trying to update to [displayState]. */
+    fun logStateChangeError(@StatusBarManager.MediaTransferSenderState displayState: Int) {
+        MediaTttLoggerUtils.logStateChangeError(buffer, TAG, displayState)
+    }
+
+    /** Logs that we couldn't find information for [packageName]. */
+    fun logPackageNotFound(packageName: String) {
+        MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
+    }
+
+    /**
+     * Logs an invalid sender state transition error in trying to update to [desiredState].
+     *
+     * @param currentState the previous state of the chip.
+     * @param desiredState the new state of the chip.
+     */
+    fun logInvalidStateTransitionError(currentState: String, desiredState: String) {
+        buffer.log(
+            TAG,
+            LogLevel.ERROR,
+            {
+                str1 = currentState
+                str2 = desiredState
+            },
+            { "Cannot display state=$str2 after state=$str1; invalid transition" }
+        )
+    }
+
+    /**
+     * Logs that a removal request has been bypassed (ignored).
+     *
+     * @param removalReason the reason that the chip removal was requested.
+     * @param bypassReason the reason that the request was bypassed.
+     */
+    fun logRemovalBypass(removalReason: String, bypassReason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = removalReason
+                str2 = bypassReason
+            },
+            { "Chip removal requested due to $str1; however, removal was ignored because $str2" }
+        )
+    }
+
+    /** Logs the current contents of the state map. */
+    fun logStateMap(map: Map<String, ChipStateSender>) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = map.toString() },
+            { "Current sender states: $str1" }
+        )
+    }
+
+    /** Logs that [id] has been removed from the state map due to [reason]. */
+    fun logStateMapRemoval(id: String, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = id
+                str2 = reason
+            },
+            { "State removal: id=$str1 reason=$str2" }
+        )
+    }
+
+    companion object {
+        private const val TAG = "MediaTttSender"
+    }
+}
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 6c41caa..3088d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -19,17 +19,24 @@
 import android.app.Activity
 import android.content.ComponentName
 import android.content.Context
+import android.os.UserHandle
 import com.android.launcher3.icons.IconFactory
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.media.MediaProjectionAppSelectorActivity
+import com.android.systemui.media.MediaProjectionAppSelectorActivity.Companion.EXTRA_HOST_APP_USER_HANDLE
+import com.android.systemui.media.MediaProjectionPermissionActivity
+import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader
 import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
 import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
 import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskLabelLoader
 import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
 import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
 import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider
 import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
 import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider
+import com.android.systemui.mediaprojection.devicepolicy.MediaProjectionDevicePolicyModule
+import com.android.systemui.mediaprojection.devicepolicy.PersonalProfile
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
 import com.android.systemui.statusbar.policy.ConfigurationController
 import dagger.Binds
@@ -46,9 +53,14 @@
 
 @Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
 
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class HostUserHandle
+
 @Retention(AnnotationRetention.RUNTIME) @Scope annotation class MediaProjectionAppSelectorScope
 
-@Module(subcomponents = [MediaProjectionAppSelectorComponent::class])
+@Module(
+    subcomponents = [MediaProjectionAppSelectorComponent::class],
+    includes = [MediaProjectionDevicePolicyModule::class]
+)
 interface MediaProjectionModule {
     @Binds
     @IntoMap
@@ -56,11 +68,17 @@
     fun provideMediaProjectionAppSelectorActivity(
         activity: MediaProjectionAppSelectorActivity
     ): Activity
+
+    @Binds
+    @IntoMap
+    @ClassKey(MediaProjectionPermissionActivity::class)
+    fun bindsMediaProjectionPermissionActivity(impl: MediaProjectionPermissionActivity): Activity
 }
 
-/** Scoped values for [MediaProjectionAppSelectorComponent].
- *  We create a scope for the activity so certain dependencies like [TaskPreviewSizeProvider]
- *  could be reused. */
+/**
+ * Scoped values for [MediaProjectionAppSelectorComponent]. We create a scope for the activity so
+ * certain dependencies like [TaskPreviewSizeProvider] could be reused.
+ */
 @Module
 interface MediaProjectionAppSelectorModule {
 
@@ -72,6 +90,10 @@
 
     @Binds
     @MediaProjectionAppSelectorScope
+    fun bindRecentTaskLabelLoader(impl: ActivityTaskManagerLabelLoader): RecentTaskLabelLoader
+
+    @Binds
+    @MediaProjectionAppSelectorScope
     fun bindRecentTaskListProvider(impl: ShellRecentTaskListProvider): RecentTaskListProvider
 
     @Binds
@@ -83,7 +105,13 @@
         @MediaProjectionAppSelector
         @MediaProjectionAppSelectorScope
         fun provideAppSelectorComponentName(context: Context): ComponentName =
-                ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
+            ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
+
+        @Provides
+        @MediaProjectionAppSelector
+        @MediaProjectionAppSelectorScope
+        fun provideCallerPackageName(activity: MediaProjectionAppSelectorActivity): String? =
+            activity.callingPackage
 
         @Provides
         @MediaProjectionAppSelector
@@ -93,9 +121,20 @@
         ): ConfigurationController = ConfigurationControllerImpl(activity)
 
         @Provides
-        fun bindIconFactory(
-            context: Context
-        ): IconFactory = IconFactory.obtain(context)
+        @HostUserHandle
+        @MediaProjectionAppSelectorScope
+        fun hostUserHandle(activity: MediaProjectionAppSelectorActivity): UserHandle {
+            val extras =
+                activity.intent.extras
+                    ?: error("MediaProjectionAppSelectorActivity should be launched with extras")
+            return extras.getParcelable(EXTRA_HOST_APP_USER_HANDLE)
+                ?: error(
+                    "MediaProjectionAppSelectorActivity should be provided with " +
+                        "$EXTRA_HOST_APP_USER_HANDLE extra"
+                )
+        }
+
+        @Provides fun bindIconFactory(context: Context): IconFactory = IconFactory.obtain(context)
 
         @Provides
         @MediaProjectionAppSelector
@@ -112,9 +151,7 @@
     /** Generates [MediaProjectionAppSelectorComponent]. */
     @Subcomponent.Factory
     interface Factory {
-        /**
-         * Create a factory to inject the activity into the graph
-         */
+        /** Create a factory to inject the activity into the graph */
         fun create(
             @BindsInstance activity: MediaProjectionAppSelectorActivity,
             @BindsInstance view: MediaProjectionAppSelectorView,
@@ -124,6 +161,9 @@
 
     val controller: MediaProjectionAppSelectorController
     val recentsViewController: MediaProjectionRecentsViewController
+    val emptyStateProvider: MediaProjectionBlockerEmptyStateProvider
+    @get:HostUserHandle val hostUserHandle: UserHandle
+    @get:PersonalProfile val personalProfileUserHandle: UserHandle
 
     @MediaProjectionAppSelector val configurationController: ConfigurationController
 }
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 d744a40b..219629b 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
@@ -17,24 +17,36 @@
 package com.android.systemui.mediaprojection.appselector
 
 import android.content.ComponentName
+import android.os.UserHandle
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
 import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
-import javax.inject.Inject
 
 @MediaProjectionAppSelectorScope
-class MediaProjectionAppSelectorController @Inject constructor(
+class MediaProjectionAppSelectorController
+@Inject
+constructor(
     private val recentTaskListProvider: RecentTaskListProvider,
     private val view: MediaProjectionAppSelectorView,
+    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 tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
+            val recentTasks = recentTaskListProvider.loadRecentTasks()
+
+            val tasks =
+                recentTasks.filterDevicePolicyRestrictedTasks().filterAppSelector().sortedTasks()
+
             view.bind(tasks)
         }
     }
@@ -43,9 +55,25 @@
         scope.cancel()
     }
 
-    private fun List<RecentTask>.sortTasks(): List<RecentTask> =
-        sortedBy {
-            // Show normal tasks first and only then tasks with opened app selector
-            it.topActivityComponent == appSelectorComponentName
+    /**
+     * Removes all recent tasks that are different from the profile of the host app to avoid any
+     * cross-profile sharing
+     */
+    private fun List<RecentTask>.filterDevicePolicyRestrictedTasks(): List<RecentTask> =
+        if (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+            // TODO(b/263950746): filter tasks based on the enterprise policies
+            this
+        } else {
+            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?.packageName == callerPackageName
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionBlockerEmptyStateProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionBlockerEmptyStateProvider.kt
new file mode 100644
index 0000000..829b0dd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionBlockerEmptyStateProvider.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.mediaprojection.appselector
+
+import android.content.Context
+import android.os.UserHandle
+import com.android.internal.R as AndroidR
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyState
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
+import com.android.internal.app.ResolverListAdapter
+import com.android.systemui.R
+import com.android.systemui.mediaprojection.devicepolicy.PersonalProfile
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
+import javax.inject.Inject
+
+@MediaProjectionAppSelectorScope
+class MediaProjectionBlockerEmptyStateProvider
+@Inject
+constructor(
+    @HostUserHandle private val hostAppHandle: UserHandle,
+    @PersonalProfile private val personalProfileHandle: UserHandle,
+    private val policyResolver: ScreenCaptureDevicePolicyResolver,
+    private val context: Context
+) : EmptyStateProvider {
+
+    override fun getEmptyState(resolverListAdapter: ResolverListAdapter): EmptyState? {
+        val screenCaptureAllowed =
+            policyResolver.isScreenCaptureAllowed(
+                targetAppUserHandle = resolverListAdapter.userHandle,
+                hostAppUserHandle = hostAppHandle
+            )
+
+        val isHostAppInPersonalProfile = hostAppHandle == personalProfileHandle
+
+        val subtitle =
+            if (isHostAppInPersonalProfile) {
+                AndroidR.string.resolver_cant_share_with_personal_apps_explanation
+            } else {
+                AndroidR.string.resolver_cant_share_with_work_apps_explanation
+            }
+
+        if (!screenCaptureAllowed) {
+            return object : EmptyState {
+                override fun getSubtitle(): String = context.resources.getString(subtitle)
+                override fun getTitle(): String =
+                    context.resources.getString(
+                        R.string.screen_capturing_disabled_by_policy_dialog_title
+                    )
+                override fun onEmptyStateShown() {
+                    // TODO(b/237397740) report analytics
+                }
+                override fun shouldSkipDataRebuild(): Boolean = true
+            }
+        }
+        return null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
index cd994b8..41e2286 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
@@ -17,11 +17,12 @@
 package com.android.systemui.mediaprojection.appselector.data
 
 import android.annotation.ColorInt
+import android.annotation.UserIdInt
 import android.content.ComponentName
 
 data class RecentTask(
     val taskId: Int,
-    val userId: Int,
+    @UserIdInt val userId: Int,
     val topActivityComponent: ComponentName?,
     val baseIntentComponent: ComponentName?,
     @ColorInt val colorBackground: Int?
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskLabelLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskLabelLoader.kt
new file mode 100644
index 0000000..eadcb93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskLabelLoader.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.mediaprojection.appselector.data
+
+import android.annotation.UserIdInt
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+interface RecentTaskLabelLoader {
+    suspend fun loadLabel(userId: Int, componentName: ComponentName): CharSequence?
+}
+
+class ActivityTaskManagerLabelLoader
+@Inject
+constructor(
+    @Background private val coroutineDispatcher: CoroutineDispatcher,
+    private val packageManager: PackageManager
+) : RecentTaskLabelLoader {
+
+    override suspend fun loadLabel(
+        @UserIdInt userId: Int,
+        componentName: ComponentName
+    ): CharSequence? =
+        withContext(coroutineDispatcher) {
+            val userHandle = UserHandle(userId)
+            val appInfo =
+                packageManager.getApplicationInfo(
+                    componentName.packageName,
+                    PackageManager.ApplicationInfoFlags.of(0 /* no flags */)
+                )
+            val label = packageManager.getApplicationLabel(appInfo)
+            return@withContext packageManager.getUserBadgedLabel(label, userHandle)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
index 15cfeee..64f97f2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
@@ -20,11 +20,12 @@
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
-import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.android.systemui.R
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelector
 import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskLabelLoader
 import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import dagger.assisted.Assisted
@@ -40,9 +41,10 @@
     @Assisted private val root: ViewGroup,
     private val iconLoader: AppIconLoader,
     private val thumbnailLoader: RecentTaskThumbnailLoader,
+    private val labelLoader: RecentTaskLabelLoader,
     private val taskViewSizeProvider: TaskPreviewSizeProvider,
     @MediaProjectionAppSelector private val scope: CoroutineScope
-) : RecyclerView.ViewHolder(root), ConfigurationListener, TaskPreviewSizeProvider.TaskPreviewSizeListener {
+) : ViewHolder(root), ConfigurationListener, TaskPreviewSizeProvider.TaskPreviewSizeListener {
 
     val thumbnailView: MediaProjectionTaskView = root.requireViewById(R.id.task_thumbnail)
     private val iconView: ImageView = root.requireViewById(R.id.task_icon)
@@ -64,6 +66,10 @@
                         val icon = iconLoader.loadIcon(task.userId, component)
                         iconView.setImageDrawable(icon)
                     }
+                    launch {
+                        val label = labelLoader.loadLabel(task.userId, component)
+                        root.contentDescription = label
+                    }
                 }
                 launch {
                     val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
@@ -88,10 +94,10 @@
 
     private fun updateThumbnailSize() {
         thumbnailView.layoutParams =
-                thumbnailView.layoutParams.apply {
-                    width = taskViewSizeProvider.size.width()
-                    height = taskViewSizeProvider.size.height()
-                }
+            thumbnailView.layoutParams.apply {
+                width = taskViewSizeProvider.size.width()
+                height = taskViewSizeProvider.size.height()
+            }
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/MediaProjectionDevicePolicyModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/MediaProjectionDevicePolicyModule.kt
new file mode 100644
index 0000000..13b71a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/MediaProjectionDevicePolicyModule.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.os.UserHandle
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import dagger.Module
+import dagger.Provides
+import javax.inject.Qualifier
+
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class WorkProfile
+
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class PersonalProfile
+
+/** Module for media projection device policy related dependencies */
+@Module
+class MediaProjectionDevicePolicyModule {
+    @Provides
+    @PersonalProfile
+    fun personalUserHandle(activityManagerWrapper: ActivityManagerWrapper): UserHandle {
+        // Current foreground user is the 'personal' profile
+        return UserHandle.of(activityManagerWrapper.currentUserId)
+    }
+
+    @Provides
+    @WorkProfile
+    fun workProfileUserHandle(userTracker: UserTracker): UserHandle? =
+        userTracker.userProfiles.find { it.isManagedProfile }?.userHandle
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt
new file mode 100644
index 0000000..6bd33e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.app.admin.DevicePolicyManager
+import android.os.UserHandle
+import android.os.UserManager
+import javax.inject.Inject
+
+/**
+ * Utility class to resolve if screen capture allowed for a particular target app/host app pair. It
+ * caches the state of the policies, so you need to create a new instance of this class if you want
+ * to react to updated policies state.
+ */
+class ScreenCaptureDevicePolicyResolver
+@Inject
+constructor(
+    private val devicePolicyManager: DevicePolicyManager,
+    private val userManager: UserManager,
+    @PersonalProfile private val personalProfileUserHandle: UserHandle,
+    @WorkProfile private val workProfileUserHandle: UserHandle?
+) {
+
+    /**
+     * Returns true if [hostAppUserHandle] is allowed to perform screen capture of
+     * [targetAppUserHandle]
+     */
+    fun isScreenCaptureAllowed(
+        targetAppUserHandle: UserHandle,
+        hostAppUserHandle: UserHandle,
+    ): Boolean {
+        if (hostAppUserHandle.isWorkProfile() && workProfileScreenCaptureDisabled) {
+            // Disable screen capturing as host apps should not capture the screen
+            return false
+        }
+
+        if (!hostAppUserHandle.isWorkProfile() && personalProfileScreenCaptureDisabled) {
+            // Disable screen capturing as personal apps should not capture the screen
+            return false
+        }
+
+        if (targetAppUserHandle.isWorkProfile()) {
+            // Work profile target
+            if (workProfileScreenCaptureDisabled) {
+                // Do not allow sharing work profile apps as work profile capturing is disabled
+                return false
+            }
+        } else {
+            // Personal profile target
+            if (hostAppUserHandle.isWorkProfile() && disallowSharingIntoManagedProfile) {
+                // Do not allow sharing of personal apps into work profile apps
+                return false
+            }
+
+            if (personalProfileScreenCaptureDisabled) {
+                // Disable screen capturing as personal apps should not be captured
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns true if [hostAppUserHandle] is NOT allowed to capture an app from any profile,
+     * could be useful to finish the screen capture flow as soon as possible when the screen
+     * could not be captured at all.
+     */
+    fun isScreenCaptureCompletelyDisabled(hostAppUserHandle: UserHandle): Boolean {
+        val isWorkAppsCaptureDisabled =
+                if (workProfileUserHandle != null) {
+                    !isScreenCaptureAllowed(
+                            targetAppUserHandle = workProfileUserHandle,
+                            hostAppUserHandle = hostAppUserHandle
+                    )
+                } else true
+
+        val isPersonalAppsCaptureDisabled =
+                !isScreenCaptureAllowed(
+                        targetAppUserHandle = personalProfileUserHandle,
+                        hostAppUserHandle = hostAppUserHandle
+                )
+
+        return isWorkAppsCaptureDisabled && isPersonalAppsCaptureDisabled
+    }
+
+    private val personalProfileScreenCaptureDisabled: Boolean by lazy {
+        devicePolicyManager.getScreenCaptureDisabled(
+            /* admin */ null,
+            personalProfileUserHandle.identifier
+        )
+    }
+
+    private val workProfileScreenCaptureDisabled: Boolean by lazy {
+        workProfileUserHandle?.let {
+            devicePolicyManager.getScreenCaptureDisabled(/* admin */ null, it.identifier)
+        }
+            ?: false
+    }
+
+    private val disallowSharingIntoManagedProfile: Boolean by lazy {
+        workProfileUserHandle?.let {
+            userManager.hasUserRestrictionForUser(
+                UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE,
+                it
+            )
+        }
+            ?: false
+    }
+
+    private fun UserHandle?.isWorkProfile(): Boolean = this == workProfileUserHandle
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt
new file mode 100644
index 0000000..a6b3da0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Dialog that shows that screen capture is disabled on this device. */
+class ScreenCaptureDisabledDialog(context: Context) : SystemUIDialog(context) {
+
+    init {
+        setTitle(context.getString(R.string.screen_capturing_disabled_by_policy_dialog_title))
+        setMessage(
+            context.getString(R.string.screen_capturing_disabled_by_policy_dialog_description)
+        )
+        setIcon(R.drawable.ic_cast)
+        setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> cancel() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
index 3ecf154..8d80990 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
@@ -16,13 +16,12 @@
 
 package com.android.systemui.model;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import android.annotation.NonNull;
 import android.util.Log;
 
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.system.QuickStepContract;
 
 import java.io.PrintWriter;
@@ -39,11 +38,16 @@
     private static final String TAG = SysUiState.class.getSimpleName();
     public static final boolean DEBUG = false;
 
+    private final DisplayTracker mDisplayTracker;
     private @QuickStepContract.SystemUiStateFlags int mFlags;
     private final List<SysUiStateCallback> mCallbacks = new ArrayList<>();
     private int mFlagsToSet = 0;
     private int mFlagsToClear = 0;
 
+    public SysUiState(DisplayTracker displayTracker) {
+        mDisplayTracker = displayTracker;
+    }
+
     /**
      * Add listener to be notified of changes made to SysUI state.
      * The callback will also be called as part of this function.
@@ -81,7 +85,7 @@
     }
 
     private void updateFlags(int displayId) {
-        if (displayId != DEFAULT_DISPLAY) {
+        if (displayId != mDisplayTracker.getDefaultDisplayId()) {
             // Ignore non-default displays for now
             Log.w(TAG, "Ignoring flag update for display: " + displayId, new Throwable());
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
index 1324d2c..c4a1ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
@@ -19,7 +19,6 @@
 import android.view.WindowManagerGlobal
 import com.android.app.motiontool.DdmHandleMotionTool
 import com.android.app.motiontool.MotionToolManager
-import com.android.app.viewcapture.ViewCapture
 import com.android.systemui.CoreStartable
 import dagger.Binds
 import dagger.Module
@@ -38,17 +37,12 @@
         }
 
         @Provides
-        fun provideMotionToolManager(
-            viewCapture: ViewCapture,
-            windowManagerGlobal: WindowManagerGlobal
-        ): MotionToolManager {
-            return MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+        fun provideMotionToolManager(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+            return MotionToolManager.getInstance(windowManagerGlobal)
         }
 
         @Provides
         fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance()
-
-        @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance()
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index a92203c..1121e160 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -297,7 +297,7 @@
 
     private void updateAssistantAvailability() {
         boolean assistantAvailableForUser = mAssistManagerLazy.get()
-                .getAssistInfoForUser(UserHandle.USER_CURRENT) != null;
+                .getAssistInfoForUser(mUserTracker.getUserId()) != null;
         boolean longPressDefault = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault);
         mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 26c1083..888918e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.navigationbar;
 
+import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
 import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
 import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
@@ -24,7 +25,6 @@
 import static android.app.StatusBarManager.WindowVisibleState;
 import static android.app.StatusBarManager.windowStateToString;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.containsType;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
@@ -44,6 +44,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
 import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
@@ -70,7 +71,6 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.telecom.TelecomManager;
 import android.text.TextUtils;
@@ -129,6 +129,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
@@ -136,9 +137,10 @@
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.rotation.RotationButton;
 import com.android.systemui.shared.rotation.RotationButtonController;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
@@ -216,6 +218,7 @@
     private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
     private final UserContextProvider mUserContextProvider;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
+    private final DisplayTracker mDisplayTracker;
     private final RegionSamplingHelper mRegionSamplingHelper;
     private final int mNavColorSampleMargin;
     private NavigationBarFrame mFrame;
@@ -252,6 +255,7 @@
     private final AutoHideController.Factory mAutoHideControllerFactory;
     private final Optional<TelecomManager> mTelecomManagerOptional;
     private final InputMethodManager mInputMethodManager;
+    private final TaskStackChangeListeners mTaskStackChangeListeners;
 
     @VisibleForTesting
     public int mDisplayId;
@@ -458,7 +462,8 @@
                 @Override
                 public void onStartedWakingUp() {
                     notifyScreenStateChanged(true);
-                    if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) {
+                    if (isGesturalModeOnDefaultDisplay(getContext(), mDisplayTracker,
+                            mNavBarMode)) {
                         mRegionSamplingHelper.start(mSamplingBounds);
                     }
                 }
@@ -488,6 +493,18 @@
             }
     };
 
+    private boolean mScreenPinningActive = false;
+    private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+        @Override
+        public void onLockTaskModeChanged(int mode) {
+            mScreenPinningActive = (mode == LOCK_TASK_MODE_PINNED);
+            mSysUiFlagsContainer.setFlag(SYSUI_STATE_SCREEN_PINNING, mScreenPinningActive)
+                    .commitUpdate(mDisplayId);
+            mView.setInScreenPinning(mScreenPinningActive);
+            updateScreenPinningGestures();
+        }
+    };
+
     @Inject
     NavigationBar(
             NavigationBarView navigationBarView,
@@ -529,7 +546,9 @@
             EdgeBackGestureHandler edgeBackGestureHandler,
             Optional<BackAnimation> backAnimation,
             UserContextProvider userContextProvider,
-            WakefulnessLifecycle wakefulnessLifecycle) {
+            WakefulnessLifecycle wakefulnessLifecycle,
+            TaskStackChangeListeners taskStackChangeListeners,
+            DisplayTracker displayTracker) {
         super(navigationBarView);
         mFrame = navigationBarFrame;
         mContext = context;
@@ -568,6 +587,8 @@
         mInputMethodManager = inputMethodManager;
         mUserContextProvider = userContextProvider;
         mWakefulnessLifecycle = wakefulnessLifecycle;
+        mTaskStackChangeListeners = taskStackChangeListeners;
+        mDisplayTracker = displayTracker;
 
         mNavColorSampleMargin = getResources()
                 .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin);
@@ -615,12 +636,14 @@
 
                     @Override
                     public boolean isSamplingEnabled() {
-                        return isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode);
+                        return isGesturalModeOnDefaultDisplay(getContext(), mDisplayTracker,
+                                mNavBarMode);
                     }
                 }, mainExecutor, bgExecutor);
 
         mView.setBackgroundExecutor(bgExecutor);
         mView.setEdgeBackGestureHandler(mEdgeBackGestureHandler);
+        mView.setDisplayTracker(mDisplayTracker);
         mNavBarMode = mNavigationModeController.addListener(mModeChangedListener);
     }
 
@@ -648,7 +671,7 @@
                 getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
                         .getRotation()));
         mDisplayId = mContext.getDisplayId();
-        mIsOnDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
+        mIsOnDefaultDisplay = mDisplayId == mDisplayTracker.getDefaultDisplayId();
 
         // Ensure we try to get currentSysuiState from navBarHelper before command queue callbacks
         // start firing, since the latter is source of truth
@@ -676,6 +699,7 @@
         mCommandQueue.recomputeDisableFlags(mDisplayId, false);
 
         mNotificationShadeDepthController.addListener(mDepthListener);
+        mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
     }
 
     public void destroyView() {
@@ -689,6 +713,7 @@
         mNotificationShadeDepthController.removeListener(mDepthListener);
 
         mDeviceConfigProxy.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
+        mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener);
     }
 
     @Override
@@ -990,11 +1015,15 @@
         pw.println("  mTransientShown=" + mTransientShown);
         pw.println("  mTransientShownFromGestureOnSystemBar="
                 + mTransientShownFromGestureOnSystemBar);
+        pw.println("  mScreenPinningActive=" + mScreenPinningActive);
         dumpBarTransitions(pw, "mNavigationBarView", getBarTransitions());
 
         pw.println("  mOrientedHandleSamplingRegion: " + mOrientedHandleSamplingRegion);
         mView.dump(pw);
         mRegionSamplingHelper.dump(pw);
+        if (mAutoHideController != null) {
+            mAutoHideController.dump(pw);
+        }
     }
 
     // ----- CommandQueue Callbacks -----
@@ -1213,10 +1242,9 @@
 
     private void updateScreenPinningGestures() {
         // Change the cancel pin gesture to home and back if recents button is invisible
-        boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
         ButtonDispatcher backButton = mView.getBackButton();
         ButtonDispatcher recentsButton = mView.getRecentsButton();
-        if (pinningActive) {
+        if (mScreenPinningActive) {
             boolean recentsVisible = mView.isRecentsButtonVisible();
             backButton.setOnLongClickListener(recentsVisible
                     ? this::onLongPressBackRecents
@@ -1227,8 +1255,8 @@
             recentsButton.setOnLongClickListener(null);
         }
         // Note, this needs to be set after even if we're setting the listener to null
-        backButton.setLongClickable(pinningActive);
-        recentsButton.setLongClickable(pinningActive);
+        backButton.setLongClickable(mScreenPinningActive);
+        recentsButton.setLongClickable(mScreenPinningActive);
     }
 
     private void notifyNavigationBarScreenOn() {
@@ -1311,8 +1339,7 @@
 
     @VisibleForTesting
     boolean onHomeLongClick(View v) {
-        if (!mView.isRecentsButtonVisible()
-                && ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
+        if (!mView.isRecentsButtonVisible() && mScreenPinningActive) {
             return onLongPressBackHome(v);
         }
         if (shouldDisableNavbarGestures()) {
@@ -1447,7 +1474,7 @@
     private void onAccessibilityClick(View v) {
         final Display display = v.getDisplay();
         mAccessibilityManager.notifyAccessibilityButtonClicked(
-                display != null ? display.getDisplayId() : DEFAULT_DISPLAY);
+                display != null ? display.getDisplayId() : mDisplayTracker.getDefaultDisplayId());
     }
 
     private boolean onAccessibilityLongClick(View v) {
@@ -1455,7 +1482,7 @@
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         final String chooserClassName = AccessibilityButtonChooserActivity.class.getName();
         intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
-        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+        mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
         return true;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 8914552..8c19111 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -19,12 +19,10 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
-import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
 import static com.android.systemui.shared.recents.utilities.Utilities.isTablet;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
@@ -56,14 +54,16 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.settings.SecureSettings;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.pip.Pip;
 
@@ -86,9 +86,10 @@
     private final Handler mHandler;
     private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
     private FeatureFlags mFeatureFlags;
+    private final SecureSettings mSecureSettings;
+    private final DisplayTracker mDisplayTracker;
     private final DisplayManager mDisplayManager;
     private final TaskbarDelegate mTaskbarDelegate;
-    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private int mNavMode;
     @VisibleForTesting boolean mIsTablet;
 
@@ -112,28 +113,31 @@
             NavBarHelper navBarHelper,
             TaskbarDelegate taskbarDelegate,
             NavigationBarComponent.Factory navigationBarComponentFactory,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             DumpManager dumpManager,
             AutoHideController autoHideController,
             LightBarController lightBarController,
+            TaskStackChangeListeners taskStackChangeListeners,
             Optional<Pip> pipOptional,
             Optional<BackAnimation> backAnimation,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            SecureSettings secureSettings,
+            DisplayTracker displayTracker) {
         mContext = context;
         mHandler = mainHandler;
         mNavigationBarComponentFactory = navigationBarComponentFactory;
         mFeatureFlags = featureFlags;
+        mSecureSettings = secureSettings;
+        mDisplayTracker = displayTracker;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         commandQueue.addCallback(this);
         configurationController.addCallback(this);
         mConfigChanges.applyNewConfig(mContext.getResources());
         mNavMode = navigationModeController.addListener(this);
         mTaskbarDelegate = taskbarDelegate;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService,
                 navBarHelper, navigationModeController, sysUiFlagsContainer,
                 dumpManager, autoHideController, lightBarController, pipOptional,
-                backAnimation.orElse(null));
+                backAnimation.orElse(null), taskStackChangeListeners);
         mIsTablet = isTablet(mContext);
         dumpManager.registerDumpable(this);
     }
@@ -193,8 +197,7 @@
     }
 
     private void updateAccessibilityButtonModeIfNeeded() {
-        ContentResolver contentResolver = mContext.getContentResolver();
-        final int mode = Settings.Secure.getIntForUser(contentResolver,
+        final int mode = mSecureSettings.getIntForUser(
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
                 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
 
@@ -208,14 +211,14 @@
         // force update to ACCESSIBILITY_BUTTON_MODE_GESTURE.
         if (QuickStepContract.isGesturalMode(mNavMode)
                 && mode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR) {
-            Settings.Secure.putIntForUser(contentResolver,
+            mSecureSettings.putIntForUser(
                     Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_GESTURE,
                     UserHandle.USER_CURRENT);
             // ACCESSIBILITY_BUTTON_MODE_GESTURE is incompatible under non gestural mode. Need to
             // force update to ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR.
         } else if (!QuickStepContract.isGesturalMode(mNavMode)
                 && mode == ACCESSIBILITY_BUTTON_MODE_GESTURE) {
-            Settings.Secure.putIntForUser(contentResolver,
+            mSecureSettings.putIntForUser(
                     Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
                     ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
         }
@@ -296,9 +299,10 @@
         // Don't need to create nav bar on the default display if we initialize TaskBar.
         final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
                 && !initializeTaskbarIfNecessary();
-        Display[] displays = mDisplayManager.getDisplays();
+        Display[] displays = mDisplayTracker.getAllDisplays();
         for (Display display : displays) {
-            if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) {
+            if (shouldCreateDefaultNavbar
+                    || display.getDisplayId() != mDisplayTracker.getDefaultDisplayId()) {
                 createNavigationBar(display, null /* savedState */, result);
             }
         }
@@ -317,7 +321,7 @@
         }
 
         final int displayId = display.getDisplayId();
-        final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;
+        final boolean isOnDefaultDisplay = displayId == mDisplayTracker.getDefaultDisplayId();
 
         // We may show TaskBar on the default display for large screen device. Don't need to create
         // navigation bar for this case.
@@ -412,7 +416,7 @@
 
     /** @return {@link NavigationBarView} on the default display. */
     public @Nullable NavigationBarView getDefaultNavigationBarView() {
-        return getNavigationBarView(DEFAULT_DISPLAY);
+        return getNavigationBarView(mDisplayTracker.getDefaultDisplayId());
     }
 
     /**
@@ -433,7 +437,8 @@
         final NavigationBarView navBarView = getNavigationBarView(displayId);
         if (navBarView != null) {
             navBarView.showPinningEnterExitToast(entering);
-        } else if (displayId == DEFAULT_DISPLAY && mTaskbarDelegate.isInitialized()) {
+        } else if (displayId == mDisplayTracker.getDefaultDisplayId()
+                && mTaskbarDelegate.isInitialized()) {
             mTaskbarDelegate.showPinningEnterExitToast(entering);
         }
     }
@@ -442,7 +447,8 @@
         final NavigationBarView navBarView = getNavigationBarView(displayId);
         if (navBarView != null) {
             navBarView.showPinningEscapeToast();
-        } else if (displayId == DEFAULT_DISPLAY && mTaskbarDelegate.isInitialized()) {
+        } else if (displayId == mDisplayTracker.getDefaultDisplayId()
+                && mTaskbarDelegate.isInitialized()) {
             mTaskbarDelegate.showPinningEscapeToast();
         }
     }
@@ -459,7 +465,7 @@
     /** @return {@link NavigationBar} on the default display. */
     @Nullable
     public NavigationBar getDefaultNavigationBar() {
-        return mNavigationBars.get(DEFAULT_DISPLAY);
+        return mNavigationBars.get(mDisplayTracker.getDefaultDisplayId());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
index 6793f01..a4de9ff 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
@@ -24,7 +24,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.util.SparseArray;
-import android.view.Display;
 import android.view.IWallpaperVisibilityListener;
 import android.view.IWindowManager;
 import android.view.View;
@@ -32,6 +31,7 @@
 import com.android.systemui.R;
 import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
 import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController;
 
@@ -65,6 +65,7 @@
     @org.jetbrains.annotations.NotNull
     private final IWindowManager mWindowManagerService;
     private final LightBarTransitionsController mLightTransitionsController;
+    private final DisplayTracker mDisplayTracker;
     private final boolean mAllowAutoDimWallpaperNotVisible;
     private boolean mWallpaperVisible;
 
@@ -89,18 +90,20 @@
     public NavigationBarTransitions(
             NavigationBarView view,
             IWindowManager windowManagerService,
-            LightBarTransitionsController.Factory lightBarTransitionsControllerFactory) {
+            LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
+            DisplayTracker displayTracker) {
         super(view, R.drawable.nav_background);
         mView = view;
         mWindowManagerService = windowManagerService;
         mLightTransitionsController = lightBarTransitionsControllerFactory.create(this);
+        mDisplayTracker = displayTracker;
         mAllowAutoDimWallpaperNotVisible = view.getContext().getResources()
                 .getBoolean(R.bool.config_navigation_bar_enable_auto_dim_no_visible_wallpaper);
         mDarkIntensityListeners = new ArrayList();
 
         try {
             mWallpaperVisible = mWindowManagerService.registerWallpaperVisibilityListener(
-                    mWallpaperVisibilityListener, Display.DEFAULT_DISPLAY);
+                    mWallpaperVisibilityListener, mDisplayTracker.getDefaultDisplayId());
         } catch (RemoteException e) {
         }
         mView.addOnLayoutChangeListener(
@@ -126,7 +129,7 @@
     public void destroy() {
         try {
             mWindowManagerService.unregisterWallpaperVisibilityListener(mWallpaperVisibilityListener,
-                    Display.DEFAULT_DISPLAY);
+                    mDisplayTracker.getDefaultDisplayId());
         } catch (RemoteException e) {
         }
         mLightTransitionsController.destroy();
@@ -135,7 +138,10 @@
     @Override
     public void setAutoDim(boolean autoDim) {
         // Ensure we aren't in gestural nav if we are triggering auto dim
-        if (autoDim && isGesturalModeOnDefaultDisplay(mView.getContext(), mNavBarMode)) return;
+        if (autoDim && isGesturalModeOnDefaultDisplay(mView.getContext(), mDisplayTracker,
+                mNavBarMode)) {
+            return;
+        }
         if (mAutoDim == autoDim) return;
         mAutoDim = autoDim;
         applyLightsOut(true, false);
@@ -219,7 +225,7 @@
 
     @Override
     public int getTintAnimationDuration() {
-        if (isGesturalModeOnDefaultDisplay(mView.getContext(), mNavBarMode)) {
+        if (isGesturalModeOnDefaultDisplay(mView.getContext(), mDisplayTracker, mNavBarMode)) {
             return Math.max(DEFAULT_COLOR_ADAPT_TRANSITION_TIME, MIN_COLOR_ADAPT_TRANSITION_TIME);
         }
         return LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
index 403d276..63fb499 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
@@ -21,7 +21,6 @@
 
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SEARCH_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
 
@@ -74,11 +73,11 @@
 import com.android.systemui.navigationbar.buttons.RotationContextButton;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton.RotationButtonUpdatesCallback;
 import com.android.systemui.shared.rotation.RotationButtonController;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -125,6 +124,7 @@
     private int mDarkIconColor;
 
     private EdgeBackGestureHandler mEdgeBackGestureHandler;
+    private DisplayTracker mDisplayTracker;
     private final DeadZone mDeadZone;
     private NavigationBarTransitions mBarTransitions;
     @Nullable
@@ -160,6 +160,7 @@
      * fully locked mode we only show that unlocking is blocked.
      */
     private ScreenPinningNotify mScreenPinningNotify;
+    private boolean mScreenPinningActive = false;
 
     /**
      * {@code true} if the IME can render the back button and the IME switcher button.
@@ -300,7 +301,8 @@
                 R.dimen.floating_rotation_button_taskbar_left_margin,
                 R.dimen.floating_rotation_button_taskbar_bottom_margin,
                 R.dimen.floating_rotation_button_diameter,
-                R.dimen.key_button_ripple_max_width);
+                R.dimen.key_button_ripple_max_width,
+                R.bool.floating_rotation_button_position_left);
         mRotationButtonController = new RotationButtonController(mLightContext, mLightIconColor,
                 mDarkIconColor, R.drawable.ic_sysbar_rotate_button_ccw_start_0,
                 R.drawable.ic_sysbar_rotate_button_ccw_start_90,
@@ -358,6 +360,10 @@
         mBgExecutor = bgExecutor;
     }
 
+    public void setDisplayTracker(DisplayTracker displayTracker) {
+        mDisplayTracker = displayTracker;
+    }
+
     public void setTouchHandler(Gefingerpoken touchHandler) {
         mTouchHandler = touchHandler;
     }
@@ -555,7 +561,8 @@
     }
 
     public void setBehavior(@Behavior int behavior) {
-        mRotationButtonController.onBehaviorChanged(Display.DEFAULT_DISPLAY, behavior);
+        mRotationButtonController.onBehaviorChanged(mDisplayTracker.getDefaultDisplayId(),
+                behavior);
     }
 
     @Override
@@ -636,14 +643,13 @@
         // When screen pinning, don't hide back and home when connected service or back and
         // recents buttons when disconnected from launcher service in screen pinning mode,
         // as they are used for exiting.
-        final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
         if (mOverviewProxyEnabled) {
             // Force disable recents when not in legacy mode
             disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode);
-            if (pinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) {
+            if (mScreenPinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) {
                 disableBack = disableHome = false;
             }
-        } else if (pinningActive) {
+        } else if (mScreenPinningActive) {
             disableBack = disableRecent = false;
         }
 
@@ -676,7 +682,7 @@
     @VisibleForTesting
     boolean isRecentsButtonDisabled() {
         return mUseCarModeUi || !isOverviewEnabled()
-                || getContext().getDisplayId() != Display.DEFAULT_DISPLAY;
+                || getContext().getDisplayId() != mDisplayTracker.getDefaultDisplayId();
     }
 
     private Display getContextDisplay() {
@@ -738,9 +744,7 @@
     public void updateDisabledSystemUiStateFlags(SysUiState sysUiState) {
         int displayId = mContext.getDisplayId();
 
-        sysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING,
-                        ActivityManagerWrapper.getInstance().isScreenPinningActive())
-                .setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
+        sysUiState.setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
                         (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0)
                 .setFlag(SYSUI_STATE_HOME_DISABLED,
                         (mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0)
@@ -749,6 +753,10 @@
                 .commitUpdate(displayId);
     }
 
+    public void setInScreenPinning(boolean active) {
+        mScreenPinningActive = active;
+    }
+
     private void updatePanelSystemUiStateFlags() {
         if (SysUiState.DEBUG) {
             Log.d(TAG, "Updating panel sysui state flags: panelView=" + mPanelView);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 5e26e60..5b02aaf 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.navigationbar;
 
+import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
 import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
 import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
@@ -66,14 +67,16 @@
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.shared.recents.utilities.Utilities;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.pip.Pip;
 
@@ -101,6 +104,7 @@
     private AutoHideController mAutoHideController;
     private LightBarController mLightBarController;
     private LightBarTransitionsController mLightBarTransitionsController;
+    private TaskStackChangeListeners mTaskStackChangeListeners;
     private Optional<Pip> mPipOptional;
     private int mDisplayId;
     private int mNavigationIconHints;
@@ -127,6 +131,14 @@
     private final DisplayManager mDisplayManager;
     private Context mWindowContext;
     private ScreenPinningNotify mScreenPinningNotify;
+    private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+        @Override
+        public void onLockTaskModeChanged(int mode) {
+            mSysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, mode == LOCK_TASK_MODE_PINNED)
+                    .commitUpdate(mDisplayId);
+        }
+    };
+
     private int mNavigationMode = -1;
     private final Consumer<Rect> mPipListener;
 
@@ -156,16 +168,20 @@
 
     private BackAnimation mBackAnimation;
 
+    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Inject
     public TaskbarDelegate(Context context,
             EdgeBackGestureHandler.Factory edgeBackGestureHandlerFactory,
-            LightBarTransitionsController.Factory lightBarTransitionsControllerFactory) {
+            LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
         mLightBarTransitionsControllerFactory = lightBarTransitionsControllerFactory;
         mEdgeBackGestureHandler = edgeBackGestureHandlerFactory.create(context);
 
         mContext = context;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mPipListener = mEdgeBackGestureHandler::setPipStashExclusionBounds;
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mStatusBarKeyguardViewManager.setTaskbarDelegate(this);
     }
 
     public void setDependencies(CommandQueue commandQueue,
@@ -176,7 +192,8 @@
             AutoHideController autoHideController,
             LightBarController lightBarController,
             Optional<Pip> pipOptional,
-            BackAnimation backAnimation) {
+            BackAnimation backAnimation,
+            TaskStackChangeListeners taskStackChangeListeners) {
         // TODO: adding this in the ctor results in a dagger dependency cycle :(
         mCommandQueue = commandQueue;
         mOverviewProxyService = overviewProxyService;
@@ -189,6 +206,7 @@
         mPipOptional = pipOptional;
         mBackAnimation = backAnimation;
         mLightBarTransitionsController = createLightBarTransitionsController();
+        mTaskStackChangeListeners = taskStackChangeListeners;
     }
 
     // Separated into a method to keep setDependencies() clean/readable.
@@ -234,6 +252,7 @@
         mPipOptional.ifPresent(this::addPipExclusionBoundsChangeListener);
         mEdgeBackGestureHandler.setBackAnimation(mBackAnimation);
         mEdgeBackGestureHandler.onConfigurationChanged(mContext.getResources().getConfiguration());
+        mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
         mInitialized = true;
     }
 
@@ -253,6 +272,7 @@
         mLightBarTransitionsController.destroy();
         mLightBarController.setNavigationBar(null);
         mPipOptional.ifPresent(this::removePipExclusionBoundsChangeListener);
+        mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener);
         mInitialized = false;
     }
 
@@ -300,8 +320,6 @@
                 .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible())
                 .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
                         allowSystemGestureIgnoringBarVisibility())
-                .setFlag(SYSUI_STATE_SCREEN_PINNING,
-                        ActivityManagerWrapper.getInstance().isScreenPinningActive())
                 .setFlag(SYSUI_STATE_IMMERSIVE_MODE, isImmersiveMode())
                 .commitUpdate(mDisplayId);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
index 2822435..f335733 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -1,14 +1,20 @@
 package com.android.systemui.navigationbar.gestural
 
 import android.content.Context
+import android.content.res.Configuration
+import android.content.res.TypedArray
 import android.graphics.Canvas
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.RectF
+import android.util.MathUtils.min
+import android.util.TypedValue
 import android.view.View
+import androidx.appcompat.view.ContextThemeWrapper
 import androidx.dynamicanimation.animation.FloatPropertyCompat
 import androidx.dynamicanimation.animation.SpringAnimation
 import androidx.dynamicanimation.animation.SpringForce
+import com.android.internal.R.style.Theme_DeviceDefault
 import com.android.internal.util.LatencyTracker
 import com.android.settingslib.Utils
 import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
@@ -16,7 +22,10 @@
 private const val TAG = "BackPanel"
 private const val DEBUG = false
 
-class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
+class BackPanel(
+        context: Context,
+        private val latencyTracker: LatencyTracker
+) : View(context) {
 
     var arrowsPointLeft = false
         set(value) {
@@ -45,52 +54,54 @@
     /**
      * The length of the arrow measured horizontally. Used for animating [arrowPath]
      */
-    private var arrowLength = AnimatedFloat("arrowLength", SpringForce())
+    private var arrowLength = AnimatedFloat(
+            name = "arrowLength",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS
+    )
 
     /**
      * The height of the arrow measured vertically from its center to its top (i.e. half the total
      * height). Used for animating [arrowPath]
      */
-    private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce())
-
-    private val backgroundWidth = AnimatedFloat(
-        name = "backgroundWidth",
-        SpringForce().apply {
-            stiffness = 600f
-            dampingRatio = 0.65f
-        }
+    var arrowHeight = AnimatedFloat(
+            name = "arrowHeight",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
     )
 
-    private val backgroundHeight = AnimatedFloat(
-        name = "backgroundHeight",
-        SpringForce().apply {
-            stiffness = 600f
-            dampingRatio = 0.65f
-        }
+    val backgroundWidth = AnimatedFloat(
+            name = "backgroundWidth",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+            minimumValue = 0f,
+    )
+
+    val backgroundHeight = AnimatedFloat(
+            name = "backgroundHeight",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+            minimumValue = 0f,
     )
 
     /**
      * Corners of the background closer to the edge of the screen (where the arrow appeared from).
      * Used for animating [arrowBackgroundRect]
      */
-    private val backgroundEdgeCornerRadius = AnimatedFloat(
-        name = "backgroundEdgeCornerRadius",
-        SpringForce().apply {
-            stiffness = 400f
-            dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
-        }
-    )
+    val backgroundEdgeCornerRadius = AnimatedFloat("backgroundEdgeCornerRadius")
 
     /**
      * Corners of the background further from the edge of the screens (toward the direction the
      * arrow is being dragged). Used for animating [arrowBackgroundRect]
      */
-    private val backgroundFarCornerRadius = AnimatedFloat(
-        name = "backgroundDragCornerRadius",
-        SpringForce().apply {
-            stiffness = 2200f
-            dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
-        }
+    val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
+
+    var scale = AnimatedFloat(
+            name = "scale",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
+            minimumValue = 0f
+    )
+
+    val scalePivotX = AnimatedFloat(
+            name = "scalePivotX",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+            minimumValue = backgroundWidth.pos / 2,
     )
 
     /**
@@ -98,34 +109,40 @@
      * background's margin relative to the screen edge. The arrow will be centered within the
      * background.
      */
-    private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce())
+    var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
 
-    private val currentAlpha: FloatPropertyCompat<BackPanel> =
-        object : FloatPropertyCompat<BackPanel>("currentAlpha") {
-            override fun setValue(panel: BackPanel, value: Float) {
-                panel.alpha = value
-            }
+    var arrowAlpha = AnimatedFloat(
+            name = "arrowAlpha",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+            minimumValue = 0f,
+            maximumValue = 1f
+    )
 
-            override fun getValue(panel: BackPanel): Float = panel.alpha
-        }
+    val backgroundAlpha = AnimatedFloat(
+            name = "backgroundAlpha",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+            minimumValue = 0f,
+            maximumValue = 1f
+    )
 
-    private val alphaAnimation = SpringAnimation(this, currentAlpha)
-        .setSpring(
-            SpringForce()
-                .setStiffness(60f)
-                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
-        )
+    private val allAnimatedFloat = setOf(
+            arrowLength,
+            arrowHeight,
+            backgroundWidth,
+            backgroundEdgeCornerRadius,
+            backgroundFarCornerRadius,
+            scalePivotX,
+            scale,
+            horizontalTranslation,
+            arrowAlpha,
+            backgroundAlpha
+    )
 
     /**
      * Canvas vertical translation. How far up/down the arrow and background appear relative to the
      * canvas.
      */
-    private var verticalTranslation: AnimatedFloat = AnimatedFloat(
-        name = "verticalTranslation",
-        SpringForce().apply {
-            stiffness = SpringForce.STIFFNESS_MEDIUM
-        }
-    )
+    var verticalTranslation = AnimatedFloat("verticalTranslation")
 
     /**
      * Use for drawing debug info. Can only be set if [DEBUG]=true
@@ -136,28 +153,67 @@
         }
 
     internal fun updateArrowPaint(arrowThickness: Float) {
-        // Arrow constants
+
         arrowPaint.strokeWidth = arrowThickness
 
-        arrowPaint.color =
-            Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
-        arrowBackgroundPaint.color = Utils.getColorAccentDefaultColor(context)
+        val isDeviceInNightTheme = resources.configuration.uiMode and
+                Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+
+        val colorControlActivated = ContextThemeWrapper(context, Theme_DeviceDefault)
+                .run {
+                    val typedValue = TypedValue()
+                    val a: TypedArray = obtainStyledAttributes(typedValue.data,
+                            intArrayOf(android.R.attr.colorControlActivated))
+                    val color = a.getColor(0, 0)
+                    a.recycle()
+                    color
+                }
+
+        val colorPrimary =
+                Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+
+        arrowPaint.color = Utils.getColorAccentDefaultColor(context)
+
+        arrowBackgroundPaint.color = if (isDeviceInNightTheme) {
+            colorPrimary
+        } else {
+            colorControlActivated
+        }
     }
 
-    private inner class AnimatedFloat(name: String, springForce: SpringForce) {
+    inner class AnimatedFloat(
+            name: String,
+            private val minimumVisibleChange: Float? = null,
+            private val minimumValue: Float? = null,
+            private val maximumValue: Float? = null,
+    ) {
+
         // The resting position when not stretched by a touch drag
         private var restingPosition = 0f
 
         // The current position as updated by the SpringAnimation
         var pos = 0f
-            set(v) {
+            private set(v) {
                 if (field != v) {
                     field = v
                     invalidate()
                 }
             }
 
-        val animation: SpringAnimation
+        private val animation: SpringAnimation
+        var spring: SpringForce
+            get() = animation.spring
+            set(value) {
+                animation.cancel()
+                animation.spring = value
+            }
+
+        val isRunning: Boolean
+            get() = animation.isRunning
+
+        fun addEndListener(listener: DelayedOnAnimationEndListener) {
+            animation.addEndListener(listener)
+        }
 
         init {
             val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) {
@@ -167,8 +223,12 @@
 
                 override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
             }
-            animation = SpringAnimation(this, floatProp)
-            animation.spring = springForce
+            animation = SpringAnimation(this, floatProp).apply {
+                spring = SpringForce()
+                this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
+                this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
+                this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
+            }
         }
 
         fun snapTo(newPosition: Float) {
@@ -178,8 +238,24 @@
             pos = newPosition
         }
 
-        fun stretchTo(stretchAmount: Float) {
-            animation.animateToFinalPosition(restingPosition + stretchAmount)
+        fun snapToRestingPosition() {
+            snapTo(restingPosition)
+        }
+
+
+        fun stretchTo(
+                stretchAmount: Float,
+                startingVelocity: Float? = null,
+                springForce: SpringForce? = null
+        ) {
+            animation.apply {
+                startingVelocity?.let {
+                    cancel()
+                    setStartVelocity(it)
+                }
+                springForce?.let { spring = springForce }
+                animateToFinalPosition(restingPosition + stretchAmount)
+            }
         }
 
         /**
@@ -188,18 +264,23 @@
          *
          * The [restingPosition] will remain unchanged. Only the animation is updated.
          */
-        fun stretchBy(finalPosition: Float, amount: Float) {
-            val stretchedAmount = amount * (finalPosition - restingPosition)
+        fun stretchBy(finalPosition: Float?, amount: Float) {
+            val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition)
             animation.animateToFinalPosition(restingPosition + stretchedAmount)
         }
 
-        fun updateRestingPosition(pos: Float, animated: Boolean) {
+        fun updateRestingPosition(pos: Float?, animated: Boolean = true) {
+            if (pos == null) return
+
             restingPosition = pos
-            if (animated)
+            if (animated) {
                 animation.animateToFinalPosition(restingPosition)
-            else
+            } else {
                 snapTo(restingPosition)
+            }
         }
+
+        fun cancel() = animation.cancel()
     }
 
     init {
@@ -224,126 +305,203 @@
         return arrowPath
     }
 
-    fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean {
-        return if (alphaAnimation.isRunning) {
-            alphaAnimation.addEndListener(endListener)
-            true
-        } else if (horizontalTranslation.animation.isRunning) {
-            horizontalTranslation.animation.addEndListener(endListener)
+    fun addAnimationEndListener(
+            animatedFloat: AnimatedFloat,
+            endListener: DelayedOnAnimationEndListener
+    ): Boolean {
+        return if (animatedFloat.isRunning) {
+            animatedFloat.addEndListener(endListener)
             true
         } else {
-            endListener.runNow()
+            endListener.run()
             false
         }
     }
 
+    fun cancelAnimations() {
+        allAnimatedFloat.forEach { it.cancel() }
+    }
+
     fun setStretch(
-        horizontalTranslationStretchAmount: Float,
-        arrowStretchAmount: Float,
-        backgroundWidthStretchAmount: Float,
-        fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
+            horizontalTranslationStretchAmount: Float,
+            arrowStretchAmount: Float,
+            arrowAlphaStretchAmount: Float,
+            backgroundAlphaStretchAmount: Float,
+            backgroundWidthStretchAmount: Float,
+            backgroundHeightStretchAmount: Float,
+            edgeCornerStretchAmount: Float,
+            farCornerStretchAmount: Float,
+            fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
     ) {
         horizontalTranslation.stretchBy(
-            finalPosition = fullyStretchedDimens.horizontalTranslation,
-            amount = horizontalTranslationStretchAmount
+                finalPosition = fullyStretchedDimens.horizontalTranslation,
+                amount = horizontalTranslationStretchAmount
         )
         arrowLength.stretchBy(
-            finalPosition = fullyStretchedDimens.arrowDimens.length,
-            amount = arrowStretchAmount
+                finalPosition = fullyStretchedDimens.arrowDimens.length,
+                amount = arrowStretchAmount
         )
         arrowHeight.stretchBy(
-            finalPosition = fullyStretchedDimens.arrowDimens.height,
-            amount = arrowStretchAmount
+                finalPosition = fullyStretchedDimens.arrowDimens.height,
+                amount = arrowStretchAmount
+        )
+        arrowAlpha.stretchBy(
+                finalPosition = fullyStretchedDimens.arrowDimens.alpha,
+                amount = arrowAlphaStretchAmount
+        )
+        backgroundAlpha.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
+                amount = backgroundAlphaStretchAmount
         )
         backgroundWidth.stretchBy(
-            finalPosition = fullyStretchedDimens.backgroundDimens.width,
-            amount = backgroundWidthStretchAmount
+                finalPosition = fullyStretchedDimens.backgroundDimens.width,
+                amount = backgroundWidthStretchAmount
+        )
+        backgroundHeight.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.height,
+                amount = backgroundHeightStretchAmount
+        )
+        backgroundEdgeCornerRadius.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
+                amount = edgeCornerStretchAmount
+        )
+        backgroundFarCornerRadius.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
+                amount = farCornerStretchAmount
         )
     }
 
+    fun popOffEdge(startingVelocity: Float) {
+        val heightStretchAmount = startingVelocity * 50
+        val widthStretchAmount = startingVelocity * 150
+        val scaleStretchAmount = startingVelocity * 0.8f
+        backgroundHeight.stretchTo(stretchAmount = 0f, startingVelocity = -heightStretchAmount)
+        backgroundWidth.stretchTo(stretchAmount = 0f, startingVelocity = widthStretchAmount)
+        scale.stretchTo(stretchAmount = 0f, startingVelocity = -scaleStretchAmount)
+    }
+
+    fun popScale(startingVelocity: Float) {
+        scalePivotX.snapTo(backgroundWidth.pos / 2)
+        scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity)
+    }
+
+    fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
+        arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity,
+                springForce = springForce)
+    }
+
     fun resetStretch() {
-        horizontalTranslation.stretchTo(0f)
-        arrowLength.stretchTo(0f)
-        arrowHeight.stretchTo(0f)
-        backgroundWidth.stretchTo(0f)
-        backgroundHeight.stretchTo(0f)
-        backgroundEdgeCornerRadius.stretchTo(0f)
-        backgroundFarCornerRadius.stretchTo(0f)
+        backgroundAlpha.snapTo(1f)
+        verticalTranslation.snapTo(0f)
+        scale.snapTo(1f)
+
+        horizontalTranslation.snapToRestingPosition()
+        arrowLength.snapToRestingPosition()
+        arrowHeight.snapToRestingPosition()
+        arrowAlpha.snapToRestingPosition()
+        backgroundWidth.snapToRestingPosition()
+        backgroundHeight.snapToRestingPosition()
+        backgroundEdgeCornerRadius.snapToRestingPosition()
+        backgroundFarCornerRadius.snapToRestingPosition()
     }
 
     /**
      * Updates resting arrow and background size not accounting for stretch
      */
     internal fun setRestingDimens(
-        restingParams: EdgePanelParams.BackIndicatorDimens,
-        animate: Boolean
+            restingParams: EdgePanelParams.BackIndicatorDimens,
+            animate: Boolean = true
     ) {
-        horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation, animate)
+        horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
+        scale.updateRestingPosition(restingParams.scale)
+        arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha)
+        backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha)
+
         arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
         arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
+        scalePivotX.updateRestingPosition(restingParams.backgroundDimens.width, animate)
         backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
         backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
         backgroundEdgeCornerRadius.updateRestingPosition(
-            restingParams.backgroundDimens.edgeCornerRadius,
-            animate
+                restingParams.backgroundDimens.edgeCornerRadius, animate
         )
         backgroundFarCornerRadius.updateRestingPosition(
-            restingParams.backgroundDimens.farCornerRadius,
-            animate
+                restingParams.backgroundDimens.farCornerRadius, animate
         )
     }
 
     fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
 
-    fun setArrowStiffness(arrowStiffness: Float, arrowDampingRatio: Float) {
-        arrowLength.animation.spring.apply {
-            stiffness = arrowStiffness
-            dampingRatio = arrowDampingRatio
-        }
-        arrowHeight.animation.spring.apply {
-            stiffness = arrowStiffness
-            dampingRatio = arrowDampingRatio
-        }
+    fun setSpring(
+            horizontalTranslation: SpringForce? = null,
+            verticalTranslation: SpringForce? = null,
+            scale: SpringForce? = null,
+            arrowLength: SpringForce? = null,
+            arrowHeight: SpringForce? = null,
+            arrowAlpha: SpringForce? = null,
+            backgroundAlpha: SpringForce? = null,
+            backgroundFarCornerRadius: SpringForce? = null,
+            backgroundEdgeCornerRadius: SpringForce? = null,
+            backgroundWidth: SpringForce? = null,
+            backgroundHeight: SpringForce? = null,
+    ) {
+        arrowLength?.let { this.arrowLength.spring = it }
+        arrowHeight?.let { this.arrowHeight.spring = it }
+        arrowAlpha?.let { this.arrowAlpha.spring = it }
+        backgroundAlpha?.let { this.backgroundAlpha.spring = it }
+        backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it }
+        backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it }
+        scale?.let { this.scale.spring = it }
+        backgroundWidth?.let { this.backgroundWidth.spring = it }
+        backgroundHeight?.let { this.backgroundHeight.spring = it }
+        horizontalTranslation?.let { this.horizontalTranslation.spring = it }
+        verticalTranslation?.let { this.verticalTranslation.spring = it }
     }
 
     override fun hasOverlappingRendering() = false
 
     override fun onDraw(canvas: Canvas) {
-        var edgeCorner = backgroundEdgeCornerRadius.pos
+        val edgeCorner = backgroundEdgeCornerRadius.pos
         val farCorner = backgroundFarCornerRadius.pos
         val halfHeight = backgroundHeight.pos / 2
+        val canvasWidth = width
+        val backgroundWidth = backgroundWidth.pos
+        val scalePivotX = scalePivotX.pos
 
         canvas.save()
 
-        if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f)
+        if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f)
 
         canvas.translate(
-            horizontalTranslation.pos,
-            height * 0.5f + verticalTranslation.pos
+                horizontalTranslation.pos,
+                height * 0.5f + verticalTranslation.pos
         )
 
+        canvas.scale(scale.pos, scale.pos, scalePivotX, 0f)
+
         val arrowBackground = arrowBackgroundRect.apply {
             left = 0f
             top = -halfHeight
-            right = backgroundWidth.pos
+            right = backgroundWidth
             bottom = halfHeight
         }.toPathWithRoundCorners(
-            topLeft = edgeCorner,
-            bottomLeft = edgeCorner,
-            topRight = farCorner,
-            bottomRight = farCorner
+                topLeft = edgeCorner,
+                bottomLeft = edgeCorner,
+                topRight = farCorner,
+                bottomRight = farCorner
         )
-        canvas.drawPath(arrowBackground, arrowBackgroundPaint)
+        canvas.drawPath(arrowBackground,
+                arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() })
 
         val dx = arrowLength.pos
         val dy = arrowHeight.pos
 
         // How far the arrow bounding box should be from the edge of the screen. Measured from
         // either the tip or the back of the arrow, whichever is closer
-        var arrowOffset = (backgroundWidth.pos - dx) / 2
+        val arrowOffset = (backgroundWidth - dx) / 2
         canvas.translate(
-            /* dx= */ arrowOffset,
-            /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
+                /* dx= */ arrowOffset,
+                /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
         )
 
         val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
@@ -355,6 +513,8 @@
         }
 
         val arrowPath = calculateArrowPath(dx = dx, dy = dy)
+        val arrowPaint = arrowPaint
+                .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
         canvas.drawPath(arrowPath, arrowPaint)
         canvas.restore()
 
@@ -372,26 +532,17 @@
     }
 
     private fun RectF.toPathWithRoundCorners(
-        topLeft: Float = 0f,
-        topRight: Float = 0f,
-        bottomRight: Float = 0f,
-        bottomLeft: Float = 0f
+            topLeft: Float = 0f,
+            topRight: Float = 0f,
+            bottomRight: Float = 0f,
+            bottomLeft: Float = 0f
     ): Path = Path().apply {
         val corners = floatArrayOf(
-            topLeft, topLeft,
-            topRight, topRight,
-            bottomRight, bottomRight,
-            bottomLeft, bottomLeft
+                topLeft, topLeft,
+                topRight, topRight,
+                bottomRight, bottomRight,
+                bottomLeft, bottomLeft
         )
         addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
     }
-
-    fun cancelAlphaAnimations() {
-        alphaAnimation.cancel()
-        alpha = 1f
-    }
-
-    fun fadeOut() {
-        alphaAnimation.animateToFinalPosition(0f)
-    }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 6e927b0..367d125 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -24,18 +24,16 @@
 import android.os.SystemClock
 import android.os.VibrationEffect
 import android.util.Log
-import android.util.MathUtils.constrain
-import android.util.MathUtils.saturate
+import android.util.MathUtils
 import android.view.Gravity
 import android.view.MotionEvent
 import android.view.VelocityTracker
-import android.view.View
 import android.view.ViewConfiguration
 import android.view.WindowManager
-import android.view.animation.DecelerateInterpolator
-import android.view.animation.PathInterpolator
+import androidx.annotation.VisibleForTesting
+import androidx.core.os.postDelayed
+import androidx.core.view.isVisible
 import androidx.dynamicanimation.animation.DynamicAnimation
-import androidx.dynamicanimation.animation.SpringForce
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.NavigationEdgeBackPlugin
@@ -50,58 +48,42 @@
 import kotlin.math.sign
 
 private const val TAG = "BackPanelController"
-private const val DEBUG = false
-
 private const val ENABLE_FAILSAFE = true
 
-private const val FAILSAFE_DELAY_MS: Long = 350
+private const val PX_PER_SEC = 1000
+private const val PX_PER_MS = 1
 
-/**
- * The time required between the arrow-appears vibration effect and the back-committed vibration
- * effect. If the arrow is flung quickly, the phone only vibrates once. However, if the arrow is
- * held on the screen for a long time, it will vibrate a second time when the back gesture is
- * committed.
- */
-private const val GESTURE_DURATION_FOR_CLICK_MS = 400
+internal const val MIN_DURATION_ACTIVE_ANIMATION = 300L
+private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
+private const val MIN_DURATION_COMMITTED_ANIMATION = 200L
+private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
+private const val MIN_DURATION_CONSIDERED_AS_FLING = 100L
 
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val FLING_MIN_APPEARANCE_DURATION = 235L
+private const val FAILSAFE_DELAY_MS = 350L
+private const val POP_ON_FLING_DELAY = 160L
 
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val MIN_FLING_VELOCITY = 3000
+internal val VIBRATE_ACTIVATED_EFFECT =
+        VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
 
-/**
- * The amount of rubber banding we do for the vertical translation
- */
-private const val RUBBER_BAND_AMOUNT = 15
+internal val VIBRATE_DEACTIVATED_EFFECT =
+        VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
 
-private const val ARROW_APPEAR_STIFFNESS = 600f
-private const val ARROW_APPEAR_DAMPING_RATIO = 0.4f
-private const val ARROW_DISAPPEAR_STIFFNESS = 1200f
-private const val ARROW_DISAPPEAR_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY
+private const val DEBUG = false
 
-/**
- * The interpolator used to rubber band
- */
-private val RUBBER_BAND_INTERPOLATOR = PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f)
-
-private val DECELERATE_INTERPOLATOR = DecelerateInterpolator()
-
-private val DECELERATE_INTERPOLATOR_SLOW = DecelerateInterpolator(0.7f)
-
-class BackPanelController private constructor(
-    context: Context,
-    private val windowManager: WindowManager,
-    private val viewConfiguration: ViewConfiguration,
-    @Main private val mainHandler: Handler,
-    private val vibratorHelper: VibratorHelper,
-    private val configurationController: ConfigurationController,
-    latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
+class BackPanelController internal constructor(
+        context: Context,
+        private val windowManager: WindowManager,
+        private val viewConfiguration: ViewConfiguration,
+        @Main private val mainHandler: Handler,
+        private val vibratorHelper: VibratorHelper,
+        private val configurationController: ConfigurationController,
+        private val latencyTracker: LatencyTracker
+) : ViewController<BackPanel>(
+        BackPanel(
+                context,
+                latencyTracker
+        )
+), NavigationEdgeBackPlugin {
 
     /**
      * Injectable instance to create a new BackPanelController.
@@ -110,44 +92,44 @@
      * BackPanelController, and we need to match EdgeBackGestureHandler's context.
      */
     class Factory @Inject constructor(
-        private val windowManager: WindowManager,
-        private val viewConfiguration: ViewConfiguration,
-        @Main private val mainHandler: Handler,
-        private val vibratorHelper: VibratorHelper,
-        private val configurationController: ConfigurationController,
-        private val latencyTracker: LatencyTracker
+            private val windowManager: WindowManager,
+            private val viewConfiguration: ViewConfiguration,
+            @Main private val mainHandler: Handler,
+            private val vibratorHelper: VibratorHelper,
+            private val configurationController: ConfigurationController,
+            private val latencyTracker: LatencyTracker
     ) {
         /** Construct a [BackPanelController].  */
         fun create(context: Context): BackPanelController {
             val backPanelController = BackPanelController(
-                context,
-                windowManager,
-                viewConfiguration,
-                mainHandler,
-                vibratorHelper,
-                configurationController,
-                latencyTracker
+                    context,
+                    windowManager,
+                    viewConfiguration,
+                    mainHandler,
+                    vibratorHelper,
+                    configurationController,
+                    latencyTracker
             )
             backPanelController.init()
             return backPanelController
         }
     }
 
-    private var params: EdgePanelParams = EdgePanelParams(resources)
-    private var currentState: GestureState = GestureState.GONE
+    @VisibleForTesting
+    internal var params: EdgePanelParams = EdgePanelParams(resources)
+    @VisibleForTesting
+    internal var currentState: GestureState = GestureState.GONE
     private var previousState: GestureState = GestureState.GONE
 
-    // Phone should only vibrate the first time the arrow is activated
-    private var hasHapticPlayed = false
-
     // Screen attributes
     private lateinit var layoutParams: WindowManager.LayoutParams
     private val displaySize = Point()
 
     private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
-
+    private var previousXTranslationOnActiveOffset = 0f
     private var previousXTranslation = 0f
     private var totalTouchDelta = 0f
+    private var touchDeltaStartX = 0f
     private var velocityTracker: VelocityTracker? = null
         set(value) {
             if (field != value) field?.recycle()
@@ -161,8 +143,18 @@
     // The x,y position of the first touch event
     private var startX = 0f
     private var startY = 0f
+    private var startIsLeft: Boolean? = null
 
-    private var gestureStartTime = 0L
+    private var gestureSinceActionDown = 0L
+    private var gestureEntryTime = 0L
+    private var gestureActiveTime = 0L
+    private var gestureInactiveOrEntryTime = 0L
+    private var gestureArrowStrokeVisibleTime = 0L
+
+    private val elapsedTimeSinceActionDown
+        get() = SystemClock.uptimeMillis() - gestureSinceActionDown
+    private val elapsedTimeSinceEntry
+        get() = SystemClock.uptimeMillis() - gestureEntryTime
 
     // Whether the current gesture has moved a sufficiently large amount,
     // so that we can unambiguously start showing the ENTRY animation
@@ -170,7 +162,7 @@
 
     private val failsafeRunnable = Runnable { onFailsafe() }
 
-    private enum class GestureState {
+    internal enum class GestureState {
         /* Arrow is off the screen and invisible */
         GONE,
 
@@ -191,17 +183,6 @@
 
         /* back action currently cancelling, arrow soon to be GONE */
         CANCELLED;
-
-        /**
-         * @return true if the current state responds to touch move events in some way (e.g. by
-         * stretching the back indicator)
-         */
-        fun isInteractive(): Boolean {
-            return when (this) {
-                ENTRY, ACTIVE, INACTIVE -> true
-                GONE, FLUNG, COMMITTED, CANCELLED -> false
-            }
-        }
     }
 
     /**
@@ -209,50 +190,43 @@
      * runnable is not called if the animation is cancelled
      */
     inner class DelayedOnAnimationEndListener internal constructor(
-        private val handler: Handler,
-        private val runnable: Runnable,
-        private val minDuration: Long
+            private val handler: Handler,
+            private val runnableDelay: Long,
+            val runnable: Runnable,
     ) : DynamicAnimation.OnAnimationEndListener {
+
         override fun onAnimationEnd(
-            animation: DynamicAnimation<*>,
-            canceled: Boolean,
-            value: Float,
-            velocity: Float
+                animation: DynamicAnimation<*>,
+                canceled: Boolean,
+                value: Float,
+                velocity: Float
         ) {
             animation.removeEndListener(this)
+
             if (!canceled) {
-                // Total elapsed time of the gesture and the animation
-                val totalElapsedTime = SystemClock.uptimeMillis() - gestureStartTime
+
                 // The delay between finishing this animation and starting the runnable
-                val delay = max(0, minDuration - totalElapsedTime)
+                val delay = max(0, runnableDelay - elapsedTimeSinceEntry)
+
                 handler.postDelayed(runnable, delay)
             }
         }
 
-        internal fun runNow() {
-            runnable.run()
-        }
+        internal fun run() = runnable.run()
     }
 
-    private val setCommittedEndListener =
-        DelayedOnAnimationEndListener(
-            mainHandler,
-            { updateArrowState(GestureState.COMMITTED) },
-            minDuration = FLING_MIN_APPEARANCE_DURATION
-        )
+    private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
+        updateArrowState(GestureState.COMMITTED)
+    }
 
-    private val setGoneEndListener =
-        DelayedOnAnimationEndListener(
-            mainHandler,
-            {
+
+    private val onEndSetGoneStateListener =
+            DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
                 cancelFailsafe()
                 updateArrowState(GestureState.GONE)
-            },
-            minDuration = 0
-        )
+            }
 
-    // Vibration
-    private var vibrationTime: Long = 0
+    private val playAnimationThenSetGoneOnAlphaEnd = Runnable { playAnimationThenSetGoneEnd() }
 
     // Minimum of the screen's width or the predefined threshold
     private var fullyStretchedThreshold = 0f
@@ -279,7 +253,7 @@
         updateConfiguration()
         updateArrowDirection(configurationController.isLayoutRtl)
         updateArrowState(GestureState.GONE, force = true)
-        updateRestingArrowDimens(animated = false, currentState)
+        updateRestingArrowDimens()
         configurationController.addCallback(configurationListener)
     }
 
@@ -296,22 +270,57 @@
         velocityTracker!!.addMovement(event)
         when (event.actionMasked) {
             MotionEvent.ACTION_DOWN -> {
-                resetOnDown()
+                gestureSinceActionDown = SystemClock.uptimeMillis()
+                cancelAllPendingAnimations()
                 startX = event.x
                 startY = event.y
-                gestureStartTime = SystemClock.uptimeMillis()
+
+                updateArrowState(GestureState.GONE)
+                updateYStartPosition(startY)
+
+                // reset animation properties
+                startIsLeft = mView.isLeftPanel
+                hasPassedDragSlop = false
+                mView.resetStretch()
             }
             MotionEvent.ACTION_MOVE -> {
-                // only go to the ENTRY state after some minimum motion has occurred
                 if (dragSlopExceeded(event.x, startX)) {
                     handleMoveEvent(event)
                 }
             }
             MotionEvent.ACTION_UP -> {
-                if (currentState == GestureState.ACTIVE) {
-                    updateArrowState(if (isFlung()) GestureState.FLUNG else GestureState.COMMITTED)
-                } else if (currentState != GestureState.GONE) { // if invisible, skip animation
-                    updateArrowState(GestureState.CANCELLED)
+                when (currentState) {
+                    GestureState.ENTRY -> {
+                        if (isFlungAwayFromEdge(endX = event.x)) {
+                            updateArrowState(GestureState.ACTIVE)
+                            updateArrowState(GestureState.FLUNG)
+                        } else {
+                            updateArrowState(GestureState.CANCELLED)
+                        }
+                    }
+                    GestureState.INACTIVE -> {
+                        if (isFlungAwayFromEdge(endX = event.x)) {
+                            mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) {
+                                updateArrowState(GestureState.ACTIVE)
+                                updateArrowState(GestureState.FLUNG)
+                            }
+                        } else {
+                            updateArrowState(GestureState.CANCELLED)
+                        }
+                    }
+                    GestureState.ACTIVE -> {
+                        if (elapsedTimeSinceEntry < MIN_DURATION_CONSIDERED_AS_FLING) {
+                            updateArrowState(GestureState.FLUNG)
+                        } else {
+                            updateArrowState(GestureState.COMMITTED)
+                        }
+                    }
+                    GestureState.GONE,
+                    GestureState.FLUNG,
+                    GestureState.COMMITTED,
+                    GestureState.CANCELLED -> {
+                        updateArrowState(GestureState.CANCELLED)
+                    }
                 }
                 velocityTracker = null
             }
@@ -325,6 +334,14 @@
         }
     }
 
+    private fun cancelAllPendingAnimations() {
+        cancelFailsafe()
+        mView.cancelAnimations()
+        mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable)
+        mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable)
+        mainHandler.removeCallbacks(playAnimationThenSetGoneOnAlphaEnd)
+    }
+
     /**
      * Returns false until the current gesture exceeds the touch slop threshold,
      * and returns true thereafter (we reset on the subsequent back gesture).
@@ -335,7 +352,7 @@
     private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
         if (hasPassedDragSlop) return true
 
-        if (abs(curX - startX) > viewConfiguration.scaledTouchSlop) {
+        if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) {
             // Reset the arrow to the side
             updateArrowState(GestureState.ENTRY)
 
@@ -348,39 +365,46 @@
     }
 
     private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
-        if (!currentState.isInteractive())
-            return
+
+        val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation
 
         when (currentState) {
-            // Check if we should transition from ENTRY to ACTIVE
-            GestureState.ENTRY ->
-                if (xTranslation > params.swipeTriggerThreshold) {
+            GestureState.ENTRY -> {
+                if (xTranslation > params.staticTriggerThreshold) {
                     updateArrowState(GestureState.ACTIVE)
                 }
+            }
+            GestureState.ACTIVE -> {
+                val isPastDynamicDeactivationThreshold =
+                        totalTouchDelta <= params.deactivationSwipeTriggerThreshold
+                val isMinDurationElapsed =
+                        elapsedTimeSinceActionDown > MIN_DURATION_ACTIVE_ANIMATION
 
-            // Abort if we had continuous motion toward the edge for a while, OR the direction
-            // in Y is bigger than X * 2
-            GestureState.ACTIVE ->
-                if ((totalTouchDelta < 0 && -totalTouchDelta > params.minDeltaForSwitch) ||
-                    (yTranslation > xTranslation * 2)
+                if (isMinDurationElapsed && (!isWithinYActivationThreshold ||
+                                isPastDynamicDeactivationThreshold)
                 ) {
                     updateArrowState(GestureState.INACTIVE)
                 }
+            }
+            GestureState.INACTIVE -> {
+                val isPastStaticThreshold =
+                        xTranslation > params.staticTriggerThreshold
+                val isPastDynamicReactivationThreshold = totalTouchDelta > 0 &&
+                        abs(totalTouchDelta) >=
+                        params.reactivationTriggerThreshold
 
-            //  Re-activate if we had continuous motion away from the edge for a while
-            GestureState.INACTIVE ->
-                if (totalTouchDelta > 0 && totalTouchDelta > params.minDeltaForSwitch) {
+                if (isPastStaticThreshold &&
+                        isPastDynamicReactivationThreshold &&
+                        isWithinYActivationThreshold
+                ) {
                     updateArrowState(GestureState.ACTIVE)
                 }
-
-            // By default assume the current direction is kept
+            }
             else -> {}
         }
     }
 
     private fun handleMoveEvent(event: MotionEvent) {
-        if (!currentState.isInteractive())
-            return
 
         val x = event.x
         val y = event.y
@@ -400,23 +424,44 @@
         previousXTranslation = xTranslation
 
         if (abs(xDelta) > 0) {
-            if (sign(xDelta) == sign(totalTouchDelta)) {
+            val range =
+                params.run { deactivationSwipeTriggerThreshold..reactivationTriggerThreshold }
+            val isTouchInContinuousDirection =
+                    sign(xDelta) == sign(totalTouchDelta) || totalTouchDelta in range
+
+            if (isTouchInContinuousDirection) {
                 // Direction has NOT changed, so keep counting the delta
                 totalTouchDelta += xDelta
             } else {
                 // Direction has changed, so reset the delta
                 totalTouchDelta = xDelta
+                touchDeltaStartX = x
             }
         }
 
         updateArrowStateOnMove(yTranslation, xTranslation)
+
         when (currentState) {
-            GestureState.ACTIVE ->
-                stretchActiveBackIndicator(fullScreenStretchProgress(xTranslation))
-            GestureState.ENTRY ->
-                stretchEntryBackIndicator(preThresholdStretchProgress(xTranslation))
-            GestureState.INACTIVE ->
-                mView.resetStretch()
+            GestureState.ACTIVE -> {
+                stretchActiveBackIndicator(fullScreenProgress(xTranslation))
+            }
+            GestureState.ENTRY -> {
+                val progress = staticThresholdProgress(xTranslation)
+                stretchEntryBackIndicator(progress)
+
+                params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+                    mView.popArrowAlpha(0f, it.value)
+                }
+            }
+            GestureState.INACTIVE -> {
+                val progress = reactivationThresholdProgress(totalTouchDelta)
+                stretchInactiveBackIndicator(progress)
+
+                params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+                    gestureArrowStrokeVisibleTime = SystemClock.uptimeMillis()
+                    mView.popArrowAlpha(0f, it.value)
+                }
+            }
             else -> {}
         }
 
@@ -427,21 +472,22 @@
     private fun setVerticalTranslation(yOffset: Float) {
         val yTranslation = abs(yOffset)
         val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
-        val yProgress = saturate(yTranslation / (maxYOffset * RUBBER_BAND_AMOUNT))
-        mView.animateVertically(
-            RUBBER_BAND_INTERPOLATOR.getInterpolation(yProgress) * maxYOffset *
+        val rubberbandAmount = 15f
+        val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
+        val yPosition = params.translationInterpolator.getInterpolation(yProgress) *
+                maxYOffset *
                 sign(yOffset)
-        )
+        mView.animateVertically(yPosition)
     }
 
     /**
-     * @return the relative position of the drag from the time after the arrow is activated until
+     * Tracks the relative position of the drag from the time after the arrow is activated until
      * the arrow is fully stretched (between 0.0 - 1.0f)
      */
-    private fun fullScreenStretchProgress(xTranslation: Float): Float {
-        return saturate(
-            (xTranslation - params.swipeTriggerThreshold) /
-                (fullyStretchedThreshold - params.swipeTriggerThreshold)
+    private fun fullScreenProgress(xTranslation: Float): Float {
+        return MathUtils.saturate(
+                (xTranslation - previousXTranslationOnActiveOffset) /
+                        (fullyStretchedThreshold - previousXTranslationOnActiveOffset)
         )
     }
 
@@ -449,26 +495,74 @@
      * Tracks the relative position of the drag from the entry until the threshold where the arrow
      * activates (between 0.0 - 1.0f)
      */
-    private fun preThresholdStretchProgress(xTranslation: Float): Float {
-        return saturate(xTranslation / params.swipeTriggerThreshold)
+    private fun staticThresholdProgress(xTranslation: Float): Float {
+        return MathUtils.saturate(xTranslation / params.staticTriggerThreshold)
+    }
+
+    private fun reactivationThresholdProgress(totalTouchDelta: Float): Float {
+        return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold)
     }
 
     private fun stretchActiveBackIndicator(progress: Float) {
-        val rubberBandIterpolation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
         mView.setStretch(
-            horizontalTranslationStretchAmount = rubberBandIterpolation,
-            arrowStretchAmount = rubberBandIterpolation,
-            backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR_SLOW.getInterpolation(progress),
-            params.fullyStretchedIndicator
+                horizontalTranslationStretchAmount = params.translationInterpolator
+                        .getInterpolation(progress),
+                arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+                backgroundWidthStretchAmount = params.activeWidthInterpolator
+                        .getInterpolation(progress),
+                backgroundAlphaStretchAmount = 1f,
+                backgroundHeightStretchAmount = 1f,
+                arrowAlphaStretchAmount = 1f,
+                edgeCornerStretchAmount = 1f,
+                farCornerStretchAmount = 1f,
+                fullyStretchedDimens = params.fullyStretchedIndicator
         )
     }
 
     private fun stretchEntryBackIndicator(progress: Float) {
         mView.setStretch(
-            horizontalTranslationStretchAmount = 0f,
-            arrowStretchAmount = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress),
-            backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR.getInterpolation(progress),
-            params.preThresholdIndicator
+                horizontalTranslationStretchAmount = 0f,
+                arrowStretchAmount = params.arrowAngleInterpolator
+                        .getInterpolation(progress),
+                backgroundWidthStretchAmount = params.entryWidthInterpolator
+                        .getInterpolation(progress),
+                backgroundHeightStretchAmount = params.heightInterpolator
+                        .getInterpolation(progress),
+                backgroundAlphaStretchAmount = 1f,
+                arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+                edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+                farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+                fullyStretchedDimens = params.preThresholdIndicator
+        )
+    }
+
+    private var previousPreThresholdWidthInterpolator = params.entryWidthTowardsEdgeInterpolator
+    fun preThresholdWidthStretchAmount(progress: Float): Float {
+        val interpolator = run {
+            val isPastSlop = abs(totalTouchDelta) > ViewConfiguration.get(context).scaledTouchSlop
+            if (isPastSlop) {
+                if (totalTouchDelta > 0) {
+                    params.entryWidthInterpolator
+                } else params.entryWidthTowardsEdgeInterpolator
+            } else {
+                previousPreThresholdWidthInterpolator
+            }.also { previousPreThresholdWidthInterpolator = it }
+        }
+        return interpolator.getInterpolation(progress).coerceAtLeast(0f)
+    }
+
+    private fun stretchInactiveBackIndicator(progress: Float) {
+        mView.setStretch(
+                horizontalTranslationStretchAmount = 0f,
+                arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+                backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+                backgroundHeightStretchAmount = params.heightInterpolator
+                        .getInterpolation(progress),
+                backgroundAlphaStretchAmount = 1f,
+                arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+                edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+                farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+                fullyStretchedDimens = params.preThresholdIndicator
         )
     }
 
@@ -486,8 +580,7 @@
         }
     }
 
-    override fun setInsets(insetLeft: Int, insetRight: Int) {
-    }
+    override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
 
     override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) {
         backCallback = callback
@@ -498,62 +591,54 @@
         windowManager.addView(mView, layoutParams)
     }
 
-    private fun isFlung() = velocityTracker!!.run {
-        computeCurrentVelocity(1000)
-        abs(xVelocity) > MIN_FLING_VELOCITY
+    private fun isDragAwayFromEdge(velocityPxPerSecThreshold: Int = 0) = velocityTracker!!.run {
+        computeCurrentVelocity(PX_PER_SEC)
+        val velocity = xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+        velocity > velocityPxPerSecThreshold
     }
 
-    private fun playFlingBackAnimation() {
-        playAnimation(setCommittedEndListener)
+    private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
+        val minDistanceConsideredForFling = ViewConfiguration.get(context).scaledTouchSlop
+        val flingDistance = abs(endX - startX)
+        val isPastFlingVelocity = isDragAwayFromEdge(
+                velocityPxPerSecThreshold =
+                ViewConfiguration.get(context).scaledMinimumFlingVelocity)
+        return flingDistance > minDistanceConsideredForFling && isPastFlingVelocity
     }
 
-    private fun playCommitBackAnimation() {
-        // Check if we should vibrate again
-        if (previousState != GestureState.FLUNG) {
-            velocityTracker!!.computeCurrentVelocity(1000)
-            val isSlow = abs(velocityTracker!!.xVelocity) < 500
-            val hasNotVibratedRecently =
-                SystemClock.uptimeMillis() - vibrationTime >= GESTURE_DURATION_FOR_CLICK_MS
-            if (isSlow || hasNotVibratedRecently) {
-                vibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK)
-            }
-        }
-        // Dispatch the actual back trigger
-        if (DEBUG) Log.d(TAG, "playCommitBackAnimation() invoked triggerBack() on backCallback")
-        backCallback.triggerBack()
-
-        playAnimation(setGoneEndListener)
-    }
-
-    private fun playCancelBackAnimation() {
-        backCallback.cancelBack()
-        playAnimation(setGoneEndListener)
-    }
-
-    /**
-     * @return true if the animation is running, false otherwise. Some transitions don't animate
-     */
-    private fun playAnimation(endListener: DelayedOnAnimationEndListener) {
-        updateRestingArrowDimens(animated = true, currentState)
-
-        if (!mView.addEndListener(endListener)) {
+    private fun playHorizontalAnimationThen(onEnd: DelayedOnAnimationEndListener) {
+        updateRestingArrowDimens()
+        if (!mView.addAnimationEndListener(mView.horizontalTranslation, onEnd)) {
             scheduleFailsafe()
         }
     }
 
-    private fun resetOnDown() {
-        hasPassedDragSlop = false
-        hasHapticPlayed = false
-        totalTouchDelta = 0f
-        vibrationTime = 0
-        cancelFailsafe()
+    private fun playAnimationThenSetGoneEnd() {
+        updateRestingArrowDimens()
+        if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+            scheduleFailsafe()
+        }
     }
 
-    private fun updateYPosition(touchY: Float) {
+    private fun playWithBackgroundWidthAnimation(
+            onEnd: DelayedOnAnimationEndListener,
+            delay: Long = 0L
+    ) {
+        if (delay == 0L) {
+            updateRestingArrowDimens()
+            if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) {
+                scheduleFailsafe()
+            }
+        } else {
+            mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) }
+        }
+    }
+
+    private fun updateYStartPosition(touchY: Float) {
         var yPosition = touchY - params.fingerOffset
         yPosition = max(yPosition, params.minArrowYPosition.toFloat())
         yPosition -= layoutParams.height / 2.0f
-        layoutParams.y = constrain(yPosition.toInt(), 0, displaySize.y)
+        layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y)
     }
 
     override fun setDisplaySize(displaySize: Point) {
@@ -564,53 +649,135 @@
     /**
      * Updates resting arrow and background size not accounting for stretch
      */
-    private fun updateRestingArrowDimens(animated: Boolean, currentState: GestureState) {
-        if (animated) {
-            when (currentState) {
-                GestureState.ENTRY, GestureState.ACTIVE, GestureState.FLUNG ->
-                    mView.setArrowStiffness(ARROW_APPEAR_STIFFNESS, ARROW_APPEAR_DAMPING_RATIO)
-                GestureState.CANCELLED -> mView.fadeOut()
-                else ->
-                    mView.setArrowStiffness(
-                        ARROW_DISAPPEAR_STIFFNESS,
-                        ARROW_DISAPPEAR_DAMPING_RATIO
-                    )
+    private fun updateRestingArrowDimens() {
+        when (currentState) {
+            GestureState.GONE,
+            GestureState.ENTRY -> {
+                mView.setSpring(
+                        arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+                        arrowAlpha = params.entryIndicator.arrowDimens.alphaSpring,
+                        scale = params.entryIndicator.scaleSpring,
+                        verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+                        horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+                        backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+                        backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
             }
+            GestureState.INACTIVE -> {
+                mView.setSpring(
+                        arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+                        horizontalTranslation = params.preThresholdIndicator
+                                .horizontalTranslationSpring,
+                        scale = params.preThresholdIndicator.scaleSpring,
+                        backgroundWidth = params.preThresholdIndicator.backgroundDimens
+                                .widthSpring,
+                        backgroundHeight = params.preThresholdIndicator.backgroundDimens
+                                .heightSpring,
+                        backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            GestureState.ACTIVE -> {
+                mView.setSpring(
+                        arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+                        scale = params.activeIndicator.scaleSpring,
+                        horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+                        backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            GestureState.FLUNG -> {
+                mView.setSpring(
+                        arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+                        backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            GestureState.COMMITTED -> {
+                mView.setSpring(
+                        arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+                        scale = params.committedIndicator.scaleSpring,
+                        backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            else -> {}
         }
+
         mView.setRestingDimens(
-            restingParams = EdgePanelParams.BackIndicatorDimens(
-                horizontalTranslation = when (currentState) {
-                    GestureState.GONE -> -params.activeIndicator.backgroundDimens.width
-                    // Position the committed arrow slightly further off the screen so we  do not
-                    // see part of it bouncing
-                    GestureState.COMMITTED ->
-                        -params.activeIndicator.backgroundDimens.width * 1.5f
-                    GestureState.FLUNG -> params.fullyStretchedIndicator.horizontalTranslation
-                    GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
-                    GestureState.ENTRY, GestureState.INACTIVE, GestureState.CANCELLED ->
-                        params.entryIndicator.horizontalTranslation
-                },
-                arrowDimens = when (currentState) {
-                    GestureState.ACTIVE, GestureState.INACTIVE,
-                    GestureState.COMMITTED, GestureState.FLUNG -> params.activeIndicator.arrowDimens
-                    GestureState.CANCELLED -> params.cancelledArrowDimens
-                    GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.arrowDimens
-                },
-                backgroundDimens = when (currentState) {
-                    GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.backgroundDimens
-                    else ->
-                        params.activeIndicator.backgroundDimens.copy(
-                            edgeCornerRadius =
-                            if (currentState == GestureState.INACTIVE ||
-                                currentState == GestureState.CANCELLED
-                            )
-                                params.cancelledEdgeCornerRadius
-                            else
-                                params.activeIndicator.backgroundDimens.edgeCornerRadius
-                        )
-                }
-            ),
-            animate = animated
+                animate = !(currentState == GestureState.FLUNG ||
+                        currentState == GestureState.COMMITTED),
+                restingParams = EdgePanelParams.BackIndicatorDimens(
+                        scale = when (currentState) {
+                            GestureState.ACTIVE,
+                            GestureState.FLUNG,
+                            -> params.activeIndicator.scale
+                            GestureState.COMMITTED -> params.committedIndicator.scale
+                            else -> params.preThresholdIndicator.scale
+                        },
+                        scalePivotX = when (currentState) {
+                            GestureState.GONE,
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE,
+                            GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX
+                            else -> params.committedIndicator.scalePivotX
+                        },
+                        horizontalTranslation = when (currentState) {
+                            GestureState.GONE -> {
+                                params.activeIndicator.backgroundDimens.width?.times(-1)
+                            }
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation
+                            GestureState.FLUNG -> params.activeIndicator.horizontalTranslation
+                            GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
+                            GestureState.CANCELLED -> {
+                                params.cancelledIndicator.horizontalTranslation
+                            }
+                            else -> null
+                        },
+                        arrowDimens = when (currentState) {
+                            GestureState.GONE,
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE -> params.entryIndicator.arrowDimens
+                            GestureState.ACTIVE -> params.activeIndicator.arrowDimens
+                            GestureState.FLUNG,
+                            GestureState.COMMITTED -> params.committedIndicator.arrowDimens
+                            GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
+                        },
+                        backgroundDimens = when (currentState) {
+                            GestureState.GONE,
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
+                            GestureState.ACTIVE -> params.activeIndicator.backgroundDimens
+                            GestureState.FLUNG -> params.activeIndicator.backgroundDimens
+                            GestureState.COMMITTED -> params.committedIndicator.backgroundDimens
+                            GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens
+                        }
+                )
         )
     }
 
@@ -623,42 +790,123 @@
     private fun updateArrowState(newState: GestureState, force: Boolean = false) {
         if (!force && currentState == newState) return
 
-        if (DEBUG) Log.d(TAG, "updateArrowState $currentState -> $newState")
         previousState = currentState
         currentState = newState
-        if (currentState == GestureState.GONE) {
-            mView.cancelAlphaAnimations()
-            mView.visibility = View.GONE
-        } else {
-            mView.visibility = View.VISIBLE
+
+        when (currentState) {
+            GestureState.CANCELLED -> {
+                backCallback.cancelBack()
+            }
+            GestureState.FLUNG,
+            GestureState.COMMITTED -> {
+                // When flung, trigger back immediately but don't fire again
+                // once state resolves to committed.
+                if (previousState != GestureState.FLUNG) backCallback.triggerBack()
+            }
+            GestureState.ENTRY,
+            GestureState.INACTIVE -> {
+                backCallback.setTriggerBack(false)
+            }
+            GestureState.ACTIVE -> {
+                backCallback.setTriggerBack(true)
+            }
+            GestureState.GONE -> { }
         }
 
         when (currentState) {
             // Transitioning to GONE never animates since the arrow is (presumably) already off the
             // screen
-            GestureState.GONE -> updateRestingArrowDimens(animated = false, currentState)
+            GestureState.GONE -> {
+                updateRestingArrowDimens()
+                mView.isVisible = false
+            }
             GestureState.ENTRY -> {
-                updateYPosition(startY)
-                updateRestingArrowDimens(animated = true, currentState)
+                mView.isVisible = true
+
+                updateRestingArrowDimens()
+                gestureEntryTime = SystemClock.uptimeMillis()
+                gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
             }
             GestureState.ACTIVE -> {
-                updateRestingArrowDimens(animated = true, currentState)
-                // Vibrate the first time we transition to ACTIVE
-                if (!hasHapticPlayed) {
-                    hasHapticPlayed = true
-                    vibrationTime = SystemClock.uptimeMillis()
-                    vibratorHelper.vibrate(VibrationEffect.EFFECT_TICK)
+                previousXTranslationOnActiveOffset = previousXTranslation
+                gestureActiveTime = SystemClock.uptimeMillis()
+
+                updateRestingArrowDimens()
+
+                vibratorHelper.cancel()
+                mainHandler.postDelayed(10L) {
+                    vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+                }
+
+                val startingVelocity = convertVelocityToSpringStartingVelocity(
+                    valueOnFastVelocity = 0f,
+                    valueOnSlowVelocity = if (previousState == GestureState.ENTRY) 2f else 4.5f
+                )
+
+                when (previousState) {
+                    GestureState.ENTRY,
+                    GestureState.INACTIVE -> {
+                        mView.popOffEdge(startingVelocity)
+                    }
+                    GestureState.COMMITTED -> {
+                        // if previous state was committed then this activation
+                        // was due to a quick second swipe. Don't pop the arrow this time
+                    }
+                    else -> { }
                 }
             }
+
             GestureState.INACTIVE -> {
-                updateRestingArrowDimens(animated = true, currentState)
+                gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
+
+                val startingVelocity = convertVelocityToSpringStartingVelocity(
+                        valueOnFastVelocity = -1.05f,
+                        valueOnSlowVelocity = -1.50f
+                )
+                mView.popOffEdge(startingVelocity)
+
+                vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+                updateRestingArrowDimens()
             }
-            GestureState.FLUNG -> playFlingBackAnimation()
-            GestureState.COMMITTED -> playCommitBackAnimation()
-            GestureState.CANCELLED -> playCancelBackAnimation()
+            GestureState.FLUNG -> {
+                mainHandler.postDelayed(POP_ON_FLING_DELAY) { mView.popScale(1.9f) }
+                playHorizontalAnimationThen(onEndSetCommittedStateListener)
+            }
+            GestureState.COMMITTED -> {
+                if (previousState == GestureState.FLUNG) {
+                    playAnimationThenSetGoneEnd()
+                } else {
+                    mView.popScale(3f)
+                    mainHandler.postDelayed(
+                            playAnimationThenSetGoneOnAlphaEnd,
+                            MIN_DURATION_COMMITTED_ANIMATION
+                    )
+                }
+            }
+            GestureState.CANCELLED -> {
+                val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
+                playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
+
+                params.arrowStrokeAlphaSpring.get(0f).takeIf { it.isNewState }?.let {
+                    mView.popArrowAlpha(0f, it.value)
+                }
+                mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+            }
         }
     }
 
+    private fun convertVelocityToSpringStartingVelocity(
+            valueOnFastVelocity: Float,
+            valueOnSlowVelocity: Float,
+    ): Float {
+        val factor = velocityTracker?.run {
+            computeCurrentVelocity(PX_PER_MS)
+            MathUtils.smoothStep(0f, 3f, abs(xVelocity))
+        } ?: valueOnFastVelocity
+
+        return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
+    }
+
     private fun scheduleFailsafe() {
         if (!ENABLE_FAILSAFE) return
         cancelFailsafe()
@@ -685,24 +933,24 @@
     init {
         if (DEBUG) mView.drawDebugInfo = { canvas ->
             val debugStrings = listOf(
-                "$currentState",
-                "startX=$startX",
-                "startY=$startY",
-                "xDelta=${"%.1f".format(totalTouchDelta)}",
-                "xTranslation=${"%.1f".format(previousXTranslation)}",
-                "pre=${"%.0f".format(preThresholdStretchProgress(previousXTranslation) * 100)}%",
-                "post=${"%.0f".format(fullScreenStretchProgress(previousXTranslation) * 100)}%"
+                    "$currentState",
+                    "startX=$startX",
+                    "startY=$startY",
+                    "xDelta=${"%.1f".format(totalTouchDelta)}",
+                    "xTranslation=${"%.1f".format(previousXTranslation)}",
+                    "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
+                    "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
             )
             val debugPaint = Paint().apply {
                 color = Color.WHITE
             }
             val debugInfoBottom = debugStrings.size * 32f + 4f
             canvas.drawRect(
-                4f,
-                4f,
-                canvas.width.toFloat(),
-                debugStrings.size * 32f + 4f,
-                debugPaint
+                    4f,
+                    4f,
+                    canvas.width.toFloat(),
+                    debugStrings.size * 32f + 4f,
+                    debugPaint
             )
             debugPaint.apply {
                 color = Color.BLACK
@@ -728,9 +976,71 @@
                 canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
             }
 
-            drawVerticalLine(x = params.swipeTriggerThreshold, color = Color.BLUE)
+            drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+            drawVerticalLine(x = params.deactivationSwipeTriggerThreshold, color = Color.BLUE)
             drawVerticalLine(x = startX, color = Color.GREEN)
             drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
         }
     }
 }
+
+/**
+ * In addition to a typical step function which returns one or two
+ * values based on a threshold, `Step` also gracefully handles quick
+ * changes in input near the threshold value that would typically
+ * result in the output rapidly changing.
+ *
+ * In the context of Back arrow, the arrow's stroke opacity should
+ * always appear transparent or opaque. Using a typical Step function,
+ * this would resulting in a flickering appearance as the output would
+ * change rapidly. `Step` addresses this by moving the threshold after
+ * it is crossed so it cannot be easily crossed again with small changes
+ * in touch events.
+ */
+class Step<T>(
+        private val threshold: Float,
+        private val factor: Float = 1.1f,
+        private val postThreshold: T,
+        private val preThreshold: T
+) {
+
+    data class Value<T>(val value: T, val isNewState: Boolean)
+
+    private val lowerFactor = 2 - factor
+
+    private lateinit var startValue: Value<T>
+    private lateinit var previousValue: Value<T>
+    private var hasCrossedUpperBoundAtLeastOnce = false
+    private var progress: Float = 0f
+
+    init {
+        reset()
+    }
+
+    fun reset() {
+        hasCrossedUpperBoundAtLeastOnce = false
+        progress = 0f
+        startValue = Value(preThreshold, false)
+        previousValue = startValue
+    }
+
+    fun get(progress: Float): Value<T> {
+        this.progress = progress
+
+        val hasCrossedUpperBound = progress > threshold * factor
+        val hasCrossedLowerBound = progress > threshold * lowerFactor
+
+        return when {
+            hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> {
+                hasCrossedUpperBoundAtLeastOnce = true
+                Value(postThreshold, true)
+            }
+            hasCrossedLowerBound -> previousValue.copy(isNewState = false)
+            hasCrossedUpperBoundAtLeastOnce -> {
+                hasCrossedUpperBoundAtLeastOnce = false
+                Value(preThreshold, true)
+            }
+            else -> startValue
+        }.also { previousValue = it }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 13c5b48..eea0e4c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -273,7 +273,6 @@
                 @Override
                 public void triggerBack() {
                     // Notify FalsingManager that an intentional gesture has occurred.
-                    // TODO(b/186519446): use a different method than isFalseTouch
                     mFalsingManager.isFalseTouch(BACK_GESTURE);
                     // Only inject back keycodes when ahead-of-time back dispatching is disabled.
                     if (mBackAnimation == null) {
@@ -502,6 +501,15 @@
     }
 
     private void updateIsEnabled() {
+        try {
+            Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
+            updateIsEnabledTraced();
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    private void updateIsEnabledTraced() {
         boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
         if (isEnabled == mIsEnabled) {
             return;
@@ -587,13 +595,18 @@
     }
 
     private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) {
-        if (mEdgeBackPlugin != null) {
-            mEdgeBackPlugin.onDestroy();
+        try {
+            Trace.beginSection("setEdgeBackPlugin");
+            if (mEdgeBackPlugin != null) {
+                mEdgeBackPlugin.onDestroy();
+            }
+            mEdgeBackPlugin = edgeBackPlugin;
+            mEdgeBackPlugin.setBackCallback(mBackCallback);
+            mEdgeBackPlugin.setLayoutParams(createLayoutParams());
+            updateDisplaySize();
+        } finally {
+            Trace.endSection();
         }
-        mEdgeBackPlugin = edgeBackPlugin;
-        mEdgeBackPlugin.setBackCallback(mBackCallback);
-        mEdgeBackPlugin.setLayoutParams(createLayoutParams());
-        updateDisplaySize();
     }
 
     public boolean isHandlingGestures() {
@@ -919,6 +932,10 @@
                             mThresholdCrossed = true;
                             // Capture inputs
                             mInputMonitor.pilferPointers();
+                            if (mBackAnimation != null) {
+                                // Notify FalsingManager that an intentional gesture has occurred.
+                                mFalsingManager.isFalseTouch(BACK_GESTURE);
+                            }
                             mInputEventReceiver.setBatchingEnabled(true);
                         } else {
                             logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_FAR_FROM_EDGE);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index d56537b..0c00022 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -1,52 +1,82 @@
 package com.android.systemui.navigationbar.gestural
 
 import android.content.res.Resources
+import android.util.TypedValue
+import androidx.core.animation.PathInterpolator
+import androidx.dynamicanimation.animation.SpringForce
 import com.android.systemui.R
 
 data class EdgePanelParams(private var resources: Resources) {
 
     data class ArrowDimens(
-        val length: Float = 0f,
-        val height: Float = 0f
+            val length: Float = 0f,
+            val height: Float = 0f,
+            val alpha: Float = 0f,
+            var alphaSpring: SpringForce? = null,
+            val heightSpring: SpringForce? = null,
+            val lengthSpring: SpringForce? = null,
     )
 
     data class BackgroundDimens(
-        val width: Float = 0f,
-        val height: Float = 0f,
-        val edgeCornerRadius: Float = 0f,
-        val farCornerRadius: Float = 0f
+            val width: Float? = 0f,
+            val height: Float = 0f,
+            val edgeCornerRadius: Float = 0f,
+            val farCornerRadius: Float = 0f,
+            val alpha: Float = 0f,
+            val widthSpring: SpringForce? = null,
+            val heightSpring: SpringForce? = null,
+            val farCornerRadiusSpring: SpringForce? = null,
+            val edgeCornerRadiusSpring: SpringForce? = null,
+            val alphaSpring: SpringForce? = null,
     )
 
     data class BackIndicatorDimens(
-        val horizontalTranslation: Float = 0f,
-        val arrowDimens: ArrowDimens = ArrowDimens(),
-        val backgroundDimens: BackgroundDimens = BackgroundDimens()
+            val horizontalTranslation: Float? = 0f,
+            val scale: Float = 0f,
+            val scalePivotX: Float = 0f,
+            val arrowDimens: ArrowDimens,
+            val backgroundDimens: BackgroundDimens,
+            val verticalTranslationSpring: SpringForce? = null,
+            val horizontalTranslationSpring: SpringForce? = null,
+            val scaleSpring: SpringForce? = null,
     )
 
-    var arrowThickness: Float = 0f
+    lateinit var entryIndicator: BackIndicatorDimens
         private set
-    var entryIndicator = BackIndicatorDimens()
+    lateinit var activeIndicator: BackIndicatorDimens
         private set
-    var activeIndicator = BackIndicatorDimens()
+    lateinit var cancelledIndicator: BackIndicatorDimens
         private set
-    var preThresholdIndicator = BackIndicatorDimens()
+    lateinit var flungIndicator: BackIndicatorDimens
         private set
-    var fullyStretchedIndicator = BackIndicatorDimens()
+    lateinit var committedIndicator: BackIndicatorDimens
         private set
-    var cancelledEdgeCornerRadius: Float = 0f
+    lateinit var preThresholdIndicator: BackIndicatorDimens
         private set
-    var cancelledArrowDimens = ArrowDimens()
+    lateinit var fullyStretchedIndicator: BackIndicatorDimens
+        private set
 
     // navigation bar edge constants
     var arrowPaddingEnd: Int = 0
         private set
+    var arrowThickness: Float = 0f
+        private set
+    lateinit var arrowStrokeAlphaSpring: Step<SpringForce>
+        private set
+    lateinit var arrowStrokeAlphaInterpolator: Step<Float>
+        private set
 
     // The closest to y
     var minArrowYPosition: Int = 0
         private set
     var fingerOffset: Int = 0
         private set
-    var swipeTriggerThreshold: Float = 0f
+    var staticTriggerThreshold: Float = 0f
+        private set
+    var reactivationTriggerThreshold: Float = 0f
+        private set
+    var deactivationSwipeTriggerThreshold: Float = 0f
+        get() = -field
         private set
     var swipeProgressThreshold: Float = 0f
         private set
@@ -55,6 +85,26 @@
     var minDeltaForSwitch: Int = 0
         private set
 
+    var minDragToStartAnimation: Float = 0f
+        private set
+
+    lateinit var entryWidthInterpolator: PathInterpolator
+        private set
+    lateinit var entryWidthTowardsEdgeInterpolator: PathInterpolator
+        private set
+    lateinit var activeWidthInterpolator: PathInterpolator
+        private set
+    lateinit var arrowAngleInterpolator: PathInterpolator
+        private set
+    lateinit var translationInterpolator: PathInterpolator
+        private set
+    lateinit var farCornerInterpolator: PathInterpolator
+        private set
+    lateinit var edgeCornerInterpolator: PathInterpolator
+        private set
+    lateinit var heightInterpolator: PathInterpolator
+        private set
+
     init {
         update(resources)
     }
@@ -63,6 +113,10 @@
         return resources.getDimension(id)
     }
 
+    private fun getDimenFloat(id: Int): Float {
+        return TypedValue().run { resources.getValue(id, this, true); float }
+    }
+
     private fun getPx(id: Int): Int {
         return resources.getDimensionPixelSize(id)
     }
@@ -73,72 +127,200 @@
         arrowPaddingEnd = getPx(R.dimen.navigation_edge_panel_padding)
         minArrowYPosition = getPx(R.dimen.navigation_edge_arrow_min_y)
         fingerOffset = getPx(R.dimen.navigation_edge_finger_offset)
-        swipeTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+        staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+        reactivationTriggerThreshold =
+                getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
+        deactivationSwipeTriggerThreshold =
+                getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
         swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
         minDeltaForSwitch = getPx(R.dimen.navigation_edge_minimum_x_delta_for_switch)
+        minDragToStartAnimation =
+                getDimen(R.dimen.navigation_edge_action_min_distance_to_start_animation)
+
+        entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f)
+        entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f)
+        activeWidthInterpolator = PathInterpolator(.15f, .48f, .46f, .89f)
+        arrowAngleInterpolator = entryWidthInterpolator
+        translationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f)
+        farCornerInterpolator = PathInterpolator(.03f, .19f, .14f, 1.09f)
+        edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f)
+        heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f)
+
+        val showArrowOnProgressValue = .2f
+        val showArrowOnProgressValueFactor = 1.05f
+
+        val entryActiveHorizontalTranslationSpring = createSpring(675f, 0.8f)
+        val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f)
+        val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f)
+        val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f)
+        val flungCommittedFarCornerSpring = createSpring(10000f, 1f)
+        val flungCommittedWidthSpring = createSpring(10000f, 1f)
+        val flungCommittedHeightSpring = createSpring(10000f, 1f)
 
         entryIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
-            arrowDimens = ArrowDimens(
-                length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
-                height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_entry_background_width),
-                height = getDimen(R.dimen.navigation_edge_entry_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners)
-            )
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_entry_scale),
+                scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+                horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+                verticalTranslationSpring = createSpring(10000f, 0.9f),
+                scaleSpring = createSpring(120f, 0.8f),
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
+                        alpha = 0f,
+                        alphaSpring = createSpring(200f, 1f),
+                        lengthSpring = createSpring(600f, 0.4f),
+                        heightSpring = createSpring(600f, 0.4f),
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_entry_background_width),
+                        height = getDimen(R.dimen.navigation_edge_entry_background_height),
+                        edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
+                        farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners),
+                        alphaSpring = createSpring(900f, 1f),
+                        widthSpring = createSpring(450f, 0.65f),
+                        heightSpring = createSpring(1500f, 0.45f),
+                        farCornerRadiusSpring = createSpring(300f, 0.5f),
+                        edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+                )
         )
 
         activeIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
-            arrowDimens = ArrowDimens(
-                length = getDimen(R.dimen.navigation_edge_active_arrow_length),
-                height = getDimen(R.dimen.navigation_edge_active_arrow_height),
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_active_background_width),
-                height = getDimen(R.dimen.navigation_edge_active_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners)
-
-            )
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
+                horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+                scaleSpring = createSpring(450f, 0.415f),
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_active_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_active_arrow_height),
+                        alpha = 1f,
+                        lengthSpring = activeCommittedArrowLengthSpring,
+                        heightSpring = activeCommittedArrowHeightSpring,
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_active_background_width),
+                        height = getDimen(R.dimen.navigation_edge_active_background_height),
+                        edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
+                        farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners),
+                        widthSpring = createSpring(375f, 0.675f),
+                        heightSpring = createSpring(10000f, 1f),
+                        edgeCornerRadiusSpring = createSpring(600f, 0.36f),
+                        farCornerRadiusSpring = createSpring(2500f, 0.855f),
+                )
         )
 
         preThresholdIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
-            arrowDimens = ArrowDimens(
-                length = entryIndicator.arrowDimens.length,
-                height = entryIndicator.arrowDimens.height,
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
-                height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners)
-            )
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale),
+                scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+                scaleSpring = createSpring(120f, 0.8f),
+                horizontalTranslationSpring = createSpring(6000f, 1f),
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height),
+                        alpha = 1f,
+                        lengthSpring = createSpring(100f, 0.6f),
+                        heightSpring = createSpring(100f, 0.6f),
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+                        height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
+                        edgeCornerRadius =
+                                getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
+                        farCornerRadius =
+                                getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
+                        widthSpring = createSpring(200f, 0.65f),
+                        heightSpring = createSpring(1500f, 0.45f),
+                        farCornerRadiusSpring = createSpring(200f, 1f),
+                        edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+                )
+        )
+
+        committedIndicator = activeIndicator.copy(
+                horizontalTranslation = null,
+                arrowDimens = activeIndicator.arrowDimens.copy(
+                        lengthSpring = activeCommittedArrowLengthSpring,
+                        heightSpring = activeCommittedArrowHeightSpring,
+                ),
+                backgroundDimens = activeIndicator.backgroundDimens.copy(
+                        alpha = 0f,
+                        // explicitly set to null to preserve previous width upon state change
+                        width = null,
+                        widthSpring = flungCommittedWidthSpring,
+                        heightSpring = flungCommittedHeightSpring,
+                        edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+                        farCornerRadiusSpring = flungCommittedFarCornerSpring,
+                ),
+                scale = 0.85f,
+                scaleSpring = createSpring(650f, 1f),
+        )
+
+        flungIndicator = committedIndicator.copy(
+                arrowDimens = committedIndicator.arrowDimens.copy(
+                        lengthSpring = createSpring(850f, 0.46f),
+                        heightSpring = createSpring(850f, 0.46f),
+                ),
+                backgroundDimens = committedIndicator.backgroundDimens.copy(
+                        widthSpring = flungCommittedWidthSpring,
+                        heightSpring = flungCommittedHeightSpring,
+                        edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+                        farCornerRadiusSpring = flungCommittedFarCornerSpring,
+                )
+        )
+
+        cancelledIndicator = entryIndicator.copy(
+                backgroundDimens = entryIndicator.backgroundDimens.copy(width = 0f)
         )
 
         fullyStretchedIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
-            arrowDimens = ArrowDimens(
-                length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
-                height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_stretch_background_width),
-                height = getDimen(R.dimen.navigation_edge_stretch_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners)
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale),
+                horizontalTranslationSpring = null,
+                verticalTranslationSpring = null,
+                scaleSpring = null,
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
+                        alpha = 1f,
+                        alphaSpring = null,
+                        heightSpring = null,
+                        lengthSpring = null,
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_stretch_background_width),
+                        height = getDimen(R.dimen.navigation_edge_stretch_background_height),
+                        edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
+                        farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners),
+                        alphaSpring = null,
+                        widthSpring = null,
+                        heightSpring = null,
+                        edgeCornerRadiusSpring = null,
+                        farCornerRadiusSpring = null,
+                )
+        )
+
+        arrowStrokeAlphaInterpolator = Step(
+                threshold = showArrowOnProgressValue,
+                factor = showArrowOnProgressValueFactor,
+                postThreshold = 1f,
+                preThreshold = 0f
+        )
+
+        entryIndicator.arrowDimens.alphaSpring?.let { alphaSpring ->
+            arrowStrokeAlphaSpring = Step(
+                    threshold = showArrowOnProgressValue,
+                    factor = showArrowOnProgressValueFactor,
+                    postThreshold = alphaSpring,
+                    preThreshold = SpringForce().setStiffness(2000f).setDampingRatio(1f)
             )
-        )
-
-        cancelledEdgeCornerRadius = getDimen(R.dimen.navigation_edge_cancelled_edge_corners)
-
-        cancelledArrowDimens = ArrowDimens(
-            length = getDimen(R.dimen.navigation_edge_cancelled_arrow_length),
-            height = getDimen(R.dimen.navigation_edge_cancelled_arrow_height)
-        )
+        }
     }
 }
+
+fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce {
+    return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 1230708..590efbb 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.navigationbar.gestural;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE;
 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
 
@@ -56,6 +54,7 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.plugins.NavigationEdgeBackPlugin;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.statusbar.VibratorHelper;
 
@@ -289,7 +288,8 @@
             Context context,
             LatencyTracker latencyTracker,
             VibratorHelper vibratorHelper,
-            @Background Executor backgroundExecutor) {
+            @Background Executor backgroundExecutor,
+            DisplayTracker displayTracker) {
         super(context);
 
         mWindowManager = context.getSystemService(WindowManager.class);
@@ -365,7 +365,7 @@
 
         setVisibility(GONE);
 
-        boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY;
+        boolean isPrimaryDisplay = mContext.getDisplayId() == displayTracker.getDefaultDisplayId();
         mRegionSamplingHelper = new RegionSamplingHelper(this,
                 new RegionSamplingHelper.SamplingCallback() {
                     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 6dd60d0..be615d6 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -17,10 +17,15 @@
 package com.android.systemui.notetask
 
 import android.app.KeyguardManager
+import android.content.ActivityNotFoundException
 import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.UserManager
+import android.util.Log
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.util.kotlin.getOrNull
@@ -40,11 +45,12 @@
 @Inject
 constructor(
     private val context: Context,
-    private val intentResolver: NoteTaskIntentResolver,
+    private val resolver: NoteTaskInfoResolver,
     private val optionalBubbles: Optional<Bubbles>,
     private val optionalKeyguardManager: Optional<KeyguardManager>,
     private val optionalUserManager: Optional<UserManager>,
     @NoteTaskEnabledKey private val isEnabled: Boolean,
+    private val uiEventLogger: UiEventLogger,
 ) {
 
     /**
@@ -57,25 +63,37 @@
      * If the keyguard is locked, notes will open as a full screen experience. A locked device has
      * no contextual information which let us use the whole screen space available.
      *
-     * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience.
+     * If not in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be
+     * collapsed if the notes bubble is already opened.
+     *
      * That will let users open other apps in full screen, and take contextual notes.
      */
-    fun showNoteTask(isInMultiWindowMode: Boolean = false) {
+    @JvmOverloads
+    fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) {
+
         if (!isEnabled) return
 
         val bubbles = optionalBubbles.getOrNull() ?: return
         val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
         val userManager = optionalUserManager.getOrNull() ?: return
-        val intent = intentResolver.resolveIntent() ?: return
 
         // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
         if (!userManager.isUserUnlocked) return
 
-        if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
-            context.startActivity(intent)
-        } else {
-            // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
-            bubbles.showAppBubble(intent)
+        val noteTaskInfo = resolver.resolveInfo() ?: return
+
+        uiEvent?.let { uiEventLogger.log(it, noteTaskInfo.uid, noteTaskInfo.packageName) }
+
+        // TODO(b/266686199): We should handle when app not available. For now, we log.
+        val intent = noteTaskInfo.toCreateNoteIntent()
+        try {
+            if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
+                context.startActivity(intent)
+            } else {
+                bubbles.showOrHideAppBubble(intent)
+            }
+        } catch (e: ActivityNotFoundException) {
+            Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", e)
         }
     }
 
@@ -102,4 +120,48 @@
             PackageManager.DONT_KILL_APP,
         )
     }
+
+    /** IDs of UI events accepted by [showNoteTask]. */
+    enum class ShowNoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
+        NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),
+
+        /* ktlint-disable max-line-length */
+        @UiEvent(
+            doc =
+                "User opened a note by pressing the stylus tail button while the screen was unlocked."
+        )
+        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),
+        @UiEvent(
+            doc =
+                "User opened a note by pressing the stylus tail button while the screen was locked."
+        )
+        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),
+        @UiEvent(doc = "User opened a note by tapping on an app shortcut.")
+        NOTE_OPENED_VIA_SHORTCUT(1297);
+
+        override fun getId() = _id
+    }
+
+    companion object {
+        private val TAG = NoteTaskController::class.simpleName.orEmpty()
+
+        private fun NoteTaskInfoResolver.NoteTaskInfo.toCreateNoteIntent(): Intent {
+            return Intent(ACTION_CREATE_NOTE)
+                .setPackage(packageName)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
+                // was used to start it.
+                .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
+        }
+
+        // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
+        const val NOTE_TASK_KEY_EVENT = 311
+
+        // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
+        const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
+
+        // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
+        const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
new file mode 100644
index 0000000..bd822d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.notetask
+
+import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.util.Log
+import javax.inject.Inject
+
+internal class NoteTaskInfoResolver
+@Inject
+constructor(
+    private val context: Context,
+    private val roleManager: RoleManager,
+    private val packageManager: PackageManager,
+) {
+    fun resolveInfo(): NoteTaskInfo? {
+        // TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking.
+        val user = context.user
+        val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, user).firstOrNull()
+
+        if (packageName.isNullOrEmpty()) return null
+
+        return NoteTaskInfo(packageName, packageManager.getUidOf(packageName, user))
+    }
+
+    /** Package name and kernel user-ID of a note-taking app. */
+    data class NoteTaskInfo(val packageName: String, val uid: Int)
+
+    companion object {
+        private val TAG = NoteTaskInfoResolver::class.simpleName.orEmpty()
+
+        private val EMPTY_APPLICATION_INFO_FLAGS = PackageManager.ApplicationInfoFlags.of(0)!!
+
+        /**
+         * Returns the kernel user-ID of [packageName] for a [user]. Returns zero if the app cannot
+         * be found.
+         */
+        private fun PackageManager.getUidOf(packageName: String, user: UserHandle): Int {
+            val applicationInfo =
+                try {
+                    getApplicationInfoAsUser(packageName, EMPTY_APPLICATION_INFO_FLAGS, user)
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.e(TAG, "Couldn't find notes app UID", e)
+                    return 0
+                }
+            return applicationInfo.uid
+        }
+
+        // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
+        const val ROLE_NOTES = "android.app.role.NOTES"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d14b7a7..d40bf2b 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,9 +16,10 @@
 
 package com.android.systemui.notetask
 
-import android.view.KeyEvent
+import android.app.KeyguardManager
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.kotlin.getOrNull
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
 import javax.inject.Inject
@@ -31,18 +32,29 @@
     private val noteTaskController: NoteTaskController,
     private val commandQueue: CommandQueue,
     @NoteTaskEnabledKey private val isEnabled: Boolean,
+    private val optionalKeyguardManager: Optional<KeyguardManager>,
 ) {
 
     @VisibleForTesting
     val callbacks =
         object : CommandQueue.Callbacks {
             override fun handleSystemKey(keyCode: Int) {
-                if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
-                    noteTaskController.showNoteTask()
+                if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
+                    showNoteTask()
                 }
             }
         }
 
+    private fun showNoteTask() {
+        val uiEvent =
+            if (optionalKeyguardManager.isKeyguardLocked) {
+                NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
+            } else {
+                NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+            }
+        noteTaskController.showNoteTask(uiEvent = uiEvent)
+    }
+
     fun initialize() {
         if (isEnabled && optionalBubbles.isPresent) {
             commandQueue.addCallback(callbacks)
@@ -50,3 +62,7 @@
         noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
     }
 }
+
+private val Optional<KeyguardManager>.isKeyguardLocked: Boolean
+    // If there's no KeyguardManager, assume that the keyguard is not locked.
+    get() = getOrNull()?.isKeyguardLocked ?: false
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
deleted file mode 100644
index 98d6991..0000000
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.notetask
-
-import android.content.ComponentName
-import android.content.Intent
-import android.content.pm.ActivityInfo
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.ResolveInfoFlags
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
-import javax.inject.Inject
-
-/**
- * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
- * [Intent] ready for be launched will be returned. Otherwise, returns null.
- *
- * TODO(b/248274123): should be revisited once the notes role is implemented.
- */
-internal class NoteTaskIntentResolver
-@Inject
-constructor(
-    private val packageManager: PackageManager,
-) {
-
-    fun resolveIntent(): Intent? {
-        val intent = Intent(NOTES_ACTION)
-        val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
-        val infoList = packageManager.queryIntentActivities(intent, flags)
-
-        for (info in infoList) {
-            val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
-            val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
-
-            return Intent(NOTES_ACTION)
-                .setComponent(ComponentName(packageName, activityName))
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        }
-
-        return null
-    }
-
-    private fun resolveActivityNameForNotesAction(packageName: String): String? {
-        val intent = Intent(NOTES_ACTION).setPackage(packageName)
-        val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
-        val resolveInfo = packageManager.resolveActivity(intent, flags)
-
-        val activityInfo = resolveInfo?.activityInfo ?: return null
-        if (activityInfo.name.isNullOrBlank()) return null
-        if (!activityInfo.exported) return null
-        if (!activityInfo.enabled) return null
-        if (!activityInfo.showWhenLocked) return null
-        if (!activityInfo.turnScreenOn) return null
-
-        return activityInfo.name
-    }
-
-    companion object {
-        // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
-        const val NOTES_ACTION = "android.intent.action.NOTES"
-    }
-}
-
-private val ActivityInfo.showWhenLocked: Boolean
-    get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0
-
-private val ActivityInfo.turnScreenOn: Boolean
-    get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 8bdf319..b8800a2 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -18,11 +18,13 @@
 
 import android.app.Activity
 import android.app.KeyguardManager
+import android.app.role.RoleManager
 import android.content.Context
 import android.os.UserManager
 import androidx.core.content.getSystemService
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
 import dagger.Binds
@@ -33,20 +35,25 @@
 import java.util.Optional
 
 /** Compose all dependencies required by Note Task feature. */
-@Module
+@Module(includes = [NoteTaskQuickAffordanceModule::class])
 internal interface NoteTaskModule {
 
     @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
-    fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity?
+    fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
 
     @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
-    fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity?
+    fun CreateNoteTaskShortcutActivity.bindNoteTaskShortcutActivity(): Activity
 
     companion object {
 
         @[Provides NoteTaskEnabledKey]
-        fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
-            return featureFlags.isEnabled(Flags.NOTE_TASKS)
+        fun provideIsNoteTaskEnabled(
+            featureFlags: FeatureFlags,
+            roleManager: RoleManager,
+        ): Boolean {
+            val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskInfoResolver.ROLE_NOTES)
+            val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS)
+            return isRoleAvailable && isFeatureEnabled
         }
 
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
new file mode 100644
index 0000000..43869cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.notetask.quickaffordance
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
+import com.android.systemui.notetask.NoteTaskEnabledKey
+import javax.inject.Inject
+import kotlinx.coroutines.flow.flowOf
+
+internal class NoteTaskQuickAffordanceConfig
+@Inject
+constructor(
+    context: Context,
+    private val noteTaskController: NoteTaskController,
+    @NoteTaskEnabledKey private val isEnabled: Boolean,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key = BuiltInKeyguardQuickAffordanceKeys.CREATE_NOTE
+
+    override val pickerName: String = context.getString(R.string.note_task_button_label)
+
+    override val pickerIconResourceId = R.drawable.ic_note_task_shortcut_keyguard
+
+    override val lockScreenState = flowOf(getLockScreenState())
+
+    // TODO(b/265949213)
+    private fun getLockScreenState() =
+        if (isEnabled) {
+            val icon = Icon.Resource(pickerIconResourceId, ContentDescription.Loaded(pickerName))
+            LockScreenState.Visible(icon)
+        } else {
+            LockScreenState.Hidden
+        }
+
+    override suspend fun getPickerScreenState() =
+        if (isEnabled) {
+            PickerScreenState.Default()
+        } else {
+            PickerScreenState.UnavailableOnDevice
+        }
+
+    override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
+        noteTaskController.showNoteTask(
+            uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
+        )
+        return OnTriggeredResult.Handled
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
new file mode 100644
index 0000000..7cb932a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.notetask.quickaffordance
+
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+internal interface NoteTaskQuickAffordanceModule {
+
+    @[Binds IntoSet]
+    fun NoteTaskQuickAffordanceConfig.bindNoteTaskQuickAffordance(): KeyguardQuickAffordanceConfig
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
index f6a623e..6ab0da6 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
@@ -46,7 +46,7 @@
                 id = SHORTCUT_ID,
                 shortLabel = getString(R.string.note_task_button_label),
                 intent = LaunchNoteTaskActivity.newIntent(context = this),
-                iconResource = R.drawable.ic_note_task_button,
+                iconResource = R.drawable.ic_note_task_shortcut_widget,
             )
         setResult(Activity.RESULT_OK, intent)
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index 47fe676..3ac5bfa 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -21,7 +21,7 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskIntentResolver
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
 import javax.inject.Inject
 
 /** Activity responsible for launching the note experience, and finish. */
@@ -34,7 +34,10 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        noteTaskController.showNoteTask(isInMultiWindowMode)
+        noteTaskController.showNoteTask(
+            isInMultiWindowMode = isInMultiWindowMode,
+            uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+        )
 
         finish()
     }
@@ -45,8 +48,8 @@
         fun newIntent(context: Context): Intent {
             return Intent(context, LaunchNoteTaskActivity::class.java).apply {
                 // Intent's action must be set in shortcuts, or an exception will be thrown.
-                // TODO(b/254606432): Use Intent.ACTION_NOTES instead.
-                action = NoteTaskIntentResolver.NOTES_ACTION
+                // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
+                action = NoteTaskController.ACTION_CREATE_NOTE
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index fba5f63..7f0f894 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -68,8 +68,10 @@
         };
 
         if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+            Log.d(TAG, "Using the Compose implementation of the PeopleSpaceActivity");
             ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
         } else {
+            Log.d(TAG, "Using the View implementation of the PeopleSpaceActivity");
             ViewGroup view = PeopleViewBinder.create(this);
             PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
             setContentView(view);
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java
index 146633d..95f1419 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java
@@ -121,6 +121,6 @@
     @Provides
     @Named(PLUGIN_PRIVILEGED)
     static List<String> providesPrivilegedPlugins(Context context) {
-        return Arrays.asList(context.getResources().getStringArray(R.array.config_pluginWhitelist));
+        return Arrays.asList(context.getResources().getStringArray(R.array.config_pluginAllowlist));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index 90fc1d7..595b882 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -68,6 +68,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.NotificationChannels;
@@ -175,7 +176,7 @@
     private ActivityStarter mActivityStarter;
     private final BroadcastSender mBroadcastSender;
     private final UiEventLogger mUiEventLogger;
-
+    private final UserTracker mUserTracker;
     private final Lazy<BatteryController> mBatteryControllerLazy;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
 
@@ -184,7 +185,8 @@
     @Inject
     public PowerNotificationWarnings(Context context, ActivityStarter activityStarter,
             BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy,
-            DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger) {
+            DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger,
+            UserTracker userTracker) {
         mContext = context;
         mNoMan = mContext.getSystemService(NotificationManager.class);
         mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
@@ -196,6 +198,7 @@
         mDialogLaunchAnimator = dialogLaunchAnimator;
         mUseSevereDialog = mContext.getResources().getBoolean(R.bool.config_severe_battery_dialog);
         mUiEventLogger = uiEventLogger;
+        mUserTracker = userTracker;
     }
 
     @Override
@@ -692,7 +695,7 @@
                         Secure.putIntForUser(
                                 resolver,
                                 Secure.LOW_POWER_WARNING_ACKNOWLEDGED,
-                                1, UserHandle.USER_CURRENT);
+                                1, mUserTracker.getUserId());
                     });
         } else {
             d.setTitle(R.string.battery_saver_confirmation_title);
@@ -843,7 +846,8 @@
                 logEvent(BatteryWarningEvents
                         .LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_SETTINGS);
                 dismissLowBatteryNotification();
-                mContext.startActivityAsUser(mOpenBatterySaverSettings, UserHandle.CURRENT);
+                mContext.startActivityAsUser(mOpenBatterySaverSettings,
+                        mUserTracker.getUserHandle());
             } else if (action.equals(ACTION_START_SAVER)) {
                 logEvent(BatteryWarningEvents
                         .LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_TURN_ON);
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
index 2a6ca1a..79167f2 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -16,12 +16,16 @@
 
 import android.content.Context
 import android.util.AttributeSet
+import android.view.Gravity.CENTER_VERTICAL
+import android.view.Gravity.END
 import android.view.ViewGroup
-import android.widget.FrameLayout
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.settingslib.Utils
 import com.android.systemui.R
+import com.android.systemui.animation.LaunchableFrameLayout
 import com.android.systemui.statusbar.events.BackgroundAnimatableView
 
 class OngoingPrivacyChip @JvmOverloads constructor(
@@ -29,13 +33,13 @@
     attrs: AttributeSet? = null,
     defStyleAttrs: Int = 0,
     defStyleRes: Int = 0
-) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
+) : LaunchableFrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
 
     private var iconMargin = 0
     private var iconSize = 0
     private var iconColor = 0
 
-    private lateinit var iconsContainer: LinearLayout
+    private val iconsContainer: LinearLayout
 
     var privacyList = emptyList<PrivacyItem>()
         set(value) {
@@ -43,11 +47,13 @@
             updateView(PrivacyChipBuilder(context, field))
         }
 
-    override fun onFinishInflate() {
-        super.onFinishInflate()
-
+    init {
+        inflate(context, R.layout.ongoing_privacy_chip, this)
+        id = R.id.privacy_chip
+        layoutParams = LayoutParams(WRAP_CONTENT, MATCH_PARENT, CENTER_VERTICAL or END)
+        clipChildren = true
+        clipToPadding = true
         iconsContainer = requireViewById(R.id.icons_container)
-
         updateResources()
     }
 
@@ -107,6 +113,6 @@
         val padding = context.resources
                 .getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
         iconsContainer.setPaddingRelative(padding, 0, padding, 0)
-        iconsContainer.background = context.getDrawable(R.drawable.privacy_chip_bg)
+        iconsContainer.background = context.getDrawable(R.drawable.statusbar_privacy_chip_bg)
     }
 }
\ No newline at end of file
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..2751072
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import javax.inject.Inject;
+
+/**
+ * A simple wrapper that provides access to process-related details. This facilitates testing by
+ * providing a mockable target around these details.
+ */
+public class ProcessWrapper {
+    @Inject
+    public ProcessWrapper() {}
+
+    /**
+     * Returns {@code true} if System User is running the current process.
+     */
+    public boolean isSystemUser() {
+        return android.os.Process.myUserHandle().isSystem();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java
new file mode 100644
index 0000000..80fbf911
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java
@@ -0,0 +1,45 @@
+/*
+ * 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.shared.condition.Condition;
+
+import javax.inject.Inject;
+
+/**
+ * {@link SystemProcessCondition} checks to make sure the current process is being ran by the
+ * System User.
+ */
+public class SystemProcessCondition extends Condition {
+    private final ProcessWrapper mProcessWrapper;
+
+    @Inject
+    public SystemProcessCondition(ProcessWrapper processWrapper) {
+        super();
+        mProcessWrapper = processWrapper;
+    }
+
+    @Override
+    protected void start() {
+        updateCondition(mProcessWrapper.isSystemUser());
+    }
+
+    @Override
+    protected void stop() {
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt b/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt
new file mode 100644
index 0000000..62c99da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.qrcodescanner.dagger
+
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.QRCodeScannerTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface QRCodeScannerModule {
+
+    /**
+     */
+    @Binds
+    @IntoMap
+    @StringKey(QRCodeScannerTile.TILE_SPEC)
+    fun bindQRCodeScannerTile(qrCodeScannerTile: QRCodeScannerTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt b/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt
index b48ea23..c70cce9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt
@@ -43,6 +43,7 @@
 import javax.inject.Inject
 
 private const val TAG = "AutoAddTracker"
+private const val DELIMITER = ","
 
 /**
  * Class to track tiles that have been auto-added
@@ -67,7 +68,7 @@
 
     @GuardedBy("autoAdded")
     private val autoAdded = ArraySet<String>()
-    private var restoredTiles: Set<String>? = null
+    private var restoredTiles: Map<String, AutoTile>? = null
 
     override val currentUserId: Int
         get() = userId
@@ -98,25 +99,26 @@
         when (intent.getStringExtra(Intent.EXTRA_SETTING_NAME)) {
             Settings.Secure.QS_TILES -> {
                 restoredTiles = intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)
-                        ?.split(",")
-                        ?.toSet()
+                        ?.split(DELIMITER)
+                        ?.mapIndexed(::AutoTile)
+                        ?.associateBy(AutoTile::tileType)
                         ?: run {
                             Log.w(TAG, "Null restored tiles for user $userId")
-                            emptySet()
+                            emptyMap()
                         }
             }
             Settings.Secure.QS_AUTO_ADDED_TILES -> {
-                restoredTiles?.let { tiles ->
+                restoredTiles?.let { restoredTiles ->
                     val restoredAutoAdded = intent
                             .getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)
-                            ?.split(",")
+                            ?.split(DELIMITER)
                             ?: emptyList()
                     val autoAddedBeforeRestore = intent
                             .getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE)
-                            ?.split(",")
+                            ?.split(DELIMITER)
                             ?: emptyList()
 
-                    val tilesToRemove = restoredAutoAdded.filter { it !in tiles }
+                    val tilesToRemove = restoredAutoAdded.filter { it !in restoredTiles }
                     if (tilesToRemove.isNotEmpty()) {
                         qsHost.removeTiles(tilesToRemove)
                     }
@@ -180,6 +182,9 @@
         registerBroadcastReceiver()
     }
 
+    fun getRestoredTilePosition(tile: String): Int =
+        restoredTiles?.get(tile)?.index ?: QSHost.POSITION_AT_END
+
     /**
      * Returns `true` if the tile has been auto-added before
      */
@@ -196,12 +201,12 @@
      */
     fun setTileAdded(tile: String) {
         val tiles = synchronized(autoAdded) {
-                if (autoAdded.add(tile)) {
-                    getTilesFromListLocked()
-                } else {
-                    null
-                }
+            if (autoAdded.add(tile)) {
+                getTilesFromListLocked()
+            } else {
+                null
             }
+        }
         tiles?.let { saveTiles(it) }
     }
 
@@ -222,7 +227,7 @@
     }
 
     private fun getTilesFromListLocked(): String {
-        return TextUtils.join(",", autoAdded)
+        return TextUtils.join(DELIMITER, autoAdded)
     }
 
     private fun saveTiles(tiles: String) {
@@ -245,7 +250,7 @@
 
     private fun getAdded(): Collection<String> {
         val current = secureSettings.getStringForUser(Settings.Secure.QS_AUTO_ADDED_TILES, userId)
-        return current?.split(",") ?: emptySet()
+        return current?.split(DELIMITER) ?: emptySet()
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -281,4 +286,6 @@
             )
         }
     }
+
+    private data class AutoTile(val index: Int, val tileType: String)
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index e1289a6..a7aac5a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -135,7 +135,7 @@
     private int mNumQuickTiles;
     private int mLastQQSTileHeight;
     private float mLastPosition;
-    private final QSTileHost mHost;
+    private final QSHost mHost;
     private final Executor mExecutor;
     private boolean mShowCollapsedOnKeyguard;
     private int mQQSTop;
@@ -146,7 +146,7 @@
     @Inject
     public QSAnimator(QS qs, QuickQSPanel quickPanel, QuickStatusBarHeader quickStatusBarHeader,
             QSPanelController qsPanelController,
-            QuickQSPanelController quickQSPanelController, QSTileHost qsTileHost,
+            QuickQSPanelController quickQSPanelController, QSHost qsTileHost,
             @Main Executor executor, TunerService tunerService,
             QSExpansionPathInterpolator qsExpansionPathInterpolator) {
         mQs = qs;
@@ -330,12 +330,8 @@
 
                     // Offset the translation animation on the views
                     // (that goes from 0 to getOffsetTranslation)
-                    int offsetWithQSBHTranslation =
-                            yOffset - mQuickStatusBarHeader.getOffsetTranslation();
-                    qqsTranslationYBuilder.addFloat(quickTileView, "translationY", 0,
-                            offsetWithQSBHTranslation);
-                    translationYBuilder.addFloat(tileView, "translationY",
-                            -offsetWithQSBHTranslation, 0);
+                    qqsTranslationYBuilder.addFloat(quickTileView, "translationY", 0, yOffset);
+                    translationYBuilder.addFloat(tileView, "translationY", -yOffset, 0);
 
                     translationXBuilder.addFloat(quickTileView, "translationX", 0, xOffset);
                     translationXBuilder.addFloat(tileView, "translationX", -xOffset, 0);
@@ -489,7 +485,7 @@
         if (specs.isEmpty()) {
             // specs should not be empty in a valid secondary page, as we scrolled to it.
             // We may crash later on because there's a null animator.
-            specs = mQsPanelController.getHost().mTileSpecs;
+            specs = mHost.getSpecs();
             Log.e(TAG, "Trying to create animators for empty page " + page + ". Tiles: " + specs);
             // return null;
         }
@@ -614,7 +610,7 @@
         View commonView = mQs.getView();
         getRelativePositionInt(qsPosition, view1, commonView);
         getRelativePositionInt(qqsPosition, view2, commonView);
-        return (qsPosition[1] - qqsPosition[1]) - mQuickStatusBarHeader.getOffsetTranslation();
+        return qsPosition[1] - qqsPosition[1];
     }
 
     private boolean isIconInAnimatedRow(int count) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 0c242d9..b7f9f6b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -53,7 +53,6 @@
     private boolean mQsDisabled;
     private int mContentHorizontalPadding = -1;
     private boolean mClippingEnabled;
-    private boolean mUseCombinedHeaders;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -68,10 +67,6 @@
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
     }
 
-    void setUseCombinedHeaders(boolean useCombinedHeaders) {
-        mUseCombinedHeaders = useCombinedHeaders;
-    }
-
     @Override
     public boolean hasOverlappingRendering() {
         return false;
@@ -150,8 +145,7 @@
     void updateResources(QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
         int topPadding = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
-        if (mUseCombinedHeaders
-                && !LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) {
+        if (!LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) {
             topPadding = mContext.getResources()
                     .getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 28b4c822..73a5faa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -22,8 +22,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -66,15 +64,13 @@
             QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController,
             ConfigurationController configurationController,
-            FalsingManager falsingManager,
-            FeatureFlags featureFlags) {
+            FalsingManager falsingManager) {
         super(view);
         mQsPanelController = qsPanelController;
         mQuickStatusBarHeaderController = quickStatusBarHeaderController;
         mConfigurationController = configurationController;
         mFalsingManager = falsingManager;
         mQSPanelContainer = mView.getQSPanelContainer();
-        view.setUseCombinedHeaders(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index f49ffb4..9d34df8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -49,10 +49,9 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.animation.ShadeInterpolation;
+import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.ui.MediaHost;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -60,6 +59,7 @@
 import com.android.systemui.qs.dagger.QSFragmentComponent;
 import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
+import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.util.LifecycleFragment;
+import com.android.systemui.util.Utils;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -86,14 +87,11 @@
 
     private final Rect mQsBounds = new Rect();
     private final SysuiStatusBarStateController mStatusBarStateController;
-    private final FalsingManager mFalsingManager;
     private final KeyguardBypassController mBypassController;
     private boolean mQsExpanded;
     private boolean mHeaderAnimating;
     private boolean mStackScrollerOverscrolling;
 
-    private long mDelay;
-
     private QSAnimator mQSAnimator;
     private HeightListener mPanelView;
     private QSSquishinessController mQSSquishinessController;
@@ -114,8 +112,7 @@
     private final MediaHost mQqsMediaHost;
     private final QSFragmentComponent.Factory mQsComponentFactory;
     private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
-    private final QSTileHost mHost;
-    private final FeatureFlags mFeatureFlags;
+    private final QSLogger mLogger;
     private final FooterActionsController mFooterActionsController;
     private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
     private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner;
@@ -148,11 +145,6 @@
      */
     private boolean mTransitioningToFullShade;
 
-    /**
-     * Whether the next Quick settings
-     */
-    private boolean mAnimateNextQsUpdate;
-
     private final DumpManager mDumpManager;
 
     /**
@@ -176,14 +168,13 @@
 
     @Inject
     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
-            QSTileHost qsTileHost,
             SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
             @Named(QS_PANEL) MediaHost qsMediaHost,
             @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
             KeyguardBypassController keyguardBypassController,
             QSFragmentComponent.Factory qsComponentFactory,
             QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
-            FalsingManager falsingManager, DumpManager dumpManager, FeatureFlags featureFlags,
+            DumpManager dumpManager, QSLogger qsLogger,
             FooterActionsController footerActionsController,
             FooterActionsViewModel.Factory footerActionsViewModelFactory) {
         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
@@ -191,13 +182,11 @@
         mQqsMediaHost = qqsMediaHost;
         mQsComponentFactory = qsComponentFactory;
         mQsFragmentDisableFlagsLogger = qsFragmentDisableFlagsLogger;
+        mLogger = qsLogger;
         commandQueue.observe(getLifecycle(), this);
-        mHost = qsTileHost;
-        mFalsingManager = falsingManager;
         mBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
         mDumpManager = dumpManager;
-        mFeatureFlags = featureFlags;
         mFooterActionsController = footerActionsController;
         mFooterActionsViewModelFactory = footerActionsViewModelFactory;
         mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
@@ -227,9 +216,7 @@
 
         mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
                 this);
-        LinearLayout footerActionsView = view.findViewById(R.id.qs_footer_actions);
-        FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
-                mListeningAndVisibilityLifecycleOwner);
+        bindFooterActionsView(view);
         mFooterActionsController.init();
 
         mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
@@ -241,7 +228,6 @@
                 (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
                     // Lazily update animators whenever the scrolling changes
                     mQSAnimator.requestAnimatorUpdate();
-                    mHeader.setExpandedScrollAmount(scrollY);
                     if (mScrollListener != null) {
                         mScrollListener.onQsPanelScrollChanged(scrollY);
                     }
@@ -290,6 +276,33 @@
                 });
     }
 
+    private void bindFooterActionsView(View root) {
+        LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions);
+
+        if (!ComposeFacade.INSTANCE.isComposeAvailable()) {
+            Log.d(TAG, "Binding the View implementation of the QS footer actions");
+            FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
+                    mListeningAndVisibilityLifecycleOwner);
+            return;
+        }
+
+        // Compose is available, so let's use the Compose implementation of the footer actions.
+        Log.d(TAG, "Binding the Compose implementation of the QS footer actions");
+        View composeView = ComposeFacade.INSTANCE.createFooterActionsView(root.getContext(),
+                mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner);
+
+        // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin
+        // to all views except for qs_footer_actions, so we set it to the Compose view.
+        composeView.setId(R.id.qs_footer_actions);
+
+        // Replace the View by the Compose provided one.
+        ViewGroup parent = (ViewGroup) footerActionsView.getParent();
+        ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams();
+        int index = parent.indexOfChild(footerActionsView);
+        parent.removeViewAt(index);
+        parent.addView(composeView, index, layoutParams);
+    }
+
     @Override
     public void setScrollListener(ScrollListener listener) {
         mScrollListener = listener;
@@ -634,8 +647,6 @@
         int heightDiff = getHeightDiff();
         float panelTranslationY = translationScaleY * heightDiff;
 
-        // Let the views animate their contents correctly by giving them the necessary context.
-        mHeader.setExpansion(onKeyguardAndExpanded, expansion, panelTranslationY);
         if (expansion < 1 && expansion > 0.99) {
             if (mQuickQSPanelController.switchTileLayout(false)) {
                 mHeader.updateResources();
@@ -683,14 +694,16 @@
         } else {
             mQsMediaHost.setSquishFraction(mSquishinessFraction);
         }
-
+        updateMediaPositions();
     }
 
     private void setAlphaAnimationProgress(float progress) {
         final View view = getView();
         if (progress == 0 && view.getVisibility() != View.INVISIBLE) {
+            mLogger.logVisibility("QS fragment", View.INVISIBLE);
             view.setVisibility(View.INVISIBLE);
         } else if (progress > 0 && view.getVisibility() != View.VISIBLE) {
+            mLogger.logVisibility("QS fragment", View.VISIBLE);
             view.setVisibility((View.VISIBLE));
         }
         view.setAlpha(interpolateAlphaAnimationProgress(progress));
@@ -758,6 +771,22 @@
                         - mQSPanelController.getPaddingBottom());
     }
 
+    private void updateMediaPositions() {
+        if (Utils.useQsMediaPlayer(getContext())) {
+            View hostView = mQsMediaHost.getHostView();
+            // Make sure the media appears a bit from the top to make it look nicer
+            if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible()
+                    && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) {
+                float interpolation = 1.0f - mLastQSExpansion;
+                interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation);
+                float translationY = -hostView.getHeight() * 1.3f * interpolation;
+                hostView.setTranslationY(translationY);
+            } else {
+                hostView.setTranslationY(0);
+            }
+        }
+    }
+
     private boolean headerWillBeAnimating() {
         return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState();
     }
@@ -871,7 +900,6 @@
             getView().getViewTreeObserver().removeOnPreDrawListener(this);
             getView().animate()
                     .translationY(0f)
-                    .setStartDelay(mDelay)
                     .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                     .setListener(mAnimateHeaderSlidingInListener)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
index 1da30ad..a71e6dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
@@ -14,15 +14,48 @@
 
 package com.android.systemui.qs;
 
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.provider.Settings;
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.R;
+import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.plugins.qs.QSTileView;
+import com.android.systemui.util.leak.GarbageMonitor;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 
 public interface QSHost {
+    String TILES_SETTING = Settings.Secure.QS_TILES;
+    int POSITION_AT_END = -1;
+
+    /**
+     * Returns the default QS tiles for the context.
+     * @param context the context to obtain the resources from
+     * @return a list of specs of the default tiles
+     */
+    static List<String> getDefaultSpecs(Context context) {
+        final ArrayList<String> tiles = new ArrayList();
+
+        final Resources res = context.getResources();
+        final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
+
+        tiles.addAll(Arrays.asList(defaultTileList.split(",")));
+        if (Build.IS_DEBUGGABLE
+                && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) {
+            tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
+        }
+        return tiles;
+    }
+
     void warn(String message, Throwable t);
     void collapsePanels();
     void forceCollapsePanels();
@@ -37,6 +70,44 @@
     void removeTile(String tileSpec);
     void removeTiles(Collection<String> specs);
 
+    List<String> getSpecs();
+    /**
+     * Create a view for a tile, iterating over all possible {@link QSFactory}.
+     *
+     * @see QSFactory#createTileView
+     */
+    QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView);
+    /** Create a {@link QSTile} of a {@code tileSpec} type. */
+    QSTile createTile(String tileSpec);
+
+    /**
+     * Add a tile to the end
+     *
+     * @param spec string matching a pre-defined tilespec
+     */
+    void addTile(String spec);
+
+    /**
+     * Add a tile into the requested spot, or at the end if the position is greater than the number
+     * of tiles.
+     * @param spec string matching a pre-defined tilespec
+     * @param requestPosition -1 for end, 0 for beginning, or X for insertion at position X
+     */
+    void addTile(String spec, int requestPosition);
+    void addTile(ComponentName tile);
+
+    /**
+     * Adds a custom tile to the set of current tiles.
+     * @param tile the component name of the {@link android.service.quicksettings.TileService}
+     * @param end if true, the tile will be added at the end. If false, at the beginning.
+     */
+    void addTile(ComponentName tile, boolean end);
+    void removeTileByUser(ComponentName tile);
+    void changeTilesByUser(List<String> previousTiles, List<String> newTiles);
+
+    boolean isTileAdded(ComponentName componentName, int userId);
+    void setTileAdded(ComponentName componentName, int userId, boolean added);
+
     int indexOf(String tileSpec);
 
     InstanceId getNewInstanceId();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 7067c220..b476521 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -25,8 +25,6 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
 import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -81,7 +79,6 @@
     protected boolean mExpanded;
     protected boolean mListening;
 
-    @Nullable protected QSTileHost mHost;
     private final List<OnConfigurationChangedListener> mOnConfigurationChangedListeners =
             new ArrayList<>();
 
@@ -106,8 +103,12 @@
     private final Rect mClippingRect = new Rect();
     private ViewGroup mMediaHostView;
     private boolean mShouldMoveMediaOnExpansion = true;
-    private boolean mUsingCombinedHeaders = false;
     private QSLogger mQsLogger;
+    /**
+     * Specifies if we can collapse to QQS in current state. In split shade that should be always
+     * false. It influences available accessibility actions.
+     */
+    private boolean mCanCollapse = true;
 
     public QSPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -152,10 +153,6 @@
         }
     }
 
-    void setUsingCombinedHeaders(boolean usingCombinedHeaders) {
-        mUsingCombinedHeaders = usingCombinedHeaders;
-    }
-
     protected void setHorizontalContentContainerClipping() {
         mHorizontalContentContainer.setClipChildren(true);
         mHorizontalContentContainer.setClipToPadding(false);
@@ -361,11 +358,6 @@
         }
     }
 
-    @Nullable
-    public QSTileHost getHost() {
-        return mHost;
-    }
-
     public void updateResources() {
         updatePadding();
 
@@ -380,9 +372,7 @@
 
     protected void updatePadding() {
         final Resources res = mContext.getResources();
-        int paddingTop = res.getDimensionPixelSize(
-                mUsingCombinedHeaders ? R.dimen.qs_panel_padding_top_combined_headers
-                        : R.dimen.qs_panel_padding_top);
+        int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top);
         int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         setPaddingRelative(getPaddingStart(),
                 paddingTop,
@@ -650,7 +640,9 @@
     @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
-        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+        if (mCanCollapse) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+        }
     }
 
     @Override
@@ -669,15 +661,11 @@
         mCollapseExpandAction = action;
     }
 
-    private class H extends Handler {
-        private static final int ANNOUNCE_FOR_ACCESSIBILITY = 1;
-
-        @Override
-        public void handleMessage(Message msg) {
-            if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) {
-                announceForAccessibility((CharSequence) msg.obj);
-            }
-        }
+    /**
+     * Specifies if these expanded QS can collapse to QQS.
+     */
+    public void setCanCollapse(boolean canCollapse) {
+        mCanCollapse = canCollapse;
     }
 
     public interface QSTileLayout {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index cabe1da..83b373d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -17,7 +17,6 @@
 package com.android.systemui.qs;
 
 import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
-import static com.android.systemui.flags.Flags.COMBINED_QS_HEADERS;
 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
 import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS;
 import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
@@ -28,7 +27,6 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.media.controls.ui.MediaHostState;
@@ -73,7 +71,7 @@
 
     @Inject
     QSPanelController(QSPanel view, TunerService tunerService,
-            QSTileHost qstileHost, QSCustomizerController qsCustomizerController,
+            QSHost qsHost, QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             @Named(QS_PANEL) MediaHost mediaHost,
             QSTileRevealController.Factory qsTileRevealControllerFactory,
@@ -81,9 +79,8 @@
             QSLogger qsLogger, BrightnessController.Factory brightnessControllerFactory,
             BrightnessSliderController.Factory brightnessSliderFactory,
             FalsingManager falsingManager,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            FeatureFlags featureFlags) {
-        super(view, qstileHost, qsCustomizerController, usingMediaPlayer, mediaHost,
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+        super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost,
                 metricsLogger, uiEventLogger, qsLogger, dumpManager);
         mTunerService = tunerService;
         mQsCustomizerController = qsCustomizerController;
@@ -96,7 +93,6 @@
         mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController);
         mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
-        mView.setUsingCombinedHeaders(featureFlags.isEnabled(COMBINED_QS_HEADERS));
     }
 
     @Override
@@ -148,9 +144,10 @@
     }
 
     @Override
-    protected void onSplitShadeChanged() {
+    protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) {
         ((PagedTileLayout) mView.getOrCreateTileLayout())
                 .forceTilesRedistribution("Split shade state changed");
+        mView.setCanCollapse(!shouldUseSplitNotificationShade);
     }
 
     /** */
@@ -175,12 +172,6 @@
         mBrightnessMirrorHandler.setController(brightnessMirrorController);
     }
 
-    /** Get the QSTileHost this panel uses. */
-    public QSTileHost getHost() {
-        return mHost;
-    }
-
-
     /** Update appearance of QSPanel. */
     public void updateResources() {
         mView.updateResources();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 7bb672c..2668d2e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -64,7 +64,7 @@
 public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewController<T>
         implements Dumpable{
     private static final String TAG = "QSPanelControllerBase";
-    protected final QSTileHost mHost;
+    protected final QSHost mHost;
     private final QSCustomizerController mQsCustomizerController;
     private final boolean mUsingMediaPlayer;
     protected final MediaHost mMediaHost;
@@ -104,14 +104,14 @@
                     switchTileLayoutIfNeeded();
                     onConfigurationChanged();
                     if (previousSplitShadeState != mShouldUseSplitNotificationShade) {
-                        onSplitShadeChanged();
+                        onSplitShadeChanged(mShouldUseSplitNotificationShade);
                     }
                 }
             };
 
     protected void onConfigurationChanged() { }
 
-    protected void onSplitShadeChanged() { }
+    protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) { }
 
     private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> {
         if (mMediaVisibilityChangedListener != null) {
@@ -128,7 +128,7 @@
 
     protected QSPanelControllerBase(
             T view,
-            QSTileHost host,
+            QSHost host,
             QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             MediaHost mediaHost,
@@ -372,18 +372,18 @@
         if (mUsingHorizontalLayout) {
             // Only height remaining
             parameters.getDisappearSize().set(0.0f, 0.4f);
-            // Disappearing on the right side on the bottom
-            parameters.getGonePivot().set(1.0f, 1.0f);
+            // Disappearing on the right side on the top
+            parameters.getGonePivot().set(1.0f, 0.0f);
             // translating a bit horizontal
             parameters.getContentTranslationFraction().set(0.25f, 1.0f);
             parameters.setDisappearEnd(0.6f);
         } else {
             // Only width remaining
             parameters.getDisappearSize().set(1.0f, 0.0f);
-            // Disappearing on the bottom
-            parameters.getGonePivot().set(0.0f, 1.0f);
+            // Disappearing on the top
+            parameters.getGonePivot().set(0.0f, 0.0f);
             // translating a bit vertical
-            parameters.getContentTranslationFraction().set(0.0f, 1.05f);
+            parameters.getContentTranslationFraction().set(0.0f, 1f);
             parameters.setDisappearEnd(0.95f);
         }
         parameters.setFadeStartPosition(0.95f);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 100853c..0ead979 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -18,7 +18,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.os.Build;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings.Secure;
@@ -56,17 +55,14 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
-import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.settings.SecureSettings;
 
 import org.jetbrains.annotations.NotNull;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -94,16 +90,13 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final int MAX_QS_INSTANCE_ID = 1 << 20;
 
-    public static final int POSITION_AT_END = -1;
-    public static final String TILES_SETTING = Secure.QS_TILES;
-
     // Shared prefs that hold tile lifecycle info.
     @VisibleForTesting
     static final String TILES = "tiles_prefs";
 
     private final Context mContext;
     private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>();
-    protected final ArrayList<String> mTileSpecs = new ArrayList<>();
+    private final ArrayList<String> mTileSpecs = new ArrayList<>();
     private final TunerService mTunerService;
     private final PluginManager mPluginManager;
     private final DumpManager mDumpManager;
@@ -117,7 +110,6 @@
     private final List<Callback> mCallbacks = new ArrayList<>();
     @Nullable
     private AutoTileManager mAutoTiles;
-    private final StatusBarIconController mIconController;
     private final ArrayList<QSFactory> mQsFactories = new ArrayList<>();
     private int mCurrentUser;
     private final Optional<CentralSurfaces> mCentralSurfacesOptional;
@@ -135,7 +127,6 @@
 
     @Inject
     public QSTileHost(Context context,
-            StatusBarIconController iconController,
             QSFactory defaultFactory,
             @Main Executor mainExecutor,
             PluginManager pluginManager,
@@ -152,7 +143,6 @@
             TileLifecycleManager.Factory tileLifecycleManagerFactory,
             UserFileManager userFileManager
     ) {
-        mIconController = iconController;
         mContext = context;
         mUserContext = context;
         mTunerService = tunerService;
@@ -186,10 +176,6 @@
         });
     }
 
-    public StatusBarIconController getIconController() {
-        return mIconController;
-    }
-
     @Override
     public InstanceId getNewInstanceId() {
         return mInstanceIdSequence.newInstanceId();
@@ -314,7 +300,6 @@
         if (!TILES_SETTING.equals(key)) {
             return;
         }
-        Log.d(TAG, "Recreating tiles");
         if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
             newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
         }
@@ -327,6 +312,7 @@
             }
         }
         if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
+        Log.d(TAG, "Recreating tiles: " + tileSpecs);
         mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
                 tile -> {
                     Log.d(TAG, "Destroying tile: " + tile.getKey());
@@ -372,6 +358,8 @@
                             Log.d(TAG, "Destroying not available tile: " + tileSpec);
                             mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
                         }
+                    } else {
+                        Log.d(TAG, "No factory for a spec: " + tileSpec);
                     }
                 } catch (Throwable t) {
                     Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
@@ -436,12 +424,7 @@
         addTile(spec, POSITION_AT_END);
     }
 
-    /**
-     * Add a tile into the requested spot, or at the end if the position is greater than the number
-     * of tiles.
-     * @param spec string matching a pre-defined tilespec
-     * @param requestPosition -1 for end, 0 for beginning, or X for insertion at position X
-     */
+    @Override
     public void addTile(String spec, int requestPosition) {
         mMainExecutor.execute(() ->
                 changeTileSpecs(tileSpecs -> {
@@ -481,15 +464,12 @@
         }
     }
 
+    @Override
     public void addTile(ComponentName tile) {
         addTile(tile, /* end */ false);
     }
 
-    /**
-     * Adds a custom tile to the set of current tiles.
-     * @param tile the component name of the {@link android.service.quicksettings.TileService}
-     * @param end if true, the tile will be added at the end. If false, at the beginning.
-     */
+    @Override
     public void addTile(ComponentName tile, boolean end) {
         String spec = CustomTile.toSpec(tile);
         addTile(spec, end ? POSITION_AT_END : 0);
@@ -499,6 +479,7 @@
      * This will call through {@link #changeTilesByUser}. It should only be used when a tile is
      * removed by a <b>user action</b> like {@code adb}.
      */
+    @Override
     public void removeTileByUser(ComponentName tile) {
         mMainExecutor.execute(() -> {
             List<String> newSpecs = new ArrayList<>(mTileSpecs);
@@ -517,6 +498,7 @@
      * that are removed.
      */
     @MainThread
+    @Override
     public void changeTilesByUser(List<String> previousTiles, List<String> newTiles) {
         final List<String> copy = new ArrayList<>(previousTiles);
         final int NP = copy.size();
@@ -540,8 +522,8 @@
         saveTilesToSettings(newTiles);
     }
 
-    /** Create a {@link QSTile} of a {@code tileSpec} type. */
     @Nullable
+    @Override
     public QSTile createTile(String tileSpec) {
         for (int i = 0; i < mQsFactories.size(); i++) {
             QSTile t = mQsFactories.get(i).createTile(tileSpec);
@@ -552,11 +534,7 @@
         return null;
     }
 
-    /**
-     * Create a view for a tile, iterating over all possible {@link QSFactory}.
-     *
-     * @see QSFactory#createTileView
-     */
+    @Override
     public QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView) {
         for (int i = 0; i < mQsFactories.size(); i++) {
             QSTileView view = mQsFactories.get(i)
@@ -576,6 +554,7 @@
      *                      tile.
      * @param userId the user to check
      */
+    @Override
     public boolean isTileAdded(ComponentName componentName, int userId) {
         return mUserFileManager
                 .getSharedPreferences(TILES, 0, userId)
@@ -591,6 +570,7 @@
      * @param userId the user for this tile
      * @param added {@code true} if the tile is being added, {@code false} otherwise
      */
+    @Override
     public void setTileAdded(ComponentName componentName, int userId, boolean added) {
         mUserFileManager.getSharedPreferences(TILES, 0, userId)
                 .edit()
@@ -598,6 +578,11 @@
                 .apply();
     }
 
+    @Override
+    public List<String> getSpecs() {
+        return mTileSpecs;
+    }
+
     protected static List<String> loadTileSpecs(Context context, String tileList) {
         final Resources res = context.getResources();
 
@@ -615,7 +600,7 @@
             if (tile.isEmpty()) continue;
             if (tile.equals("default")) {
                 if (!addedDefault) {
-                    List<String> defaultSpecs = getDefaultSpecs(context);
+                    List<String> defaultSpecs = QSHost.getDefaultSpecs(context);
                     for (String spec : defaultSpecs) {
                         if (!addedSpecs.contains(spec)) {
                             tiles.add(spec);
@@ -648,25 +633,6 @@
         return tiles;
     }
 
-    /**
-     * Returns the default QS tiles for the context.
-     * @param context the context to obtain the resources from
-     * @return a list of specs of the default tiles
-     */
-    public static List<String> getDefaultSpecs(Context context) {
-        final ArrayList<String> tiles = new ArrayList<String>();
-
-        final Resources res = context.getResources();
-        final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
-
-        tiles.addAll(Arrays.asList(defaultTileList.split(",")));
-        if (Build.IS_DEBUGGABLE
-                && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) {
-            tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
-        return tiles;
-    }
-
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("QSTileHost:");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 6aabe3b..2d54313 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -48,7 +48,7 @@
     private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider;
 
     @Inject
-    QuickQSPanelController(QuickQSPanel view, QSTileHost qsTileHost,
+    QuickQSPanelController(QuickQSPanel view, QSHost qsHost,
             QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             @Named(QUICK_QS_PANEL) MediaHost mediaHost,
@@ -57,7 +57,7 @@
             MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
             DumpManager dumpManager
     ) {
-        super(view, qsTileHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
+        super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
                 uiEventLogger, qsLogger, dumpManager);
         mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 946fe54..691a1a1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -20,35 +20,15 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.Color;
-import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.DisplayCutout;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowInsets;
 import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.Space;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.internal.policy.SystemBarUtils;
-import com.android.settingslib.Utils;
 import com.android.systemui.R;
-import com.android.systemui.battery.BatteryMeterView;
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
-import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
-import com.android.systemui.statusbar.phone.StatusIconContainer;
-import com.android.systemui.statusbar.policy.Clock;
-import com.android.systemui.statusbar.policy.VariableDateView;
 import com.android.systemui.util.LargeScreenUtils;
 
-import java.util.List;
-
 /**
  * View that contains the top-most bits of the QS panel (primarily the status bar with date, time,
  * battery, carrier info and privacy icons) and also contains the {@link QuickQSPanel}.
@@ -58,184 +38,30 @@
     private boolean mExpanded;
     private boolean mQsDisabled;
 
-    @Nullable
-    private TouchAnimator mAlphaAnimator;
-    @Nullable
-    private TouchAnimator mTranslationAnimator;
-    @Nullable
-    private TouchAnimator mIconsAlphaAnimator;
-    private TouchAnimator mIconsAlphaAnimatorFixed;
-
     protected QuickQSPanel mHeaderQsPanel;
-    private View mDatePrivacyView;
-    private View mDateView;
-    // DateView next to clock. Visible on QQS
-    private VariableDateView mClockDateView;
-    private View mStatusIconsView;
-    private View mContainer;
-
-    private View mQSCarriers;
-    private ViewGroup mClockContainer;
-    private Clock mClockView;
-    private Space mDatePrivacySeparator;
-    private View mClockIconsSeparator;
-    private boolean mShowClockIconsSeparator;
-    private View mRightLayout;
-    private View mDateContainer;
-    private View mPrivacyContainer;
-
-    private BatteryMeterView mBatteryRemainingIcon;
-    private StatusIconContainer mIconContainer;
-    private View mPrivacyChip;
-
-    @Nullable
-    private TintedIconManager mTintedIconManager;
-    @Nullable
-    private QSExpansionPathInterpolator mQSExpansionPathInterpolator;
-    private StatusBarContentInsetsProvider mInsetsProvider;
-
-    private int mRoundedCornerPadding = 0;
-    private int mWaterfallTopInset;
-    private int mCutOutPaddingLeft;
-    private int mCutOutPaddingRight;
-    private float mKeyguardExpansionFraction;
-    private int mTextColorPrimary = Color.TRANSPARENT;
-    private int mTopViewMeasureHeight;
-
-    @NonNull
-    private List<String> mRssiIgnoredSlots = List.of();
-    private boolean mIsSingleCarrier;
-
-    private boolean mHasCenterCutout;
-    private boolean mConfigShowBatteryEstimate;
-
-    private boolean mUseCombinedQSHeader;
 
     public QuickStatusBarHeader(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    /**
-     * How much the view containing the clock and QQS will translate down when QS is fully expanded.
-     *
-     * This matches the measured height of the view containing the date and privacy icons.
-     */
-    public int getOffsetTranslation() {
-        return mTopViewMeasureHeight;
-    }
-
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-
         mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
-        mDatePrivacyView = findViewById(R.id.quick_status_bar_date_privacy);
-        mStatusIconsView = findViewById(R.id.quick_qs_status_icons);
-        mQSCarriers = findViewById(R.id.carrier_group);
-        mContainer = findViewById(R.id.qs_container);
-        mIconContainer = findViewById(R.id.statusIcons);
-        mPrivacyChip = findViewById(R.id.privacy_chip);
-        mDateView = findViewById(R.id.date);
-        mClockDateView = findViewById(R.id.date_clock);
-        mClockIconsSeparator = findViewById(R.id.separator);
-        mRightLayout = findViewById(R.id.rightLayout);
-        mDateContainer = findViewById(R.id.date_container);
-        mPrivacyContainer = findViewById(R.id.privacy_container);
-
-        mClockContainer = findViewById(R.id.clock_container);
-        mClockView = findViewById(R.id.clock);
-        mDatePrivacySeparator = findViewById(R.id.space);
-        // Tint for the battery icons are handled in setupHost()
-        mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon);
 
         updateResources();
-        Configuration config = mContext.getResources().getConfiguration();
-        setDatePrivacyContainersWidth(config.orientation == Configuration.ORIENTATION_LANDSCAPE);
-
-        // QS will always show the estimate, and BatteryMeterView handles the case where
-        // it's unavailable or charging
-        mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
-
-        mIconsAlphaAnimatorFixed = new TouchAnimator.Builder()
-                .addFloat(mIconContainer, "alpha", 0, 1)
-                .addFloat(mBatteryRemainingIcon, "alpha", 0, 1)
-                .build();
-    }
-
-    void onAttach(TintedIconManager iconManager,
-            QSExpansionPathInterpolator qsExpansionPathInterpolator,
-            List<String> rssiIgnoredSlots,
-            StatusBarContentInsetsProvider insetsProvider,
-            boolean useCombinedQSHeader) {
-        mUseCombinedQSHeader = useCombinedQSHeader;
-        mTintedIconManager = iconManager;
-        mRssiIgnoredSlots = rssiIgnoredSlots;
-        mInsetsProvider = insetsProvider;
-        int fillColor = Utils.getColorAttrDefaultColor(getContext(),
-                android.R.attr.textColorPrimary);
-
-        // Set the correct tint for the status icons so they contrast
-        iconManager.setTint(fillColor);
-
-        mQSExpansionPathInterpolator = qsExpansionPathInterpolator;
-        updateAnimators();
-    }
-
-    void setIsSingleCarrier(boolean isSingleCarrier) {
-        mIsSingleCarrier = isSingleCarrier;
-        updateAlphaAnimator();
-    }
-
-    public QuickQSPanel getHeaderQsPanel() {
-        return mHeaderQsPanel;
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        if (mDatePrivacyView.getMeasuredHeight() != mTopViewMeasureHeight) {
-            mTopViewMeasureHeight = mDatePrivacyView.getMeasuredHeight();
-            post(this::updateAnimators);
-        }
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updateResources();
-        setDatePrivacyContainersWidth(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
-    }
-
-    @Override
-    public void onRtlPropertiesChanged(int layoutDirection) {
-        super.onRtlPropertiesChanged(layoutDirection);
-        updateResources();
-    }
-
-    private void setDatePrivacyContainersWidth(boolean landscape) {
-        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mDateContainer.getLayoutParams();
-        lp.width = landscape ? WRAP_CONTENT : 0;
-        lp.weight = landscape ? 0f : 1f;
-        mDateContainer.setLayoutParams(lp);
-
-        lp = (LinearLayout.LayoutParams) mPrivacyContainer.getLayoutParams();
-        lp.width = landscape ? WRAP_CONTENT : 0;
-        lp.weight = landscape ? 0f : 1f;
-        mPrivacyContainer.setLayoutParams(lp);
-    }
-
-    private void updateBatteryMode() {
-        if (mConfigShowBatteryEstimate && !mHasCenterCutout) {
-            mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
-        } else {
-            mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ON);
-        }
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        // If using combined headers, only react to touches inside QuickQSPanel
-        if (!mUseCombinedQSHeader || event.getY() > mHeaderQsPanel.getTop()) {
+        // Only react to touches inside QuickQSPanel
+        if (event.getY() > mHeaderQsPanel.getTop()) {
             return super.onTouchEvent(event);
         } else {
             return false;
@@ -247,193 +73,29 @@
         boolean largeScreenHeaderActive =
                 LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources);
 
-        boolean gone = largeScreenHeaderActive || mUseCombinedQSHeader || mQsDisabled;
-        mStatusIconsView.setVisibility(gone ? View.GONE : View.VISIBLE);
-        mDatePrivacyView.setVisibility(gone ? View.GONE : View.VISIBLE);
-
-        mConfigShowBatteryEstimate = resources.getBoolean(R.bool.config_showBatteryEstimateQSBH);
-
-        mRoundedCornerPadding = resources.getDimensionPixelSize(
-                R.dimen.rounded_corner_content_padding);
-
-        int qsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(mContext);
-
-        mDatePrivacyView.getLayoutParams().height =
-                Math.max(qsOffsetHeight, mDatePrivacyView.getMinimumHeight());
-        mDatePrivacyView.setLayoutParams(mDatePrivacyView.getLayoutParams());
-
-        mStatusIconsView.getLayoutParams().height =
-                Math.max(qsOffsetHeight, mStatusIconsView.getMinimumHeight());
-        mStatusIconsView.setLayoutParams(mStatusIconsView.getLayoutParams());
-
         ViewGroup.LayoutParams lp = getLayoutParams();
         if (mQsDisabled) {
-            lp.height = mStatusIconsView.getLayoutParams().height;
+            lp.height = 0;
         } else {
             lp.height = WRAP_CONTENT;
         }
         setLayoutParams(lp);
 
-        int textColor = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary);
-        if (textColor != mTextColorPrimary) {
-            int textColorSecondary = Utils.getColorAttrDefaultColor(mContext,
-                    android.R.attr.textColorSecondary);
-            mTextColorPrimary = textColor;
-            mClockView.setTextColor(textColor);
-            if (mTintedIconManager != null) {
-                mTintedIconManager.setTint(textColor);
-            }
-            mBatteryRemainingIcon.updateColors(mTextColorPrimary, textColorSecondary,
-                    mTextColorPrimary);
-        }
-
         MarginLayoutParams qqsLP = (MarginLayoutParams) mHeaderQsPanel.getLayoutParams();
         if (largeScreenHeaderActive) {
             qqsLP.topMargin = mContext.getResources()
                     .getDimensionPixelSize(R.dimen.qqs_layout_margin_top);
-        } else if (!mUseCombinedQSHeader) {
-            qqsLP.topMargin = qsOffsetHeight;
         } else {
             qqsLP.topMargin = mContext.getResources()
                     .getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height);
         }
         mHeaderQsPanel.setLayoutParams(qqsLP);
-
-        updateBatteryMode();
-        updateHeadersPadding();
-        updateAnimators();
-
-        updateClockDatePadding();
     }
 
-    private void updateClockDatePadding() {
-        int startPadding = mContext.getResources()
-                .getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding);
-        int endPadding = mContext.getResources()
-                .getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding);
-        mClockView.setPaddingRelative(
-                startPadding,
-                mClockView.getPaddingTop(),
-                endPadding,
-                mClockView.getPaddingBottom()
-        );
-
-        MarginLayoutParams lp = (MarginLayoutParams) mClockDateView.getLayoutParams();
-        lp.setMarginStart(endPadding);
-        mClockDateView.setLayoutParams(lp);
-    }
-
-    private void updateAnimators() {
-        if (mUseCombinedQSHeader) {
-            mTranslationAnimator = null;
-            return;
-        }
-        updateAlphaAnimator();
-        int offset = mTopViewMeasureHeight;
-
-        mTranslationAnimator = new TouchAnimator.Builder()
-                .addFloat(mContainer, "translationY", 0, offset)
-                .setInterpolator(mQSExpansionPathInterpolator != null
-                        ? mQSExpansionPathInterpolator.getYInterpolator()
-                        : null)
-                .build();
-    }
-
-    private void updateAlphaAnimator() {
-        if (mUseCombinedQSHeader) {
-            mAlphaAnimator = null;
-            return;
-        }
-        TouchAnimator.Builder builder = new TouchAnimator.Builder()
-                // These views appear on expanding down
-                .addFloat(mDateView, "alpha", 0, 0, 1)
-                .addFloat(mClockDateView, "alpha", 1, 0, 0)
-                .addFloat(mQSCarriers, "alpha", 0, 1)
-                .setListener(new TouchAnimator.ListenerAdapter() {
-                    @Override
-                    public void onAnimationAtEnd() {
-                        super.onAnimationAtEnd();
-                        if (!mIsSingleCarrier) {
-                            mIconContainer.addIgnoredSlots(mRssiIgnoredSlots);
-                        }
-                        // Make it gone so there's enough room for carrier names
-                        mClockDateView.setVisibility(View.GONE);
-                    }
-
-                    @Override
-                    public void onAnimationStarted() {
-                        mClockDateView.setVisibility(View.VISIBLE);
-                        mClockDateView.setFreezeSwitching(true);
-                        setSeparatorVisibility(false);
-                        if (!mIsSingleCarrier) {
-                            mIconContainer.addIgnoredSlots(mRssiIgnoredSlots);
-                        }
-                    }
-
-                    @Override
-                    public void onAnimationAtStart() {
-                        super.onAnimationAtStart();
-                        mClockDateView.setFreezeSwitching(false);
-                        mClockDateView.setVisibility(View.VISIBLE);
-                        setSeparatorVisibility(mShowClockIconsSeparator);
-                        // In QQS we never ignore RSSI.
-                        mIconContainer.removeIgnoredSlots(mRssiIgnoredSlots);
-                    }
-                });
-        mAlphaAnimator = builder.build();
-    }
-
-    void setChipVisibility(boolean visibility) {
-        if (visibility) {
-            // Animates the icons and battery indicator from alpha 0 to 1, when the chip is visible
-            mIconsAlphaAnimator = mIconsAlphaAnimatorFixed;
-            mIconsAlphaAnimator.setPosition(mKeyguardExpansionFraction);
-        } else {
-            mIconsAlphaAnimator = null;
-            mIconContainer.setAlpha(1);
-            mBatteryRemainingIcon.setAlpha(1);
-        }
-
-    }
-
-    /** */
     public void setExpanded(boolean expanded, QuickQSPanelController quickQSPanelController) {
         if (mExpanded == expanded) return;
         mExpanded = expanded;
         quickQSPanelController.setExpanded(expanded);
-        updateEverything();
-    }
-
-    /**
-     * Animates the inner contents based on the given expansion details.
-     *
-     * @param forceExpanded whether we should show the state expanded forcibly
-     * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f)
-     * @param panelTranslationY how much the panel has physically moved down vertically (required
-     *                          for keyguard animations only)
-     */
-    public void setExpansion(boolean forceExpanded, float expansionFraction,
-                             float panelTranslationY) {
-        final float keyguardExpansionFraction = forceExpanded ? 1f : expansionFraction;
-
-        if (mAlphaAnimator != null) {
-            mAlphaAnimator.setPosition(keyguardExpansionFraction);
-        }
-        if (mTranslationAnimator != null) {
-            mTranslationAnimator.setPosition(keyguardExpansionFraction);
-        }
-        if (mIconsAlphaAnimator != null) {
-            mIconsAlphaAnimator.setPosition(keyguardExpansionFraction);
-        }
-        // If forceExpanded (we are opening QS from lockscreen), the animators have been set to
-        // position = 1f.
-        if (forceExpanded) {
-            setAlpha(expansionFraction);
-        } else {
-            setAlpha(1);
-        }
-
-        mKeyguardExpansionFraction = keyguardExpansionFraction;
     }
 
     public void disable(int state1, int state2, boolean animate) {
@@ -441,133 +103,13 @@
         if (disabled == mQsDisabled) return;
         mQsDisabled = disabled;
         mHeaderQsPanel.setDisabledByPolicy(disabled);
-        mStatusIconsView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
         updateResources();
     }
 
-    @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        // Handle padding of the views
-        DisplayCutout cutout = insets.getDisplayCutout();
-
-        Pair<Integer, Integer> sbInsets = mInsetsProvider
-                .getStatusBarContentInsetsForCurrentRotation();
-        boolean hasCornerCutout = mInsetsProvider.currentRotationHasCornerCutout();
-
-        mDatePrivacyView.setPadding(sbInsets.first, 0, sbInsets.second, 0);
-        mStatusIconsView.setPadding(sbInsets.first, 0, sbInsets.second, 0);
-        LinearLayout.LayoutParams datePrivacySeparatorLayoutParams =
-                (LinearLayout.LayoutParams) mDatePrivacySeparator.getLayoutParams();
-        LinearLayout.LayoutParams mClockIconsSeparatorLayoutParams =
-                (LinearLayout.LayoutParams) mClockIconsSeparator.getLayoutParams();
-        if (cutout != null) {
-            Rect topCutout = cutout.getBoundingRectTop();
-            if (topCutout.isEmpty() || hasCornerCutout) {
-                datePrivacySeparatorLayoutParams.width = 0;
-                mDatePrivacySeparator.setVisibility(View.GONE);
-                mClockIconsSeparatorLayoutParams.width = 0;
-                setSeparatorVisibility(false);
-                mShowClockIconsSeparator = false;
-                mHasCenterCutout = false;
-            } else {
-                datePrivacySeparatorLayoutParams.width = topCutout.width();
-                mDatePrivacySeparator.setVisibility(View.VISIBLE);
-                mClockIconsSeparatorLayoutParams.width = topCutout.width();
-                mShowClockIconsSeparator = true;
-                setSeparatorVisibility(mKeyguardExpansionFraction == 0f);
-                mHasCenterCutout = true;
-            }
-        }
-        mDatePrivacySeparator.setLayoutParams(datePrivacySeparatorLayoutParams);
-        mClockIconsSeparator.setLayoutParams(mClockIconsSeparatorLayoutParams);
-        mCutOutPaddingLeft = sbInsets.first;
-        mCutOutPaddingRight = sbInsets.second;
-        mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top;
-
-        updateBatteryMode();
-        updateHeadersPadding();
-        return super.onApplyWindowInsets(insets);
-    }
-
-    /**
-     * Sets the visibility of the separator between clock and icons.
-     *
-     * This separator is "visible" when there is a center cutout, to block that space. In that
-     * case, the clock and the layout on the right (containing the icons and the battery meter) are
-     * set to weight 1 to take the available space.
-     * @param visible whether the separator between clock and icons should be visible.
-     */
-    private void setSeparatorVisibility(boolean visible) {
-        int newVisibility = visible ? View.VISIBLE : View.GONE;
-        if (mClockIconsSeparator.getVisibility() == newVisibility) return;
-
-        mClockIconsSeparator.setVisibility(visible ? View.VISIBLE : View.GONE);
-        mQSCarriers.setVisibility(visible ? View.GONE : View.VISIBLE);
-
-        LinearLayout.LayoutParams lp =
-                (LinearLayout.LayoutParams) mClockContainer.getLayoutParams();
-        lp.width = visible ? 0 : WRAP_CONTENT;
-        lp.weight = visible ? 1f : 0f;
-        mClockContainer.setLayoutParams(lp);
-
-        lp = (LinearLayout.LayoutParams) mRightLayout.getLayoutParams();
-        lp.width = visible ? 0 : WRAP_CONTENT;
-        lp.weight = visible ? 1f : 0f;
-        mRightLayout.setLayoutParams(lp);
-    }
-
-    private void updateHeadersPadding() {
-        setContentMargins(mDatePrivacyView, 0, 0);
-        setContentMargins(mStatusIconsView, 0, 0);
-        int paddingLeft = 0;
-        int paddingRight = 0;
-
-        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
-        int leftMargin = lp.leftMargin;
-        int rightMargin = lp.rightMargin;
-
-        // The clock might collide with cutouts, let's shift it out of the way.
-        // We only do that if the inset is bigger than our own padding, since it's nicer to
-        // align with
-        if (mCutOutPaddingLeft > 0) {
-            // if there's a cutout, let's use at least the rounded corner inset
-            int cutoutPadding = Math.max(mCutOutPaddingLeft, mRoundedCornerPadding);
-            paddingLeft = Math.max(cutoutPadding - leftMargin, 0);
-        }
-        if (mCutOutPaddingRight > 0) {
-            // if there's a cutout, let's use at least the rounded corner inset
-            int cutoutPadding = Math.max(mCutOutPaddingRight, mRoundedCornerPadding);
-            paddingRight = Math.max(cutoutPadding - rightMargin, 0);
-        }
-
-        mDatePrivacyView.setPadding(paddingLeft,
-                mWaterfallTopInset,
-                paddingRight,
-                0);
-        mStatusIconsView.setPadding(paddingLeft,
-                mWaterfallTopInset,
-                paddingRight,
-                0);
-    }
-
-    public void updateEverything() {
-        post(() -> setClickable(!mExpanded));
-    }
-
     private void setContentMargins(View view, int marginStart, int marginEnd) {
         MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
         lp.setMarginStart(marginStart);
         lp.setMarginEnd(marginEnd);
         view.setLayoutParams(lp);
     }
-
-    /**
-     * Scroll the headers away.
-     *
-     * @param scrollY the scroll of the QSPanel container
-     */
-    public void setExpandedScrollAmount(int scrollY) {
-        mStatusIconsView.setScrollY(scrollY);
-        mDatePrivacyView.setScrollY(scrollY);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index ccaab1a..64960e6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -16,154 +16,38 @@
 
 package com.android.systemui.qs;
 
-import android.os.Bundle;
-
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.systemui.R;
-import com.android.systemui.battery.BatteryMeterViewController;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.demomode.DemoMode;
-import com.android.systemui.demomode.DemoModeController;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.qs.carrier.QSCarrierGroupController;
 import com.android.systemui.qs.dagger.QSScope;
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
-import com.android.systemui.statusbar.phone.StatusBarLocation;
-import com.android.systemui.statusbar.phone.StatusIconContainer;
-import com.android.systemui.statusbar.policy.Clock;
-import com.android.systemui.statusbar.policy.VariableDateViewController;
 import com.android.systemui.util.ViewController;
 
-import java.util.List;
-
 import javax.inject.Inject;
 
 /**
  * Controller for {@link QuickStatusBarHeader}.
  */
 @QSScope
-class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader> implements
-        ChipVisibilityListener {
+class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader> {
 
-    private final QSCarrierGroupController mQSCarrierGroupController;
     private final QuickQSPanelController mQuickQSPanelController;
-    private final Clock mClockView;
-    private final StatusBarIconController mStatusBarIconController;
-    private final DemoModeController mDemoModeController;
-    private final StatusIconContainer mIconContainer;
-    private final StatusBarIconController.TintedIconManager mIconManager;
-    private final DemoMode mDemoModeReceiver;
-    private final QSExpansionPathInterpolator mQSExpansionPathInterpolator;
-    private final FeatureFlags mFeatureFlags;
-    private final BatteryMeterViewController mBatteryMeterViewController;
-    private final StatusBarContentInsetsProvider mInsetsProvider;
-
-    private final VariableDateViewController mVariableDateViewControllerDateView;
-    private final VariableDateViewController mVariableDateViewControllerClockDateView;
-    private final HeaderPrivacyIconsController mPrivacyIconsController;
-
     private boolean mListening;
 
-    private SysuiColorExtractor mColorExtractor;
-    private ColorExtractor.OnColorsChangedListener mOnColorsChangedListener;
-
     @Inject
     QuickStatusBarHeaderController(QuickStatusBarHeader view,
-            HeaderPrivacyIconsController headerPrivacyIconsController,
-            StatusBarIconController statusBarIconController,
-            DemoModeController demoModeController,
-            QuickQSPanelController quickQSPanelController,
-            QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder,
-            SysuiColorExtractor colorExtractor,
-            QSExpansionPathInterpolator qsExpansionPathInterpolator,
-            FeatureFlags featureFlags,
-            VariableDateViewController.Factory variableDateViewControllerFactory,
-            BatteryMeterViewController batteryMeterViewController,
-            StatusBarContentInsetsProvider statusBarContentInsetsProvider,
-            StatusBarIconController.TintedIconManager.Factory tintedIconManagerFactory) {
+            QuickQSPanelController quickQSPanelController
+    ) {
         super(view);
-        mPrivacyIconsController = headerPrivacyIconsController;
-        mStatusBarIconController = statusBarIconController;
-        mDemoModeController = demoModeController;
         mQuickQSPanelController = quickQSPanelController;
-        mQSExpansionPathInterpolator = qsExpansionPathInterpolator;
-        mFeatureFlags = featureFlags;
-        mBatteryMeterViewController = batteryMeterViewController;
-        mInsetsProvider = statusBarContentInsetsProvider;
-
-        mQSCarrierGroupController = qsCarrierGroupControllerBuilder
-                .setQSCarrierGroup(mView.findViewById(R.id.carrier_group))
-                .build();
-        mClockView = mView.findViewById(R.id.clock);
-        mIconContainer = mView.findViewById(R.id.statusIcons);
-        mVariableDateViewControllerDateView = variableDateViewControllerFactory.create(
-                mView.requireViewById(R.id.date)
-        );
-        mVariableDateViewControllerClockDateView = variableDateViewControllerFactory.create(
-                mView.requireViewById(R.id.date_clock)
-        );
-
-        mIconManager = tintedIconManagerFactory.create(mIconContainer, StatusBarLocation.QS);
-        mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
-        mColorExtractor = colorExtractor;
-        mOnColorsChangedListener = (extractor, which) -> {
-            final boolean lightTheme = mColorExtractor.getNeutralColors().supportsDarkText();
-            mClockView.onColorsChanged(lightTheme);
-        };
-        mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
-
-        // Don't need to worry about tuner settings for this icon
-        mBatteryMeterViewController.ignoreTunerUpdates();
-    }
-
-    @Override
-    protected void onInit() {
-        mBatteryMeterViewController.init();
     }
 
     @Override
     protected void onViewAttached() {
-        mPrivacyIconsController.onParentVisible();
-        mPrivacyIconsController.setChipVisibilityListener(this);
-        mIconContainer.addIgnoredSlot(
-                getResources().getString(com.android.internal.R.string.status_bar_managed_profile));
-        mIconContainer.addIgnoredSlot(
-                getResources().getString(com.android.internal.R.string.status_bar_alarm_clock));
-        mIconContainer.setShouldRestrictIcons(false);
-        mStatusBarIconController.addIconGroup(mIconManager);
-
-        mView.setIsSingleCarrier(mQSCarrierGroupController.isSingleCarrier());
-        mQSCarrierGroupController
-                .setOnSingleCarrierChangedListener(mView::setIsSingleCarrier);
-
-        List<String> rssiIgnoredSlots = List.of(
-                getResources().getString(com.android.internal.R.string.status_bar_mobile)
-        );
-
-        mView.onAttach(mIconManager, mQSExpansionPathInterpolator, rssiIgnoredSlots,
-                mInsetsProvider, mFeatureFlags.isEnabled(Flags.COMBINED_QS_HEADERS));
-
-        mDemoModeController.addCallback(mDemoModeReceiver);
-
-        mVariableDateViewControllerDateView.init();
-        mVariableDateViewControllerClockDateView.init();
     }
 
     @Override
     protected void onViewDetached() {
-        mColorExtractor.removeOnColorsChangedListener(mOnColorsChangedListener);
-        mPrivacyIconsController.onParentInvisible();
-        mStatusBarIconController.removeIconGroup(mIconManager);
-        mQSCarrierGroupController.setOnSingleCarrierChangedListener(null);
-        mDemoModeController.removeCallback(mDemoModeReceiver);
         setListening(false);
     }
 
     public void setListening(boolean listening) {
-        mQSCarrierGroupController.setListening(listening);
-
         if (listening == mListening) {
             return;
         }
@@ -174,48 +58,9 @@
         if (mQuickQSPanelController.switchTileLayout(false)) {
             mView.updateResources();
         }
-
-        if (listening) {
-            mPrivacyIconsController.startListening();
-        } else {
-            mPrivacyIconsController.stopListening();
-        }
-    }
-
-    @Override
-    public void onChipVisibilityRefreshed(boolean visible) {
-        mView.setChipVisibility(visible);
     }
 
     public void setContentMargins(int marginStart, int marginEnd) {
         mQuickQSPanelController.setContentMargins(marginStart, marginEnd);
     }
-
-    private static class ClockDemoModeReceiver implements DemoMode {
-        private Clock mClockView;
-
-        @Override
-        public List<String> demoCommands() {
-            return List.of(COMMAND_CLOCK);
-        }
-
-        ClockDemoModeReceiver(Clock clockView) {
-            mClockView = clockView;
-        }
-
-        @Override
-        public void dispatchDemoCommand(String command, Bundle args) {
-            mClockView.dispatchDemoCommand(command, args);
-        }
-
-        @Override
-        public void onDemoModeStarted() {
-            mClockView.onDemoModeStarted();
-        }
-
-        @Override
-        public void onDemoModeFinished() {
-            mClockView.onDemoModeFinished();
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
index 39d081d..36dc743 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
@@ -83,8 +83,7 @@
                     if (mListeners.size() > 0) {
                         mSecureSettings.unregisterContentObserver(mContentObserver);
                         mSecureSettings.registerContentObserverForUser(
-                                Settings.Secure.getUriFor(
-                                        Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED),
+                                Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
                                 false, mContentObserver, newUser);
                     }
                 }
@@ -100,8 +99,7 @@
                 mListeners.add(listener);
                 if (mListeners.size() == 1) {
                     mSecureSettings.registerContentObserverForUser(
-                            Settings.Secure.getUriFor(
-                                    Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED),
+                            Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
                             false, mContentObserver, mUserTracker.getUserId());
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
index 9739011..a319fb8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
@@ -40,7 +40,7 @@
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSEditEvent;
 import com.android.systemui.qs.QSFragment;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -57,7 +57,7 @@
 @QSScope
 public class QSCustomizerController extends ViewController<QSCustomizer> {
     private final TileQueryHelper mTileQueryHelper;
-    private final QSTileHost mQsTileHost;
+    private final QSHost mQsHost;
     private final TileAdapter mTileAdapter;
     private final ScreenLifecycle mScreenLifecycle;
     private final KeyguardStateController mKeyguardStateController;
@@ -104,12 +104,12 @@
 
     @Inject
     protected QSCustomizerController(QSCustomizer view, TileQueryHelper tileQueryHelper,
-            QSTileHost qsTileHost, TileAdapter tileAdapter, ScreenLifecycle screenLifecycle,
+            QSHost qsHost, TileAdapter tileAdapter, ScreenLifecycle screenLifecycle,
             KeyguardStateController keyguardStateController, LightBarController lightBarController,
             ConfigurationController configurationController, UiEventLogger uiEventLogger) {
         super(view);
         mTileQueryHelper = tileQueryHelper;
-        mQsTileHost = qsTileHost;
+        mQsHost = qsHost;
         mTileAdapter = tileAdapter;
         mScreenLifecycle = screenLifecycle;
         mKeyguardStateController = keyguardStateController;
@@ -175,7 +175,7 @@
 
 
     private void reset() {
-        mTileAdapter.resetTileSpecs(QSTileHost.getDefaultSpecs(getContext()));
+        mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext()));
     }
 
     public boolean isCustomizing() {
@@ -192,7 +192,7 @@
                 mView.show(x, y, mTileAdapter);
                 mUiEventLogger.log(QSEditEvent.QS_EDIT_OPEN);
             }
-            mTileQueryHelper.queryTiles(mQsTileHost);
+            mTileQueryHelper.queryTiles(mQsHost);
             mKeyguardStateController.addCallback(mKeyguardCallback);
             mView.updateNavColors(mLightBarController);
         }
@@ -258,13 +258,13 @@
 
     private void save() {
         if (mTileQueryHelper.isFinished()) {
-            mTileAdapter.saveSpecs(mQsTileHost);
+            mTileAdapter.saveSpecs(mQsHost);
         }
     }
 
     private void setTileSpecs() {
         List<String> specs = new ArrayList<>();
-        for (QSTile tile : mQsTileHost.getTiles()) {
+        for (QSTile tile : mQsHost.getTiles()) {
             specs.add(tile.getTileSpec());
         }
         mTileAdapter.setTileSpecs(specs);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index d84b12c..6a05684 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -45,7 +45,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.qs.QSEditEvent;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.customize.TileAdapter.Holder;
 import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
 import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
@@ -91,7 +91,7 @@
     private ItemDecoration mDecoration;
     private final MarginTileDecoration mMarginDecoration;
     private final int mMinNumTiles;
-    private final QSTileHost mHost;
+    private final QSHost mHost;
     private int mEditIndex;
     private int mTileDividerIndex;
     private int mFocusIndex;
@@ -117,7 +117,7 @@
     @Inject
     public TileAdapter(
             @QSThemedContext Context context,
-            QSTileHost qsHost,
+            QSHost qsHost,
             UiEventLogger uiEventLogger) {
         mContext = context;
         mHost = qsHost;
@@ -176,7 +176,7 @@
         mMarginDecoration.setHalfMargin(halfMargin);
     }
 
-    public void saveSpecs(QSTileHost host) {
+    public void saveSpecs(QSHost host) {
         List<String> newSpecs = new ArrayList<>();
         clearAccessibilityState();
         for (int i = 1; i < mTiles.size() && mTiles.get(i) != null; i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 32a7916..d9f4484 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -38,7 +38,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTile.State;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
@@ -85,7 +85,7 @@
         mListener = listener;
     }
 
-    public void queryTiles(QSTileHost host) {
+    public void queryTiles(QSHost host) {
         mTiles.clear();
         mSpecs.clear();
         mFinished = false;
@@ -97,7 +97,7 @@
         return mFinished;
     }
 
-    private void addCurrentAndStockTiles(QSTileHost host) {
+    private void addCurrentAndStockTiles(QSHost host) {
         String stock = mContext.getString(R.string.quick_settings_tiles_stock);
         String current = Settings.Secure.getString(mContext.getContentResolver(),
                 Settings.Secure.QS_TILES);
@@ -153,14 +153,14 @@
     private class TileCollector implements QSTile.Callback {
 
         private final List<TilePair> mQSTileList = new ArrayList<>();
-        private final QSTileHost mQSTileHost;
+        private final QSHost mQSHost;
 
-        TileCollector(List<QSTile> tilesToAdd, QSTileHost host) {
+        TileCollector(List<QSTile> tilesToAdd, QSHost host) {
             for (QSTile tile: tilesToAdd) {
                 TilePair pair = new TilePair(tile);
                 mQSTileList.add(pair);
             }
-            mQSTileHost = host;
+            mQSHost = host;
             if (tilesToAdd.isEmpty()) {
                 mBgExecutor.execute(this::finished);
             }
@@ -168,7 +168,7 @@
 
         private void finished() {
             notifyTilesChanged(false);
-            addPackageTiles(mQSTileHost);
+            addPackageTiles(mQSHost);
         }
 
         private void startListening() {
@@ -207,7 +207,7 @@
         }
     }
 
-    private void addPackageTiles(final QSTileHost host) {
+    private void addPackageTiles(final QSHost host) {
         mBgExecutor.execute(() -> {
             Collection<QSTile> params = host.getTiles();
             PackageManager pm = mContext.getPackageManager();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
index 01eb636..ce6b8d4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
@@ -24,10 +24,8 @@
 import android.view.View;
 
 import com.android.systemui.R;
-import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.privacy.OngoingPrivacyChip;
 import com.android.systemui.qs.QSContainerImpl;
 import com.android.systemui.qs.QSFooter;
 import com.android.systemui.qs.QSFooterView;
@@ -37,7 +35,6 @@
 import com.android.systemui.qs.QuickQSPanel;
 import com.android.systemui.qs.QuickStatusBarHeader;
 import com.android.systemui.qs.customize.QSCustomizer;
-import com.android.systemui.statusbar.phone.StatusIconContainer;
 
 import javax.inject.Named;
 
@@ -106,12 +103,6 @@
 
     /** */
     @Provides
-    static BatteryMeterView providesBatteryMeterView(QuickStatusBarHeader quickStatusBarHeader) {
-        return quickStatusBarHeader.findViewById(R.id.batteryRemainingIcon);
-    }
-
-    /** */
-    @Provides
     static QSFooterView providesQSFooterView(@RootView View view) {
         return view.findViewById(R.id.qs_footer);
     }
@@ -144,18 +135,4 @@
     static boolean providesQSUsingCollapsedLandscapeMedia(Context context) {
         return useCollapsedMediaInLandscape(context.getResources());
     }
-
-    /** */
-    @Provides
-    @QSScope
-    static OngoingPrivacyChip providesPrivacyChip(QuickStatusBarHeader qsHeader) {
-        return qsHeader.findViewById(R.id.privacy_chip);
-    }
-
-    /** */
-    @Provides
-    @QSScope
-    static StatusIconContainer providesStatusIconContainer(QuickStatusBarHeader qsHeader) {
-        return qsHeader.findViewById(R.id.statusIcons);
-    }
 }
\ No newline at end of file
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..431d6e8 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;
@@ -29,6 +30,7 @@
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.external.QSExternalModule;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.policy.CastController;
@@ -39,11 +41,14 @@
 import com.android.systemui.statusbar.policy.WalletController;
 import com.android.systemui.util.settings.SecureSettings;
 
+import java.util.Map;
+
 import javax.inject.Named;
 
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.Multibinds;
 
 /**
  * Module for QS dependencies
@@ -52,11 +57,17 @@
         includes = {MediaModule.class, QSExternalModule.class, QSFlagsModule.class})
 public interface QSModule {
 
+    /** A map of internal QS tiles. Ensures that this can be injected even if
+     * it is empty */
+    @Multibinds
+    Map<String, QSTileImpl<?>> tileMap();
+
     @Provides
+    @SysUISingleton
     static AutoTileManager provideAutoTileManager(
             Context context,
             AutoAddTracker.Builder autoAddTrackerBuilder,
-            QSTileHost host,
+            QSHost host,
             @Background Handler handler,
             SecureSettings secureSettings,
             HotspotController hotspotController,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index cfda9fd..7c2536d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -15,7 +15,6 @@
  */
 package com.android.systemui.qs.external;
 
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
 
 import android.app.PendingIntent;
@@ -63,6 +62,7 @@
 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.settings.DisplayTracker;
 
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -90,6 +90,7 @@
     private final TileServiceManager mServiceManager;
     private final int mUser;
     private final CustomTileStatePersister mCustomTileStatePersister;
+    private final DisplayTracker mDisplayTracker;
     @Nullable
     private android.graphics.drawable.Icon mDefaultIcon;
     @Nullable
@@ -120,7 +121,8 @@
             String action,
             Context userContext,
             CustomTileStatePersister customTileStatePersister,
-            TileServices tileServices
+            TileServices tileServices,
+            DisplayTracker displayTracker
     ) {
         super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
@@ -135,6 +137,7 @@
         mServiceManager = tileServices.getTileWrapper(this);
         mService = mServiceManager.getTileService();
         mCustomTileStatePersister = customTileStatePersister;
+        mDisplayTracker = displayTracker;
     }
 
     @Override
@@ -310,7 +313,7 @@
         mIsShowingDialog = false;
         try {
             if (DEBUG) Log.d(TAG, "Removing token");
-            mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
+            mWindowManager.removeWindowToken(mToken, mDisplayTracker.getDefaultDisplayId());
         } catch (RemoteException e) {
         }
     }
@@ -335,7 +338,8 @@
                 if (mIsTokenGranted && !mIsShowingDialog) {
                     try {
                         if (DEBUG) Log.d(TAG, "Removing token");
-                        mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
+                        mWindowManager.removeWindowToken(mToken,
+                                mDisplayTracker.getDefaultDisplayId());
                     } catch (RemoteException e) {
                     }
                     mIsTokenGranted = false;
@@ -354,7 +358,7 @@
         if (mIsTokenGranted) {
             try {
                 if (DEBUG) Log.d(TAG, "Removing token");
-                mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
+                mWindowManager.removeWindowToken(mToken, mDisplayTracker.getDefaultDisplayId());
             } catch (RemoteException e) {
             }
         }
@@ -398,8 +402,8 @@
         mViewClicked = view;
         try {
             if (DEBUG) Log.d(TAG, "Adding token");
-            mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY,
-                    null /* options */);
+            mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG,
+                    mDisplayTracker.getDefaultDisplayId(), null /* options */);
             mIsTokenGranted = true;
         } catch (RemoteException e) {
         }
@@ -566,6 +570,7 @@
         final QSLogger mQSLogger;
         final CustomTileStatePersister mCustomTileStatePersister;
         private TileServices mTileServices;
+        final DisplayTracker mDisplayTracker;
 
         Context mUserContext;
         String mSpec = "";
@@ -581,7 +586,8 @@
                 ActivityStarter activityStarter,
                 QSLogger qsLogger,
                 CustomTileStatePersister customTileStatePersister,
-                TileServices tileServices
+                TileServices tileServices,
+                DisplayTracker displayTracker
         ) {
             mQSHostLazy = hostLazy;
             mBackgroundLooper = backgroundLooper;
@@ -593,6 +599,7 @@
             mQSLogger = qsLogger;
             mCustomTileStatePersister = customTileStatePersister;
             mTileServices = tileServices;
+            mDisplayTracker = displayTracker;
         }
 
         Builder setSpec(@NonNull String spec) {
@@ -623,7 +630,8 @@
                     action,
                     mUserContext,
                     mCustomTileStatePersister,
-                    mTileServices
+                    mTileServices,
+                    mDisplayTracker
             );
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
index 237b66e..d9e5580 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
@@ -25,13 +25,13 @@
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.android.internal.statusbar.IAddTileResultCallback
+import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.QSHost
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.R
-import com.android.systemui.statusbar.CommandQueue
 import java.io.PrintWriter
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.function.Consumer
@@ -40,14 +40,14 @@
 private const val TAG = "TileServiceRequestController"
 
 /**
- * Controller to interface between [TileRequestDialog] and [QSTileHost].
+ * Controller to interface between [TileRequestDialog] and [QSHost].
  */
 class TileServiceRequestController constructor(
-    private val qsTileHost: QSTileHost,
+    private val qsHost: QSHost,
     private val commandQueue: CommandQueue,
     private val commandRegistry: CommandRegistry,
     private val eventLogger: TileRequestDialogEventLogger,
-    private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsTileHost.context) }
+    private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsHost.context) }
 ) {
 
     companion object {
@@ -93,7 +93,7 @@
     }
 
     private fun addTile(componentName: ComponentName) {
-        qsTileHost.addTile(componentName, true)
+        qsHost.addTile(componentName, true)
     }
 
     @VisibleForTesting
@@ -158,7 +158,7 @@
 
     private fun isTileAlreadyAdded(componentName: ComponentName): Boolean {
         val spec = CustomTile.toSpec(componentName)
-        return qsTileHost.indexOf(spec) != -1
+        return qsHost.indexOf(spec) != -1
     }
 
     inner class TileServiceRequestCommand : Command {
@@ -194,13 +194,13 @@
         private val commandQueue: CommandQueue,
         private val commandRegistry: CommandRegistry
     ) {
-        fun create(qsTileHost: QSTileHost): TileServiceRequestController {
+        fun create(qsHost: QSHost): TileServiceRequestController {
             return TileServiceRequestController(
-                    qsTileHost,
+                    qsHost,
                     commandQueue,
                     commandRegistry,
                     TileRequestDialogEventLogger()
             )
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index 84a18d8..adc7165 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -39,7 +39,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -68,22 +68,24 @@
     private final Context mContext;
     private final Handler mMainHandler;
     private final Provider<Handler> mHandlerProvider;
-    private final QSTileHost mHost;
+    private final QSHost mHost;
     private final KeyguardStateController mKeyguardStateController;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final CommandQueue mCommandQueue;
     private final UserTracker mUserTracker;
+    private final StatusBarIconController mStatusBarIconController;
 
     private int mMaxBound = DEFAULT_MAX_BOUND;
 
     @Inject
     public TileServices(
-            QSTileHost host,
+            QSHost host,
             @Main Provider<Handler> handlerProvider,
             BroadcastDispatcher broadcastDispatcher,
             UserTracker userTracker,
             KeyguardStateController keyguardStateController,
-            CommandQueue commandQueue) {
+            CommandQueue commandQueue,
+            StatusBarIconController statusBarIconController) {
         mHost = host;
         mKeyguardStateController = keyguardStateController;
         mContext = mHost.getContext();
@@ -92,6 +94,7 @@
         mMainHandler = mHandlerProvider.get();
         mUserTracker = userTracker;
         mCommandQueue = commandQueue;
+        mStatusBarIconController = statusBarIconController;
         mCommandQueue.addCallback(mRequestListeningCallback);
     }
 
@@ -99,7 +102,7 @@
         return mContext;
     }
 
-    public QSTileHost getHost() {
+    public QSHost getHost() {
         return mHost;
     }
 
@@ -131,8 +134,7 @@
             mTiles.remove(tile.getComponent());
             final String slot = tile.getComponent().getClassName();
             // TileServices doesn't know how to add more than 1 icon per slot, so remove all
-            mMainHandler.post(() -> mHost.getIconController()
-                    .removeAllIconsForExternalSlot(slot));
+            mMainHandler.post(() -> mStatusBarIconController.removeAllIconsForSlot(slot));
         }
     }
 
@@ -309,7 +311,7 @@
                     mMainHandler.post(new Runnable() {
                         @Override
                         public void run() {
-                            StatusBarIconController iconController = mHost.getIconController();
+                            StatusBarIconController iconController = mStatusBarIconController;
                             iconController.setIcon(componentName.getClassName(), statusIcon);
                             iconController.setExternalIcon(componentName.getClassName());
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 9f376ae..23c41db 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -17,15 +17,14 @@
 package com.android.systemui.qs.logging
 
 import android.service.quicksettings.Tile
+import android.view.View
 import com.android.systemui.log.dagger.QSLog
 import com.android.systemui.plugins.log.ConstantStringsLogger
 import com.android.systemui.plugins.log.ConstantStringsLoggerImpl
 import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.plugins.log.LogLevel.ERROR
 import com.android.systemui.plugins.log.LogLevel.VERBOSE
-import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.StatusBarState
 import com.google.errorprone.annotations.CompileTimeConstant
@@ -49,109 +48,135 @@
     }
 
     fun logTileAdded(tileSpec: String) {
-        log(DEBUG, {
-            str1 = tileSpec
-        }, {
-            "[$str1] Tile added"
-        })
+        buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" })
     }
 
     fun logTileDestroyed(tileSpec: String, reason: String) {
-        log(DEBUG, {
-            str1 = tileSpec
-            str2 = reason
-        }, {
-            "[$str1] Tile destroyed. Reason: $str2"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                str2 = reason
+            },
+            { "[$str1] Tile destroyed. Reason: $str2" }
+        )
     }
 
     fun logTileChangeListening(tileSpec: String, listening: Boolean) {
-        log(VERBOSE, {
-            bool1 = listening
-            str1 = tileSpec
-        }, {
-            "[$str1] Tile listening=$bool1"
-        })
+        buffer.log(
+            TAG,
+            VERBOSE,
+            {
+                bool1 = listening
+                str1 = tileSpec
+            },
+            { "[$str1] Tile listening=$bool1" }
+        )
     }
 
     fun logAllTilesChangeListening(listening: Boolean, containerName: String, allSpecs: String) {
-        log(DEBUG, {
-            bool1 = listening
-            str1 = containerName
-            str2 = allSpecs
-        }, {
-            "Tiles listening=$bool1 in $str1. $str2"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                bool1 = listening
+                str1 = containerName
+                str2 = allSpecs
+            },
+            { "Tiles listening=$bool1 in $str1. $str2" }
+        )
     }
 
     fun logTileClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
-        log(DEBUG, {
-            str1 = tileSpec
-            int1 = eventId
-            str2 = StatusBarState.toString(statusBarState)
-            str3 = toStateString(state)
-        }, {
-            "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                int1 = eventId
+                str2 = StatusBarState.toString(statusBarState)
+                str3 = toStateString(state)
+            },
+            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" }
+        )
     }
 
     fun logHandleClick(tileSpec: String, eventId: Int) {
-        log(DEBUG, {
-            str1 = tileSpec
-            int1 = eventId
-        }, {
-            "[$str1][$int1] Tile handling click."
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                int1 = eventId
+            },
+            { "[$str1][$int1] Tile handling click." }
+        )
     }
 
     fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
-        log(DEBUG, {
-            str1 = tileSpec
-            int1 = eventId
-            str2 = StatusBarState.toString(statusBarState)
-            str3 = toStateString(state)
-        }, {
-            "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                int1 = eventId
+                str2 = StatusBarState.toString(statusBarState)
+                str3 = toStateString(state)
+            },
+            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" }
+        )
     }
 
     fun logHandleSecondaryClick(tileSpec: String, eventId: Int) {
-        log(DEBUG, {
-            str1 = tileSpec
-            int1 = eventId
-        }, {
-            "[$str1][$int1] Tile handling secondary click."
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                int1 = eventId
+            },
+            { "[$str1][$int1] Tile handling secondary click." }
+        )
     }
 
     fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
-        log(DEBUG, {
-            str1 = tileSpec
-            int1 = eventId
-            str2 = StatusBarState.toString(statusBarState)
-            str3 = toStateString(state)
-        }, {
-            "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                int1 = eventId
+                str2 = StatusBarState.toString(statusBarState)
+                str3 = toStateString(state)
+            },
+            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" }
+        )
     }
 
     fun logHandleLongClick(tileSpec: String, eventId: Int) {
-        log(DEBUG, {
-            str1 = tileSpec
-            int1 = eventId
-        }, {
-            "[$str1][$int1] Tile handling long click."
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                int1 = eventId
+            },
+            { "[$str1][$int1] Tile handling long click." }
+        )
     }
 
     fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) {
-        log(VERBOSE, {
-            str1 = tileSpec
-            int1 = lastType
-            str2 = callback
-        }, {
-            "[$str1] mLastTileState=$int1, Callback=$str2."
-        })
+        buffer.log(
+            TAG,
+            VERBOSE,
+            {
+                str1 = tileSpec
+                int1 = lastType
+                str2 = callback
+            },
+            { "[$str1] mLastTileState=$int1, Callback=$str2." }
+        )
     }
 
     // TODO(b/250618218): Remove this method once we know the root cause of b/250618218.
@@ -167,58 +192,75 @@
         if (tileSpec != "internet") {
             return
         }
-        log(VERBOSE, {
-            str1 = tileSpec
-            int1 = state
-            bool1 = disabledByPolicy
-            int2 = color
-        }, {
-            "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2."
-        })
+        buffer.log(
+            TAG,
+            VERBOSE,
+            {
+                str1 = tileSpec
+                int1 = state
+                bool1 = disabledByPolicy
+                int2 = color
+            },
+            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." }
+        )
     }
 
     fun logTileUpdated(tileSpec: String, state: QSTile.State) {
-        log(VERBOSE, {
-            str1 = tileSpec
-            str2 = state.label?.toString()
-            str3 = state.icon?.toString()
-            int1 = state.state
-            if (state is QSTile.SignalState) {
-                bool1 = true
-                bool2 = state.activityIn
-                bool3 = state.activityOut
+        buffer.log(
+            TAG,
+            VERBOSE,
+            {
+                str1 = tileSpec
+                str2 = state.label?.toString()
+                str3 = state.icon?.toString()
+                int1 = state.state
+                if (state is QSTile.SignalState) {
+                    bool1 = true
+                    bool2 = state.activityIn
+                    bool3 = state.activityOut
+                }
+            },
+            {
+                "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." +
+                    if (bool1) " Activity in/out=$bool2/$bool3" else ""
             }
-        }, {
-            "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." +
-                if (bool1) " Activity in/out=$bool2/$bool3" else ""
-        })
+        )
     }
 
     fun logPanelExpanded(expanded: Boolean, containerName: String) {
-        log(DEBUG, {
-            str1 = containerName
-            bool1 = expanded
-        }, {
-            "$str1 expanded=$bool1"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = containerName
+                bool1 = expanded
+            },
+            { "$str1 expanded=$bool1" }
+        )
     }
 
     fun logOnViewAttached(orientation: Int, containerName: String) {
-        log(DEBUG, {
-            str1 = containerName
-            int1 = orientation
-        }, {
-            "onViewAttached: $str1 orientation $int1"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = containerName
+                int1 = orientation
+            },
+            { "onViewAttached: $str1 orientation $int1" }
+        )
     }
 
     fun logOnViewDetached(orientation: Int, containerName: String) {
-        log(DEBUG, {
-            str1 = containerName
-            int1 = orientation
-        }, {
-            "onViewDetached: $str1 orientation $int1"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = containerName
+                int1 = orientation
+            },
+            { "onViewDetached: $str1 orientation $int1" }
+        )
     }
 
     fun logOnConfigurationChanged(
@@ -226,13 +268,16 @@
         newOrientation: Int,
         containerName: String
     ) {
-        log(DEBUG, {
-            str1 = containerName
-            int1 = lastOrientation
-            int2 = newOrientation
-        }, {
-            "configuration change: $str1 orientation was $int1, now $int2"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = containerName
+                int1 = lastOrientation
+                int2 = newOrientation
+            },
+            { "configuration change: $str1 orientation was $int1, now $int2" }
+        )
     }
 
     fun logSwitchTileLayout(
@@ -241,32 +286,41 @@
         force: Boolean,
         containerName: String
     ) {
-        log(DEBUG, {
-            str1 = containerName
-            bool1 = after
-            bool2 = before
-            bool3 = force
-        }, {
-            "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = containerName
+                bool1 = after
+                bool2 = before
+                bool3 = force
+            },
+            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" }
+        )
     }
 
     fun logTileDistributionInProgress(tilesPerPageCount: Int, totalTilesCount: Int) {
-        log(DEBUG, {
-            int1 = tilesPerPageCount
-            int2 = totalTilesCount
-        }, {
-            "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = tilesPerPageCount
+                int2 = totalTilesCount
+            },
+            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" }
+        )
     }
 
     fun logTileDistributed(tileName: String, pageIndex: Int) {
-        log(DEBUG, {
-            str1 = tileName
-            int1 = pageIndex
-        }, {
-            "Adding $str1 to page number $int1"
-        })
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileName
+                int1 = pageIndex
+            },
+            { "Adding $str1 to page number $int1" }
+        )
     }
 
     private fun toStateString(state: Int): String {
@@ -278,11 +332,24 @@
         }
     }
 
-    private inline fun log(
-        logLevel: LogLevel,
-        initializer: LogMessage.() -> Unit,
-        noinline printer: LogMessage.() -> String
-    ) {
-        buffer.log(TAG, logLevel, initializer, printer)
+    fun logVisibility(viewName: String, @View.Visibility visibility: Int) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = viewName
+                str2 = toVisibilityString(visibility)
+            },
+            { "$str1 visibility: $str2" }
+        )
+    }
+
+    private fun toVisibilityString(visibility: Int): String {
+        return when (visibility) {
+            View.VISIBLE -> "VISIBLE"
+            View.INVISIBLE -> "INVISIBLE"
+            View.GONE -> "GONE"
+            else -> "undefined"
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index a92c7e3..6b23f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -27,78 +27,32 @@
 import com.android.systemui.plugins.qs.QSTileView;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.external.CustomTile;
-import com.android.systemui.qs.tiles.AirplaneModeTile;
-import com.android.systemui.qs.tiles.AlarmTile;
-import com.android.systemui.qs.tiles.BatterySaverTile;
-import com.android.systemui.qs.tiles.BluetoothTile;
-import com.android.systemui.qs.tiles.CameraToggleTile;
-import com.android.systemui.qs.tiles.CastTile;
-import com.android.systemui.qs.tiles.CellularTile;
-import com.android.systemui.qs.tiles.ColorCorrectionTile;
-import com.android.systemui.qs.tiles.ColorInversionTile;
-import com.android.systemui.qs.tiles.DataSaverTile;
-import com.android.systemui.qs.tiles.DeviceControlsTile;
-import com.android.systemui.qs.tiles.DndTile;
-import com.android.systemui.qs.tiles.DreamTile;
-import com.android.systemui.qs.tiles.FlashlightTile;
-import com.android.systemui.qs.tiles.HotspotTile;
-import com.android.systemui.qs.tiles.InternetTile;
-import com.android.systemui.qs.tiles.LocationTile;
-import com.android.systemui.qs.tiles.MicrophoneToggleTile;
-import com.android.systemui.qs.tiles.NfcTile;
-import com.android.systemui.qs.tiles.NightDisplayTile;
-import com.android.systemui.qs.tiles.OneHandedModeTile;
-import com.android.systemui.qs.tiles.QRCodeScannerTile;
-import com.android.systemui.qs.tiles.QuickAccessWalletTile;
-import com.android.systemui.qs.tiles.ReduceBrightColorsTile;
-import com.android.systemui.qs.tiles.RotationLockTile;
-import com.android.systemui.qs.tiles.ScreenRecordTile;
-import com.android.systemui.qs.tiles.UiModeNightTile;
-import com.android.systemui.qs.tiles.WifiTile;
-import com.android.systemui.qs.tiles.WorkModeTile;
 import com.android.systemui.util.leak.GarbageMonitor;
 
+import java.util.Map;
+
 import javax.inject.Inject;
 import javax.inject.Provider;
 
 import dagger.Lazy;
 
+/**
+ * A factory that creates Quick Settings tiles based on a tileSpec
+ *
+ * To create a new tile within SystemUI, the tile class should extend {@link QSTileImpl} and have
+ * a public static final TILE_SPEC field which serves as a unique key for this tile. (e.g. {@link
+ * com.android.systemui.qs.tiles.DreamTile#TILE_SPEC})
+ *
+ * After, create or find an existing Module class to house the tile's binding method (e.g. {@link
+ * com.android.systemui.accessibility.AccessibilityModule}). If creating a new module, add your
+ * module to the SystemUI dagger graph by including it in an appropriate module.
+ */
 @SysUISingleton
 public class QSFactoryImpl implements QSFactory {
 
     private static final String TAG = "QSFactory";
 
-    private final Provider<WifiTile> mWifiTileProvider;
-    private final Provider<InternetTile> mInternetTileProvider;
-    private final Provider<BluetoothTile> mBluetoothTileProvider;
-    private final Provider<CellularTile> mCellularTileProvider;
-    private final Provider<DndTile> mDndTileProvider;
-    private final Provider<ColorCorrectionTile> mColorCorrectionTileProvider;
-    private final Provider<ColorInversionTile> mColorInversionTileProvider;
-    private final Provider<AirplaneModeTile> mAirplaneModeTileProvider;
-    private final Provider<WorkModeTile> mWorkModeTileProvider;
-    private final Provider<RotationLockTile> mRotationLockTileProvider;
-    private final Provider<FlashlightTile> mFlashlightTileProvider;
-    private final Provider<LocationTile> mLocationTileProvider;
-    private final Provider<CastTile> mCastTileProvider;
-    private final Provider<HotspotTile> mHotspotTileProvider;
-    private final Provider<BatterySaverTile> mBatterySaverTileProvider;
-    private final Provider<DataSaverTile> mDataSaverTileProvider;
-    private final Provider<NightDisplayTile> mNightDisplayTileProvider;
-    private final Provider<NfcTile> mNfcTileProvider;
-    private final Provider<GarbageMonitor.MemoryTile> mMemoryTileProvider;
-    private final Provider<UiModeNightTile> mUiModeNightTileProvider;
-    private final Provider<ScreenRecordTile> mScreenRecordTileProvider;
-    private final Provider<ReduceBrightColorsTile> mReduceBrightColorsTileProvider;
-    private final Provider<CameraToggleTile> mCameraToggleTileProvider;
-    private final Provider<MicrophoneToggleTile> mMicrophoneToggleTileProvider;
-    private final Provider<DeviceControlsTile> mDeviceControlsTileProvider;
-    private final Provider<AlarmTile> mAlarmTileProvider;
-    private final Provider<QuickAccessWalletTile> mQuickAccessWalletTileProvider;
-    private final Provider<QRCodeScannerTile> mQRCodeScannerTileProvider;
-    private final Provider<OneHandedModeTile> mOneHandedModeTileProvider;
-    private final Provider<DreamTile> mDreamTileProvider;
-
+    protected final Map<String, Provider<QSTileImpl<?>>> mTileMap;
     private final Lazy<QSHost> mQsHostLazy;
     private final Provider<CustomTile.Builder> mCustomTileBuilderProvider;
 
@@ -106,69 +60,10 @@
     public QSFactoryImpl(
             Lazy<QSHost> qsHostLazy,
             Provider<CustomTile.Builder> customTileBuilderProvider,
-            Provider<WifiTile> wifiTileProvider,
-            Provider<InternetTile> internetTileProvider,
-            Provider<BluetoothTile> bluetoothTileProvider,
-            Provider<CellularTile> cellularTileProvider,
-            Provider<DndTile> dndTileProvider,
-            Provider<ColorInversionTile> colorInversionTileProvider,
-            Provider<AirplaneModeTile> airplaneModeTileProvider,
-            Provider<WorkModeTile> workModeTileProvider,
-            Provider<RotationLockTile> rotationLockTileProvider,
-            Provider<FlashlightTile> flashlightTileProvider,
-            Provider<LocationTile> locationTileProvider,
-            Provider<CastTile> castTileProvider,
-            Provider<HotspotTile> hotspotTileProvider,
-            Provider<BatterySaverTile> batterySaverTileProvider,
-            Provider<DataSaverTile> dataSaverTileProvider,
-            Provider<NightDisplayTile> nightDisplayTileProvider,
-            Provider<NfcTile> nfcTileProvider,
-            Provider<GarbageMonitor.MemoryTile> memoryTileProvider,
-            Provider<UiModeNightTile> uiModeNightTileProvider,
-            Provider<ScreenRecordTile> screenRecordTileProvider,
-            Provider<ReduceBrightColorsTile> reduceBrightColorsTileProvider,
-            Provider<CameraToggleTile> cameraToggleTileProvider,
-            Provider<MicrophoneToggleTile> microphoneToggleTileProvider,
-            Provider<DeviceControlsTile> deviceControlsTileProvider,
-            Provider<AlarmTile> alarmTileProvider,
-            Provider<QuickAccessWalletTile> quickAccessWalletTileProvider,
-            Provider<QRCodeScannerTile> qrCodeScannerTileProvider,
-            Provider<OneHandedModeTile> oneHandedModeTileProvider,
-            Provider<ColorCorrectionTile> colorCorrectionTileProvider,
-            Provider<DreamTile> dreamTileProvider) {
+            Map<String, Provider<QSTileImpl<?>>> tileMap) {
         mQsHostLazy = qsHostLazy;
         mCustomTileBuilderProvider = customTileBuilderProvider;
-
-        mWifiTileProvider = wifiTileProvider;
-        mInternetTileProvider = internetTileProvider;
-        mBluetoothTileProvider = bluetoothTileProvider;
-        mCellularTileProvider = cellularTileProvider;
-        mDndTileProvider = dndTileProvider;
-        mColorInversionTileProvider = colorInversionTileProvider;
-        mAirplaneModeTileProvider = airplaneModeTileProvider;
-        mWorkModeTileProvider = workModeTileProvider;
-        mRotationLockTileProvider = rotationLockTileProvider;
-        mFlashlightTileProvider = flashlightTileProvider;
-        mLocationTileProvider = locationTileProvider;
-        mCastTileProvider = castTileProvider;
-        mHotspotTileProvider = hotspotTileProvider;
-        mBatterySaverTileProvider = batterySaverTileProvider;
-        mDataSaverTileProvider = dataSaverTileProvider;
-        mNightDisplayTileProvider = nightDisplayTileProvider;
-        mNfcTileProvider = nfcTileProvider;
-        mMemoryTileProvider = memoryTileProvider;
-        mUiModeNightTileProvider = uiModeNightTileProvider;
-        mScreenRecordTileProvider = screenRecordTileProvider;
-        mReduceBrightColorsTileProvider = reduceBrightColorsTileProvider;
-        mCameraToggleTileProvider = cameraToggleTileProvider;
-        mMicrophoneToggleTileProvider = microphoneToggleTileProvider;
-        mDeviceControlsTileProvider = deviceControlsTileProvider;
-        mAlarmTileProvider = alarmTileProvider;
-        mQuickAccessWalletTileProvider = quickAccessWalletTileProvider;
-        mQRCodeScannerTileProvider = qrCodeScannerTileProvider;
-        mOneHandedModeTileProvider = oneHandedModeTileProvider;
-        mColorCorrectionTileProvider = colorCorrectionTileProvider;
-        mDreamTileProvider = dreamTileProvider;
+        mTileMap = tileMap;
     }
 
     /** Creates a tile with a type based on {@code tileSpec} */
@@ -185,65 +80,10 @@
     @Nullable
     protected QSTileImpl createTileInternal(String tileSpec) {
         // Stock tiles.
-        switch (tileSpec) {
-            case "wifi":
-                return mWifiTileProvider.get();
-            case "internet":
-                return mInternetTileProvider.get();
-            case "bt":
-                return mBluetoothTileProvider.get();
-            case "cell":
-                return mCellularTileProvider.get();
-            case "dnd":
-                return mDndTileProvider.get();
-            case "inversion":
-                return mColorInversionTileProvider.get();
-            case "airplane":
-                return mAirplaneModeTileProvider.get();
-            case "work":
-                return mWorkModeTileProvider.get();
-            case "rotation":
-                return mRotationLockTileProvider.get();
-            case "flashlight":
-                return mFlashlightTileProvider.get();
-            case "location":
-                return mLocationTileProvider.get();
-            case "cast":
-                return mCastTileProvider.get();
-            case "hotspot":
-                return mHotspotTileProvider.get();
-            case "battery":
-                return mBatterySaverTileProvider.get();
-            case "saver":
-                return mDataSaverTileProvider.get();
-            case "night":
-                return mNightDisplayTileProvider.get();
-            case "nfc":
-                return mNfcTileProvider.get();
-            case "dark":
-                return mUiModeNightTileProvider.get();
-            case "screenrecord":
-                return mScreenRecordTileProvider.get();
-            case "reduce_brightness":
-                return mReduceBrightColorsTileProvider.get();
-            case "cameratoggle":
-                return mCameraToggleTileProvider.get();
-            case "mictoggle":
-                return mMicrophoneToggleTileProvider.get();
-            case "controls":
-                return mDeviceControlsTileProvider.get();
-            case "alarm":
-                return mAlarmTileProvider.get();
-            case "wallet":
-                return mQuickAccessWalletTileProvider.get();
-            case "qr_code_scanner":
-                return mQRCodeScannerTileProvider.get();
-            case "onehanded":
-                return mOneHandedModeTileProvider.get();
-            case "color_correction":
-                return mColorCorrectionTileProvider.get();
-            case "dream":
-                return mDreamTileProvider.get();
+        if (mTileMap.containsKey(tileSpec)
+                // We should not return a Garbage Monitory Tile if the build is not Debuggable
+                && (!tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC) || Build.IS_DEBUGGABLE)) {
+            return mTileMap.get(tileSpec).get();
         }
 
         // Custom tiles
@@ -252,13 +92,6 @@
                     mCustomTileBuilderProvider.get(), tileSpec, mQsHostLazy.get().getUserContext());
         }
 
-        // Debug tiles.
-        if (Build.IS_DEBUGGABLE) {
-            if (tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC)) {
-                return mMemoryTileProvider.get();
-            }
-        }
-
         // Broken tiles.
         Log.w(TAG, "No stock tile spec: " + tileSpec);
         return null;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 2cffe89..49ba508 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -712,6 +712,10 @@
             return context.getDrawable(mResId);
         }
 
+        public int getResId() {
+            return mResId;
+        }
+
         @Override
         public boolean equals(Object o) {
             return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 29d7fb0..de1137e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -440,12 +440,11 @@
 
         // State handling and description
         val stateDescription = StringBuilder()
-        val stateText = getStateText(state)
+        val arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
+        val stateText = state.getStateText(arrayResId, resources)
+        state.secondaryLabel = state.getSecondaryLabel(stateText)
         if (!TextUtils.isEmpty(stateText)) {
             stateDescription.append(stateText)
-            if (TextUtils.isEmpty(state.secondaryLabel)) {
-                state.secondaryLabel = stateText
-            }
         }
         if (state.disabledByPolicy && state.state != Tile.STATE_UNAVAILABLE) {
             stateDescription.append(", ")
@@ -591,16 +590,6 @@
         return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE]
     }
 
-    private fun getStateText(state: QSTile.State): String {
-        return if (state.state == Tile.STATE_UNAVAILABLE || state is BooleanState) {
-            val arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
-            val array = resources.getStringArray(arrayResId)
-            array[state.state]
-        } else {
-            ""
-        }
-    }
-
     /*
      * The view should not be animated if it's not on screen and no part of it is visible.
      */
@@ -663,45 +652,6 @@
     )
 }
 
-@VisibleForTesting
-internal object SubtitleArrayMapping {
-    private val subtitleIdsMap = mapOf<String?, Int>(
-        "internet" to R.array.tile_states_internet,
-        "wifi" to R.array.tile_states_wifi,
-        "cell" to R.array.tile_states_cell,
-        "battery" to R.array.tile_states_battery,
-        "dnd" to R.array.tile_states_dnd,
-        "flashlight" to R.array.tile_states_flashlight,
-        "rotation" to R.array.tile_states_rotation,
-        "bt" to R.array.tile_states_bt,
-        "airplane" to R.array.tile_states_airplane,
-        "location" to R.array.tile_states_location,
-        "hotspot" to R.array.tile_states_hotspot,
-        "inversion" to R.array.tile_states_inversion,
-        "saver" to R.array.tile_states_saver,
-        "dark" to R.array.tile_states_dark,
-        "work" to R.array.tile_states_work,
-        "cast" to R.array.tile_states_cast,
-        "night" to R.array.tile_states_night,
-        "screenrecord" to R.array.tile_states_screenrecord,
-        "reverse" to R.array.tile_states_reverse,
-        "reduce_brightness" to R.array.tile_states_reduce_brightness,
-        "cameratoggle" to R.array.tile_states_cameratoggle,
-        "mictoggle" to R.array.tile_states_mictoggle,
-        "controls" to R.array.tile_states_controls,
-        "wallet" to R.array.tile_states_wallet,
-        "qr_code_scanner" to R.array.tile_states_qr_code_scanner,
-        "alarm" to R.array.tile_states_alarm,
-        "onehanded" to R.array.tile_states_onehanded,
-        "color_correction" to R.array.tile_states_color_correction,
-        "dream" to R.array.tile_states_dream
-    )
-
-    fun getSubtitleId(spec: String?): Int {
-        return subtitleIdsMap.getOrDefault(spec, R.array.tile_states_default)
-    }
-}
-
 fun constrainSquishiness(squish: Float): Float {
     return 0.1f + squish * 0.9f
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
new file mode 100644
index 0000000..f672e51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.qs.tileimpl
+
+import com.android.systemui.R
+
+/** Return the subtitle resource Id of the given tile. */
+object SubtitleArrayMapping {
+    private val subtitleIdsMap: HashMap<String, Int> = HashMap()
+    init {
+        subtitleIdsMap["internet"] = R.array.tile_states_internet
+        subtitleIdsMap["wifi"] = R.array.tile_states_wifi
+        subtitleIdsMap["cell"] = R.array.tile_states_cell
+        subtitleIdsMap["battery"] = R.array.tile_states_battery
+        subtitleIdsMap["dnd"] = R.array.tile_states_dnd
+        subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight
+        subtitleIdsMap["rotation"] = R.array.tile_states_rotation
+        subtitleIdsMap["bt"] = R.array.tile_states_bt
+        subtitleIdsMap["airplane"] = R.array.tile_states_airplane
+        subtitleIdsMap["location"] = R.array.tile_states_location
+        subtitleIdsMap["hotspot"] = R.array.tile_states_hotspot
+        subtitleIdsMap["inversion"] = R.array.tile_states_inversion
+        subtitleIdsMap["saver"] = R.array.tile_states_saver
+        subtitleIdsMap["dark"] = R.array.tile_states_dark
+        subtitleIdsMap["work"] = R.array.tile_states_work
+        subtitleIdsMap["cast"] = R.array.tile_states_cast
+        subtitleIdsMap["night"] = R.array.tile_states_night
+        subtitleIdsMap["screenrecord"] = R.array.tile_states_screenrecord
+        subtitleIdsMap["reverse"] = R.array.tile_states_reverse
+        subtitleIdsMap["reduce_brightness"] = R.array.tile_states_reduce_brightness
+        subtitleIdsMap["cameratoggle"] = R.array.tile_states_cameratoggle
+        subtitleIdsMap["mictoggle"] = R.array.tile_states_mictoggle
+        subtitleIdsMap["controls"] = R.array.tile_states_controls
+        subtitleIdsMap["wallet"] = R.array.tile_states_wallet
+        subtitleIdsMap["qr_code_scanner"] = R.array.tile_states_qr_code_scanner
+        subtitleIdsMap["alarm"] = R.array.tile_states_alarm
+        subtitleIdsMap["onehanded"] = R.array.tile_states_onehanded
+        subtitleIdsMap["color_correction"] = R.array.tile_states_color_correction
+        subtitleIdsMap["dream"] = R.array.tile_states_dream
+        subtitleIdsMap["font_scaling"] = R.array.tile_states_font_scaling
+    }
+
+    /** Get the subtitle resource id of the given tile */
+    fun getSubtitleId(spec: String?): Int {
+        return if (spec == null) {
+            R.array.tile_states_default
+        } else subtitleIdsMap[spec] ?: R.array.tile_states_default
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
index 033dbe0..92a83bb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -57,6 +57,9 @@
 
 /** Quick settings tile: Airplane mode **/
 public class AirplaneModeTile extends QSTileImpl<BooleanState> {
+
+    public static final String TILE_SPEC = "airplane";
+
     private final SettingObserver mSetting;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final Lazy<ConnectivityManager> mLazyConnectivityManager;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
index 14e0f70..2ca452e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
@@ -118,4 +118,8 @@
     override fun getLongClickIntent(): Intent? {
         return null
     }
-}
\ No newline at end of file
+
+    companion object {
+        const val TILE_SPEC = "alarm"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
index ee49b29..027a464 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
@@ -48,6 +48,8 @@
 public class BatterySaverTile extends QSTileImpl<BooleanState> implements
         BatteryController.BatteryStateChangeCallback {
 
+    public static final String TILE_SPEC = "battery";
+
     private final BatteryController mBatteryController;
     @VisibleForTesting
     protected final SettingObserver mSetting;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 9a0d0d9..df1c8df 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -55,6 +55,9 @@
 
 /** Quick settings tile: Bluetooth **/
 public class BluetoothTile extends QSTileImpl<BooleanState> {
+
+    public static final String TILE_SPEC = "bt";
+
     private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
 
     private final BluetoothController mController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java
index ee41f1d..93e5f1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java
@@ -45,6 +45,8 @@
 
 public class CameraToggleTile extends SensorPrivacyToggleTile {
 
+    public static final String TILE_SPEC = "cameratoggle";
+
     @Inject
     protected CameraToggleTile(QSHost host,
             @Background Looper backgroundLooper,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index dce137f..8d98481 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -66,7 +66,9 @@
 /** Quick settings tile: Cast **/
 public class CastTile extends QSTileImpl<BooleanState> {
 
-    private static final String INTERACTION_JANK_TAG = "cast";
+    public static final String TILE_SPEC = "cast";
+
+    private static final String INTERACTION_JANK_TAG = TILE_SPEC;
 
     private static final Intent CAST_SETTINGS =
             new Intent(Settings.ACTION_CAST_SETTINGS);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
deleted file mode 100644
index 04a25fc..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles;
-
-import static com.android.systemui.Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA;
-
-import android.annotation.NonNull;
-import android.app.AlertDialog;
-import android.app.AlertDialog.Builder;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.service.quicksettings.Tile;
-import android.telephony.SubscriptionManager;
-import android.text.Html;
-import android.text.TextUtils;
-import android.view.View;
-import android.view.WindowManager.LayoutParams;
-import android.widget.Switch;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.settingslib.net.DataUsageController;
-import com.android.systemui.Prefs;
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSIconView;
-import com.android.systemui.plugins.qs.QSTile.SignalState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.SignalTileView;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.connectivity.IconState;
-import com.android.systemui.statusbar.connectivity.MobileDataIndicators;
-import com.android.systemui.statusbar.connectivity.NetworkController;
-import com.android.systemui.statusbar.connectivity.SignalCallback;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import javax.inject.Inject;
-
-/** Quick settings tile: Cellular **/
-public class CellularTile extends QSTileImpl<SignalState> {
-    private static final String ENABLE_SETTINGS_DATA_PLAN = "enable.settings.data.plan";
-
-    private final NetworkController mController;
-    private final DataUsageController mDataController;
-    private final KeyguardStateController mKeyguard;
-    private final CellSignalCallback mSignalCallback = new CellSignalCallback();
-
-    @Inject
-    public CellularTile(
-            QSHost host,
-            @Background Looper backgroundLooper,
-            @Main Handler mainHandler,
-            FalsingManager falsingManager,
-            MetricsLogger metricsLogger,
-            StatusBarStateController statusBarStateController,
-            ActivityStarter activityStarter,
-            QSLogger qsLogger,
-            NetworkController networkController,
-            KeyguardStateController keyguardStateController
-
-    ) {
-        super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
-                statusBarStateController, activityStarter, qsLogger);
-        mController = networkController;
-        mKeyguard = keyguardStateController;
-        mDataController = mController.getMobileDataController();
-        mController.observe(getLifecycle(), mSignalCallback);
-    }
-
-    @Override
-    public SignalState newTileState() {
-        return new SignalState();
-    }
-
-    @Override
-    public QSIconView createTileView(Context context) {
-        return new SignalTileView(context);
-    }
-
-    @Override
-    public Intent getLongClickIntent() {
-        if (getState().state == Tile.STATE_UNAVAILABLE) {
-            return new Intent(Settings.ACTION_WIRELESS_SETTINGS);
-        }
-        return getCellularSettingIntent();
-    }
-
-    @Override
-    protected void handleClick(@Nullable View view) {
-        if (getState().state == Tile.STATE_UNAVAILABLE) {
-            return;
-        }
-        if (mDataController.isMobileDataEnabled()) {
-            maybeShowDisableDialog();
-        } else {
-            mDataController.setMobileDataEnabled(true);
-        }
-    }
-
-    private void maybeShowDisableDialog() {
-        if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) {
-            // Directly turn off mobile data if the user has seen the dialog before.
-            mDataController.setMobileDataEnabled(false);
-            return;
-        }
-        String carrierName = mController.getMobileDataNetworkName();
-        boolean isInService = mController.isMobileDataNetworkInService();
-        if (TextUtils.isEmpty(carrierName) || !isInService) {
-            carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier);
-        }
-        AlertDialog dialog = new Builder(mContext)
-                .setTitle(R.string.mobile_data_disable_title)
-                .setMessage(mContext.getString(R.string.mobile_data_disable_message, carrierName))
-                .setNegativeButton(android.R.string.cancel, null)
-                .setPositiveButton(
-                        com.android.internal.R.string.alert_windows_notification_turn_off_action,
-                        (d, w) -> {
-                            mDataController.setMobileDataEnabled(false);
-                            Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true);
-                        })
-                .create();
-        dialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG);
-        SystemUIDialog.setShowForAllUsers(dialog, true);
-        SystemUIDialog.registerDismissListener(dialog);
-        SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing());
-        dialog.show();
-    }
-
-    @Override
-    protected void handleSecondaryClick(@Nullable View view) {
-        handleLongClick(view);
-    }
-
-    @Override
-    public CharSequence getTileLabel() {
-        return mContext.getString(R.string.quick_settings_cellular_detail_title);
-    }
-
-    @Override
-    protected void handleUpdateState(SignalState state, Object arg) {
-        CallbackInfo cb = (CallbackInfo) arg;
-        if (cb == null) {
-            cb = mSignalCallback.mInfo;
-        }
-
-        final Resources r = mContext.getResources();
-        state.label = r.getString(R.string.mobile_data);
-        boolean mobileDataEnabled = mDataController.isMobileDataSupported()
-                && mDataController.isMobileDataEnabled();
-        state.value = mobileDataEnabled;
-        state.activityIn = mobileDataEnabled && cb.activityIn;
-        state.activityOut = mobileDataEnabled && cb.activityOut;
-        state.expandedAccessibilityClassName = Switch.class.getName();
-        if (cb.noSim) {
-            state.icon = ResourceIcon.get(R.drawable.ic_qs_no_sim);
-        } else {
-            state.icon = ResourceIcon.get(R.drawable.ic_swap_vert);
-        }
-
-        if (cb.noSim) {
-            state.state = Tile.STATE_UNAVAILABLE;
-            state.secondaryLabel = r.getString(R.string.keyguard_missing_sim_message_short);
-        } else if (cb.airplaneModeEnabled) {
-            state.state = Tile.STATE_UNAVAILABLE;
-            state.secondaryLabel = r.getString(R.string.status_bar_airplane);
-        } else if (mobileDataEnabled) {
-            state.state = Tile.STATE_ACTIVE;
-            state.secondaryLabel = appendMobileDataType(
-                    // Only show carrier name if there are more than 1 subscription
-                    cb.multipleSubs ? cb.dataSubscriptionName : "",
-                    getMobileDataContentName(cb));
-        } else {
-            state.state = Tile.STATE_INACTIVE;
-            state.secondaryLabel = r.getString(R.string.cell_data_off);
-        }
-
-        state.contentDescription = state.label;
-        if (state.state == Tile.STATE_INACTIVE) {
-            // This information is appended later by converting the Tile.STATE_INACTIVE state.
-            state.stateDescription = "";
-        } else {
-            state.stateDescription = state.secondaryLabel;
-        }
-    }
-
-    private CharSequence appendMobileDataType(CharSequence current, CharSequence dataType) {
-        if (TextUtils.isEmpty(dataType)) {
-            return Html.fromHtml(current.toString(), 0);
-        }
-        if (TextUtils.isEmpty(current)) {
-            return Html.fromHtml(dataType.toString(), 0);
-        }
-        String concat = mContext.getString(R.string.mobile_carrier_text_format, current, dataType);
-        return Html.fromHtml(concat, 0);
-    }
-
-    private CharSequence getMobileDataContentName(CallbackInfo cb) {
-        if (cb.roaming && !TextUtils.isEmpty(cb.dataContentDescription)) {
-            String roaming = mContext.getString(R.string.data_connection_roaming);
-            String dataDescription = cb.dataContentDescription.toString();
-            return mContext.getString(R.string.mobile_data_text_format, roaming, dataDescription);
-        }
-        if (cb.roaming) {
-            return mContext.getString(R.string.data_connection_roaming);
-        }
-        return cb.dataContentDescription;
-    }
-
-    @Override
-    public int getMetricsCategory() {
-        return MetricsEvent.QS_CELLULAR;
-    }
-
-    @Override
-    public boolean isAvailable() {
-        return mController.hasMobileDataFeature()
-            && mHost.getUserContext().getUserId() == UserHandle.USER_SYSTEM;
-    }
-
-    private static final class CallbackInfo {
-        boolean airplaneModeEnabled;
-        @Nullable
-        CharSequence dataSubscriptionName;
-        @Nullable
-        CharSequence dataContentDescription;
-        boolean activityIn;
-        boolean activityOut;
-        boolean noSim;
-        boolean roaming;
-        boolean multipleSubs;
-    }
-
-    private final class CellSignalCallback implements SignalCallback {
-        private final CallbackInfo mInfo = new CallbackInfo();
-
-        @Override
-        public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
-            if (indicators.qsIcon == null) {
-                // Not data sim, don't display.
-                return;
-            }
-            mInfo.dataSubscriptionName = mController.getMobileDataNetworkName();
-            mInfo.dataContentDescription = indicators.qsDescription != null
-                    ? indicators.typeContentDescriptionHtml : null;
-            mInfo.activityIn = indicators.activityIn;
-            mInfo.activityOut = indicators.activityOut;
-            mInfo.roaming = indicators.roaming;
-            mInfo.multipleSubs = mController.getNumberSubscriptions() > 1;
-            refreshState(mInfo);
-        }
-
-        @Override
-        public void setNoSims(boolean show, boolean simDetected) {
-            mInfo.noSim = show;
-            refreshState(mInfo);
-        }
-
-        @Override
-        public void setIsAirplaneMode(@NonNull IconState icon) {
-            mInfo.airplaneModeEnabled = icon.visible;
-            refreshState(mInfo);
-        }
-    }
-
-    static Intent getCellularSettingIntent() {
-        Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);
-        int dataSub = SubscriptionManager.getDefaultDataSubscriptionId();
-        if (dataSub != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            intent.putExtra(Settings.EXTRA_SUB_ID,
-                    SubscriptionManager.getDefaultDataSubscriptionId());
-        }
-        return intent;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
index 6dfcf5c..b6205d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
@@ -48,6 +48,8 @@
 /** Quick settings tile: Color correction **/
 public class ColorCorrectionTile extends QSTileImpl<BooleanState> {
 
+    public static final String TILE_SPEC = "color_correction";
+
     private final Icon mIcon = ResourceIcon.get(drawable.ic_qs_color_correction);
     private final SettingObserver mSetting;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
index a31500c..9a44e83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
@@ -49,6 +49,7 @@
 /** Quick settings tile: Invert colors **/
 public class ColorInversionTile extends QSTileImpl<BooleanState> {
 
+    public static final String TILE_SPEC = "inversion";
     private final SettingObserver mSetting;
 
     @Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index 2fc99f3..add517e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -48,6 +48,8 @@
 public class DataSaverTile extends QSTileImpl<BooleanState> implements
         DataSaverController.Listener{
 
+    public static final String TILE_SPEC = "saver";
+
     private static final String INTERACTION_JANK_TAG = "start_data_saver";
 
     private final DataSaverController mDataSaverController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
index 41d8549..01164fb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
@@ -159,4 +159,8 @@
     override fun getTileLabel(): CharSequence {
         return mContext.getText(controlsComponent.getTileTitleId())
     }
+
+    companion object {
+        const val TILE_SPEC = "controls"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index 8b7f53f..434fe45 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -67,6 +67,8 @@
 /** Quick settings tile: Do not disturb **/
 public class DndTile extends QSTileImpl<BooleanState> {
 
+    public static final String TILE_SPEC = "dnd";
+
     private static final Intent ZEN_SETTINGS =
             new Intent(Settings.ACTION_ZEN_MODE_SETTINGS);
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index 5bc209a..53774e8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -60,6 +60,8 @@
 /** Quick settings tile: Screensaver (dream) **/
 public class DreamTile extends QSTileImpl<QSTile.BooleanState> {
 
+    public static final String TILE_SPEC = "dream";
+
     private static final String LOG_TAG = "QSDream";
     // TODO: consider 1 animated icon instead
     private final Icon mIconDocked = ResourceIcon.get(R.drawable.ic_qs_screen_saver);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
index a747926..e091a75 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -49,6 +49,7 @@
 public class FlashlightTile extends QSTileImpl<BooleanState> implements
         FlashlightController.FlashlightListener {
 
+    public static final String TILE_SPEC = "flashlight";
     private final FlashlightController mFlashlightController;
 
     @Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
new file mode 100644
index 0000000..9b4ac1b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.qs.tiles
+
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.logging.MetricsLogger
+import com.android.systemui.R
+import com.android.systemui.accessibility.fontscaling.FontScalingDialog
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.settings.SystemSettings
+import javax.inject.Inject
+
+class FontScalingTile
+@Inject
+constructor(
+    host: QSHost,
+    @Background backgroundLooper: Looper,
+    @Main mainHandler: Handler,
+    falsingManager: FalsingManager,
+    metricsLogger: MetricsLogger,
+    statusBarStateController: StatusBarStateController,
+    activityStarter: ActivityStarter,
+    qsLogger: QSLogger,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val systemSettings: SystemSettings,
+    private val featureFlags: FeatureFlags
+) :
+    QSTileImpl<QSTile.State?>(
+        host,
+        backgroundLooper,
+        mainHandler,
+        falsingManager,
+        metricsLogger,
+        statusBarStateController,
+        activityStarter,
+        qsLogger
+    ) {
+    private val icon = ResourceIcon.get(R.drawable.ic_qs_font_scaling)
+
+    override fun isAvailable(): Boolean {
+        return featureFlags.isEnabled(Flags.ENABLE_FONT_SCALING_TILE)
+    }
+
+    override fun newTileState(): QSTile.State {
+        val state = QSTile.State()
+        state.handlesLongClick = false
+        return state
+    }
+
+    override fun handleClick(view: View?) {
+        mUiHandler.post {
+            val dialog: SystemUIDialog = FontScalingDialog(mContext, systemSettings)
+            if (view != null) {
+                dialogLaunchAnimator.showFromView(
+                    dialog,
+                    view,
+                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
+                )
+            } else {
+                dialog.show()
+            }
+        }
+    }
+
+    override fun handleUpdateState(state: QSTile.State?, arg: Any?) {
+        state?.label = mContext.getString(R.string.quick_settings_font_scaling_label)
+        state?.icon = icon
+    }
+
+    override fun getLongClickIntent(): Intent? {
+        return null
+    }
+
+    override fun getTileLabel(): CharSequence {
+        return mContext.getString(R.string.quick_settings_font_scaling_label)
+    }
+
+    companion object {
+        const val TILE_SPEC = "font_scaling"
+        private const val INTERACTION_JANK_TAG = "font_scaling"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index 624def6..6bf8b76 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -51,6 +51,7 @@
 /** Quick settings tile: Hotspot **/
 public class HotspotTile extends QSTileImpl<BooleanState> {
 
+    public static final String TILE_SPEC = "hotspot";
     private final HotspotController mHotspotController;
     private final DataSaverController mDataSaverController;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 51de522..12e9aeb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -67,6 +67,9 @@
 
 /** Quick settings tile: Internet **/
 public class InternetTile extends QSTileImpl<SignalState> {
+
+    public static final String TILE_SPEC = "internet";
+
     private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS);
 
     protected final NetworkController mController;
@@ -255,17 +258,19 @@
                 Log.d(TAG, "setWifiIndicators: " + indicators);
             }
             mWifiInfo.mEnabled = indicators.enabled;
-            if (indicators.qsIcon == null) {
-                return;
-            }
-            mWifiInfo.mConnected = indicators.qsIcon.visible;
-            mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
-            mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
-            mWifiInfo.mEnabled = indicators.enabled;
             mWifiInfo.mSsid = indicators.description;
             mWifiInfo.mIsTransient = indicators.isTransient;
             mWifiInfo.mStatusLabel = indicators.statusLabel;
-            refreshState(mWifiInfo);
+            if (indicators.qsIcon != null) {
+                mWifiInfo.mConnected = indicators.qsIcon.visible;
+                mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
+                mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
+                refreshState(mWifiInfo);
+            } else {
+                mWifiInfo.mConnected = false;
+                mWifiInfo.mWifiSignalIconId = 0;
+                mWifiInfo.mWifiSignalContentDescription = null;
+            }
         }
 
         @Override
@@ -529,6 +534,9 @@
         if (DEBUG) {
             Log.d(TAG, "handleUpdateEthernetState: " + "EthernetCallbackInfo = " + cb.toString());
         }
+        if (!cb.mConnected) {
+            return;
+        }
         final Resources r = mContext.getResources();
         state.label = r.getString(R.string.quick_settings_internet_label);
         state.state = Tile.STATE_ACTIVE;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
index 9466a69..89d402a3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
@@ -48,6 +48,8 @@
 /** Quick settings tile: Location **/
 public class LocationTile extends QSTileImpl<BooleanState> {
 
+    public static final String TILE_SPEC = "location";
+
     private final LocationController mController;
     private final KeyguardStateController mKeyguard;
     private final Callback mCallback = new Callback();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java
index e547095..2e475d4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java
@@ -45,6 +45,8 @@
 
 public class MicrophoneToggleTile extends SensorPrivacyToggleTile {
 
+    public static final String TILE_SPEC = "mictoggle";
+
     @Inject
     protected MicrophoneToggleTile(QSHost host,
             @Background Looper backgroundLooper,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
index a61f0ce..e189f80 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
@@ -51,7 +51,9 @@
 /** Quick settings tile: Enable/Disable NFC **/
 public class NfcTile extends QSTileImpl<BooleanState> {
 
-    private static final String NFC = "nfc";
+    public static final String TILE_SPEC = "nfc";
+
+    private static final String NFC = TILE_SPEC;
     private final Icon mIcon = ResourceIcon.get(R.drawable.ic_qs_nfc);
 
     @Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
index 0e9f659..aacd53b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
@@ -61,6 +61,8 @@
 public class NightDisplayTile extends QSTileImpl<BooleanState> implements
         NightDisplayListener.Callback {
 
+    public static final String TILE_SPEC = "night";
+
     /**
      * Pattern for {@link java.time.format.DateTimeFormatter} used to approximate the time to the
      * nearest hour and add on the AM/PM indicator.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
index 7e17124..ae67d99 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
@@ -47,6 +47,9 @@
 
 /** Quick settings tile: One-handed mode **/
 public class OneHandedModeTile extends QSTileImpl<BooleanState> {
+
+    public static final String TILE_SPEC = "onehanded";
+
     private final Icon mIcon = ResourceIcon.get(
             com.android.internal.R.drawable.ic_qs_one_handed_mode);
     private final SettingObserver mSetting;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
index 6d50b56..92f5272 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
@@ -44,6 +44,9 @@
 
 /** Quick settings tile: QR Code Scanner **/
 public class QRCodeScannerTile extends QSTileImpl<QSTile.State> {
+
+    public static final String TILE_SPEC = "qr_code_scanner";
+
     private static final String TAG = "QRCodeScanner";
 
     private final CharSequence mLabel = mContext.getString(R.string.qr_code_scanner_title);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index 248c78e..4a3c563 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -62,6 +62,8 @@
 /** Quick settings tile: Quick access wallet **/
 public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
 
+    public static final String TILE_SPEC = "wallet";
+
     private static final String TAG = "QuickAccessWalletTile";
     private static final String FEATURE_CHROME_OS = "org.chromium.arc";
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
index 1dac339..10f1ce4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
@@ -49,6 +49,7 @@
 public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState>
         implements ReduceBrightColorsController.Listener{
 
+    public static final String TILE_SPEC = "reduce_brightness";
     private final boolean mIsAvailable;
     private final ReduceBrightColorsController mReduceBrightColorsController;
     private boolean mIsListening;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index 600874f..8888c73 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -57,6 +57,9 @@
 /** Quick settings tile: Rotation **/
 public class RotationLockTile extends QSTileImpl<BooleanState> implements
         BatteryController.BatteryStateChangeCallback {
+
+    public static final String TILE_SPEC = "rotation";
+
     private static final String EMPTY_SECONDARY_STRING = "";
 
     private final Icon mIcon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_auto_rotate);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 64a8a14..07b50c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -54,6 +54,9 @@
  */
 public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
         implements RecordingController.RecordingStateChangeCallback {
+
+    public static final String TILE_SPEC = "screenrecord";
+
     private static final String TAG = "ScreenRecordTile";
     private static final String INTERACTION_JANK_TAG = "screen_record";
 
@@ -171,8 +174,9 @@
             getHost().collapsePanels();
         };
 
-        Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
+        final Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
                 mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
+
         ActivityStarter.OnDismissAction dismissAction = () -> {
             if (shouldAnimateFromView) {
                 mDialogLaunchAnimator.showFromView(dialog, view, new DialogCuj(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
index 92f6690a..809689c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
@@ -17,7 +17,6 @@
 package com.android.systemui.qs.tiles;
 
 import android.app.UiModeManager;
-import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Handler;
@@ -60,6 +59,9 @@
 public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements
         ConfigurationController.ConfigurationListener,
         BatteryController.BatteryStateChangeCallback {
+
+    public static final String TILE_SPEC = "dark";
+
     public static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a");
     private final UiModeManager mUiModeManager;
     private final BatteryController mBatteryController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 57a00c9..b6b657e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -204,15 +204,6 @@
             Trace.endSection();
         }
 
-        @Override
-        public void onUserListItemClicked(@NonNull UserRecord record,
-                @Nullable UserSwitchDialogController.DialogShower dialogShower) {
-            if (dialogShower != null) {
-                mDialogShower.dismiss();
-            }
-            super.onUserListItemClicked(record, dialogShower);
-        }
-
         public void linkToViewGroup(ViewGroup viewGroup) {
             PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
deleted file mode 100644
index b2be56cc..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
-import android.service.quicksettings.Tile;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.widget.Switch;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSIconView;
-import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.plugins.qs.QSTile.SignalState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.AlphaControlledSignalTileView;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.tileimpl.QSIconViewImpl;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.connectivity.AccessPointController;
-import com.android.systemui.statusbar.connectivity.NetworkController;
-import com.android.systemui.statusbar.connectivity.SignalCallback;
-import com.android.systemui.statusbar.connectivity.WifiIcons;
-import com.android.systemui.statusbar.connectivity.WifiIndicators;
-
-import javax.inject.Inject;
-
-/** Quick settings tile: Wifi **/
-public class WifiTile extends QSTileImpl<SignalState> {
-    private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS);
-
-    protected final NetworkController mController;
-    private final AccessPointController mWifiController;
-    private final QSTile.SignalState mStateBeforeClick = newTileState();
-
-    protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback();
-    private boolean mExpectDisabled;
-
-    @Inject
-    public WifiTile(
-            QSHost host,
-            @Background Looper backgroundLooper,
-            @Main Handler mainHandler,
-            FalsingManager falsingManager,
-            MetricsLogger metricsLogger,
-            StatusBarStateController statusBarStateController,
-            ActivityStarter activityStarter,
-            QSLogger qsLogger,
-            NetworkController networkController,
-            AccessPointController accessPointController
-    ) {
-        super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
-                statusBarStateController, activityStarter, qsLogger);
-        mController = networkController;
-        mWifiController = accessPointController;
-        mController.observe(getLifecycle(), mSignalCallback);
-        mStateBeforeClick.spec = "wifi";
-    }
-
-    @Override
-    public SignalState newTileState() {
-        return new SignalState();
-    }
-
-    @Override
-    public QSIconView createTileView(Context context) {
-        return new AlphaControlledSignalTileView(context);
-    }
-
-    @Override
-    public Intent getLongClickIntent() {
-        return WIFI_SETTINGS;
-    }
-
-    @Override
-    protected void handleClick(@Nullable View view) {
-        // Secondary clicks are header clicks, just toggle.
-        mState.copyTo(mStateBeforeClick);
-        boolean wifiEnabled = mState.value;
-        // Immediately enter transient state when turning on wifi.
-        refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
-        mController.setWifiEnabled(!wifiEnabled);
-        mExpectDisabled = wifiEnabled;
-        if (mExpectDisabled) {
-            mHandler.postDelayed(() -> {
-                if (mExpectDisabled) {
-                    mExpectDisabled = false;
-                    refreshState();
-                }
-            }, QSIconViewImpl.QS_ANIM_LENGTH);
-        }
-    }
-
-    @Override
-    protected void handleSecondaryClick(@Nullable View view) {
-        if (!mWifiController.canConfigWifi()) {
-            mActivityStarter.postStartActivityDismissingKeyguard(
-                    new Intent(Settings.ACTION_WIFI_SETTINGS), 0);
-            return;
-        }
-        if (!mState.value) {
-            mController.setWifiEnabled(true);
-        }
-    }
-
-    @Override
-    public CharSequence getTileLabel() {
-        return mContext.getString(R.string.quick_settings_wifi_label);
-    }
-
-    @Override
-    protected void handleUpdateState(SignalState state, Object arg) {
-        if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg);
-        final CallbackInfo cb = mSignalCallback.mInfo;
-        if (mExpectDisabled) {
-            if (cb.enabled) {
-                return; // Ignore updates until disabled event occurs.
-            } else {
-                mExpectDisabled = false;
-            }
-        }
-        boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
-        boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0)
-                && (cb.ssid != null || cb.wifiSignalIconId != WifiIcons.QS_WIFI_NO_NETWORK);
-        boolean wifiNotConnected = (cb.ssid == null)
-                && (cb.wifiSignalIconId == WifiIcons.QS_WIFI_NO_NETWORK);
-        if (state.slash == null) {
-            state.slash = new SlashState();
-            state.slash.rotation = 6;
-        }
-        state.slash.isSlashed = false;
-        boolean isTransient = transientEnabling || cb.isTransient;
-        state.secondaryLabel = getSecondaryLabel(isTransient, cb.statusLabel);
-        state.state = Tile.STATE_ACTIVE;
-        state.dualTarget = true;
-        state.value = transientEnabling || cb.enabled;
-        state.activityIn = cb.enabled && cb.activityIn;
-        state.activityOut = cb.enabled && cb.activityOut;
-        final StringBuffer minimalContentDescription = new StringBuffer();
-        final StringBuffer minimalStateDescription = new StringBuffer();
-        final Resources r = mContext.getResources();
-        if (isTransient) {
-            state.icon = ResourceIcon.get(
-                    com.android.internal.R.drawable.ic_signal_wifi_transient_animation);
-            state.label = r.getString(R.string.quick_settings_wifi_label);
-        } else if (!state.value) {
-            state.slash.isSlashed = true;
-            state.state = Tile.STATE_INACTIVE;
-            state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_DISABLED);
-            state.label = r.getString(R.string.quick_settings_wifi_label);
-        } else if (wifiConnected) {
-            state.icon = ResourceIcon.get(cb.wifiSignalIconId);
-            state.label = cb.ssid != null ? removeDoubleQuotes(cb.ssid) : getTileLabel();
-        } else if (wifiNotConnected) {
-            state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK);
-            state.label = r.getString(R.string.quick_settings_wifi_label);
-        } else {
-            state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK);
-            state.label = r.getString(R.string.quick_settings_wifi_label);
-        }
-        minimalContentDescription.append(
-                mContext.getString(R.string.quick_settings_wifi_label)).append(",");
-        if (state.value) {
-            if (wifiConnected) {
-                minimalStateDescription.append(cb.wifiSignalContentDescription);
-                minimalContentDescription.append(removeDoubleQuotes(cb.ssid));
-                if (!TextUtils.isEmpty(state.secondaryLabel)) {
-                    minimalContentDescription.append(",").append(state.secondaryLabel);
-                }
-            }
-        }
-        state.stateDescription = minimalStateDescription.toString();
-        state.contentDescription = minimalContentDescription.toString();
-        state.dualLabelContentDescription = r.getString(
-                R.string.accessibility_quick_settings_open_settings, getTileLabel());
-        state.expandedAccessibilityClassName = Switch.class.getName();
-    }
-
-    private CharSequence getSecondaryLabel(boolean isTransient, String statusLabel) {
-        return isTransient
-                ? mContext.getString(R.string.quick_settings_wifi_secondary_label_transient)
-                : statusLabel;
-    }
-
-    @Override
-    public int getMetricsCategory() {
-        return MetricsEvent.QS_WIFI;
-    }
-
-    @Override
-    public boolean isAvailable() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI);
-    }
-
-    @Nullable
-    private static String removeDoubleQuotes(String string) {
-        if (string == null) return null;
-        final int length = string.length();
-        if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
-            return string.substring(1, length - 1);
-        }
-        return string;
-    }
-
-    protected static final class CallbackInfo {
-        boolean enabled;
-        boolean connected;
-        int wifiSignalIconId;
-        @Nullable
-        String ssid;
-        boolean activityIn;
-        boolean activityOut;
-        @Nullable
-        String wifiSignalContentDescription;
-        boolean isTransient;
-        @Nullable
-        public String statusLabel;
-
-        @Override
-        public String toString() {
-            return new StringBuilder("CallbackInfo[")
-                    .append("enabled=").append(enabled)
-                    .append(",connected=").append(connected)
-                    .append(",wifiSignalIconId=").append(wifiSignalIconId)
-                    .append(",ssid=").append(ssid)
-                    .append(",activityIn=").append(activityIn)
-                    .append(",activityOut=").append(activityOut)
-                    .append(",wifiSignalContentDescription=").append(wifiSignalContentDescription)
-                    .append(",isTransient=").append(isTransient)
-                    .append(']').toString();
-        }
-    }
-
-    protected final class WifiSignalCallback implements SignalCallback {
-        final CallbackInfo mInfo = new CallbackInfo();
-
-        @Override
-        public void setWifiIndicators(@NonNull WifiIndicators indicators) {
-            if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + indicators.enabled);
-            if (indicators.qsIcon == null) {
-                return;
-            }
-            mInfo.enabled = indicators.enabled;
-            mInfo.connected = indicators.qsIcon.visible;
-            mInfo.wifiSignalIconId = indicators.qsIcon.icon;
-            mInfo.ssid = indicators.description;
-            mInfo.activityIn = indicators.activityIn;
-            mInfo.activityOut = indicators.activityOut;
-            mInfo.wifiSignalContentDescription = indicators.qsIcon.contentDescription;
-            mInfo.isTransient = indicators.isTransient;
-            mInfo.statusLabel = indicators.statusLabel;
-            refreshState();
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index 72c6bfe..6a5c990 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -49,6 +49,9 @@
 /** Quick settings tile: Work profile on/off */
 public class WorkModeTile extends QSTileImpl<BooleanState> implements
         ManagedProfileController.Callback {
+
+    public static final String TILE_SPEC = "work";
+
     private final Icon mIcon = ResourceIcon.get(R.drawable.stat_sys_managed_profile_status);
 
     private final ManagedProfileController mProfileController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index 1ed18c3..c0e4995 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -805,6 +805,11 @@
     }
 
     @Override
+    public void onCarrierNetworkChange(boolean active) {
+        mHandler.post(() -> updateDialog(true /* shouldUpdateMobileNetwork */));
+    }
+
+    @Override
     @WorkerThread
     public void onAccessPointsChanged(@Nullable List<WifiEntry> wifiEntries,
             @Nullable WifiEntry connectedEntry, boolean hasMoreWifiEntries) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 2e6ea0e..557b718 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -206,6 +206,8 @@
     protected boolean mHasEthernet = false;
     @VisibleForTesting
     protected ConnectedWifiInternetMonitor mConnectedWifiInternetMonitor;
+    @VisibleForTesting
+    protected boolean mCarrierNetworkChangeMode;
 
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback =
             new KeyguardUpdateMonitorCallback() {
@@ -507,10 +509,13 @@
     Drawable getSignalStrengthIcon(int subId, Context context, int level, int numLevels,
             int iconType, boolean cutOut) {
         boolean isForDds = subId == mDefaultDataSubId;
+        int levelDrawable =
+                mCarrierNetworkChangeMode ? SignalDrawable.getCarrierChangeState(numLevels)
+                        : SignalDrawable.getState(level, numLevels, cutOut);
         if (isForDds) {
-            mSignalDrawable.setLevel(SignalDrawable.getState(level, numLevels, cutOut));
+            mSignalDrawable.setLevel(levelDrawable);
         } else {
-            mSecondarySignalDrawable.setLevel(SignalDrawable.getState(level, numLevels, cutOut));
+            mSecondarySignalDrawable.setLevel(levelDrawable);
         }
 
         // Make the network type drawable
@@ -672,10 +677,13 @@
         }
 
         int resId = Objects.requireNonNull(mapIconSets(config).get(iconKey)).dataContentDescription;
+        SignalIcon.MobileIconGroup iconGroup;
         if (isCarrierNetworkActive()) {
-            SignalIcon.MobileIconGroup carrierMergedWifiIconGroup =
-                    TelephonyIcons.CARRIER_MERGED_WIFI;
-            resId = carrierMergedWifiIconGroup.dataContentDescription;
+            iconGroup = TelephonyIcons.CARRIER_MERGED_WIFI;
+            resId = iconGroup.dataContentDescription;
+        } else if (mCarrierNetworkChangeMode) {
+            iconGroup = TelephonyIcons.CARRIER_NETWORK_CHANGE;
+            resId = iconGroup.dataContentDescription;
         }
 
         return resId != 0
@@ -1066,7 +1074,8 @@
             TelephonyCallback.DisplayInfoListener,
             TelephonyCallback.ServiceStateListener,
             TelephonyCallback.SignalStrengthsListener,
-            TelephonyCallback.UserMobileDataStateListener {
+            TelephonyCallback.UserMobileDataStateListener,
+            TelephonyCallback.CarrierNetworkListener{
 
         private final int mSubId;
         private InternetTelephonyCallback(int subId) {
@@ -1098,6 +1107,12 @@
         public void onUserMobileDataStateChanged(boolean enabled) {
             mCallback.onUserMobileDataStateChanged(enabled);
         }
+
+        @Override
+        public void onCarrierNetworkChange(boolean active) {
+            mCarrierNetworkChangeMode = active;
+            mCallback.onCarrierNetworkChange(active);
+        }
     }
 
     private class InternetOnSubscriptionChangedListener
@@ -1267,6 +1282,8 @@
 
         void onDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo);
 
+        void onCarrierNetworkChange(boolean active);
+
         void dismissDialog();
 
         void onAccessPointsChanged(@Nullable List<WifiEntry> wifiEntries,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index 314252b..4c9c99c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.qs.QSUserSwitcherEvent
 import com.android.systemui.qs.tiles.UserDetailView
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.user.ui.dialog.DialogShowerImpl
 import javax.inject.Inject
 import javax.inject.Provider
 
@@ -130,19 +131,6 @@
         }
     }
 
-    private class DialogShowerImpl(
-        private val animateFrom: Dialog,
-        private val dialogLaunchAnimator: DialogLaunchAnimator
-    ) : DialogInterface by animateFrom, DialogShower {
-        override fun showDialog(dialog: Dialog, cuj: DialogCuj) {
-            dialogLaunchAnimator.showFromDialog(
-                dialog,
-                animateFrom = animateFrom,
-                cuj
-            )
-        }
-    }
-
     interface DialogShower : DialogInterface {
         fun showDialog(dialog: Dialog, cuj: DialogCuj)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
index 802db7e..dc3c820 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
@@ -27,7 +27,6 @@
 import com.android.systemui.CoreStartable;
 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.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 4d005be..25ff308b 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -17,20 +17,20 @@
 package com.android.systemui.recents;
 
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
-import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
 
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SUPPORTS_WINDOW_CORNERS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_WINDOW_CORNER_RADIUS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
@@ -44,8 +44,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
-import android.graphics.Insets;
-import android.graphics.Rect;
 import android.graphics.Region;
 import android.hardware.input.InputManager;
 import android.os.Binder;
@@ -77,6 +75,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -90,17 +89,18 @@
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.buttons.KeyButtonView;
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.CallbackController;
+import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
 import com.android.wm.shell.sysui.ShellInterface;
 
 import java.io.PrintWriter;
@@ -146,7 +146,9 @@
     private final CommandQueue mCommandQueue;
     private final UserTracker mUserTracker;
     private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
+    private final Optional<UnfoldTransitionProgressForwarder> mUnfoldTransitionProgressForwarder;
     private final UiEventLogger mUiEventLogger;
+    private final DisplayTracker mDisplayTracker;
 
     private Region mActiveNavBarRegion;
     private SurfaceControl mNavigationBarSurface;
@@ -228,11 +230,11 @@
 
         @Override
         public void onImeSwitcherPressed() {
-            // TODO(b/204901476) We're intentionally using DEFAULT_DISPLAY for now since
+            // TODO(b/204901476) We're intentionally using the default display for now since
             // Launcher/Taskbar isn't display aware.
             mContext.getSystemService(InputMethodManager.class)
                     .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
-                            DEFAULT_DISPLAY);
+                            mDisplayTracker.getDefaultDisplayId());
             mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
         }
 
@@ -311,7 +313,7 @@
                         intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
                         intent.addFlags(
                                 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-                        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                        mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
                     });
         }
 
@@ -322,18 +324,8 @@
         }
 
         @Override
-        public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
-                Insets visibleInsets, Task.TaskKey task) {
-            mScreenshotHelper.provideScreenshot(
-                    screenImageBundle,
-                    locationInScreen,
-                    visibleInsets,
-                    task.id,
-                    task.userId,
-                    task.sourceComponent,
-                    SCREENSHOT_OVERVIEW,
-                    mHandler,
-                    null);
+        public void takeScreenshot(ScreenshotRequest request) {
+            mScreenshotHelper.takeScreenshot(request, mHandler, null);
         }
 
         @Override
@@ -426,6 +418,10 @@
             params.putBoolean(KEY_EXTRA_SUPPORTS_WINDOW_CORNERS, mSupportsRoundedCornersOnWindows);
             params.putBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER,
                     mSysuiUnlockAnimationController.asBinder());
+            mUnfoldTransitionProgressForwarder.ifPresent(
+                    unfoldProgressForwarder -> params.putBinder(
+                            KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER,
+                            unfoldProgressForwarder.asBinder()));
             // Add all the interfaces exposed by the shell
             mShellInterface.createExternalInterfaces(params);
 
@@ -520,9 +516,12 @@
             UserTracker userTracker,
             ScreenLifecycle screenLifecycle,
             UiEventLogger uiEventLogger,
+            DisplayTracker displayTracker,
             KeyguardUnlockAnimationController sysuiUnlockAnimationController,
             AssistUtils assistUtils,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder
+    ) {
         // b/241601880: This component shouldn't be running for a non-primary user
         if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) {
             Log.e(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable());
@@ -547,6 +546,8 @@
         mSysUiState = sysUiState;
         mSysUiState.addCallback(this::notifySystemUiStateFlags);
         mUiEventLogger = uiEventLogger;
+        mDisplayTracker = displayTracker;
+        mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder;
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
 
@@ -665,13 +666,14 @@
     }
 
     private void onStatusBarStateChanged(boolean keyguardShowing, boolean keyguardOccluded,
-            boolean bouncerShowing, boolean isDozing, boolean panelExpanded) {
+            boolean bouncerShowing, boolean isDozing, boolean panelExpanded, boolean isDreaming) {
         mSysUiState.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
                         keyguardShowing && !keyguardOccluded)
                 .setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED,
                         keyguardShowing && keyguardOccluded)
                 .setFlag(SYSUI_STATE_BOUNCER_SHOWING, bouncerShowing)
                 .setFlag(SYSUI_STATE_DEVICE_DOZING, isDozing)
+                .setFlag(SYSUI_STATE_DEVICE_DREAMING, isDreaming)
                 .commitUpdate(mContext.getDisplayId());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockModule.kt b/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockModule.kt
new file mode 100644
index 0000000..9abe90f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockModule.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.rotationlock
+
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.RotationLockTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface RotationLockModule {
+
+    /** Inject RotationLockTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(RotationLockTile.TILE_SPEC)
+    fun bindRotationLockTile(rotationLockTile: RotationLockTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index b8684ee..db2e62b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -36,6 +36,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
@@ -46,6 +48,8 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /**
  * Helper class to initiate a screen recording
  */
@@ -60,6 +64,8 @@
     private CountDownTimer mCountDownTimer = null;
     private final Executor mMainExecutor;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final Context mContext;
+    private final FeatureFlags mFlags;
     private final UserContextProvider mUserContextProvider;
     private final UserTracker mUserTracker;
 
@@ -70,6 +76,8 @@
     private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners =
             new CopyOnWriteArrayList<>();
 
+    private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver;
+
     @VisibleForTesting
     final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -100,22 +108,44 @@
     @Inject
     public RecordingController(@Main Executor mainExecutor,
             BroadcastDispatcher broadcastDispatcher,
+            Context context,
+            FeatureFlags flags,
             UserContextProvider userContextProvider,
+            Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
             UserTracker userTracker) {
         mMainExecutor = mainExecutor;
+        mContext = context;
+        mFlags = flags;
+        mDevicePolicyResolver = devicePolicyResolver;
         mBroadcastDispatcher = broadcastDispatcher;
         mUserContextProvider = userContextProvider;
         mUserTracker = userTracker;
     }
 
-    /** Create a dialog to show screen recording options to the user. */
+    /**
+     * MediaProjection host is SystemUI for the screen recorder, so return 'my user handle'
+     */
+    private UserHandle getHostUserHandle() {
+        return UserHandle.of(UserHandle.myUserId());
+    }
+
+    /** Create a dialog to show screen recording options to the user.
+     *  If screen capturing is currently not allowed it will return a dialog
+     *  that warns users about it. */
     public Dialog createScreenRecordDialog(Context context, FeatureFlags flags,
                                            DialogLaunchAnimator dialogLaunchAnimator,
                                            ActivityStarter activityStarter,
                                            @Nullable Runnable onStartRecordingClicked) {
+        if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)
+                && mDevicePolicyResolver.get()
+                        .isScreenCaptureCompletelyDisabled(getHostUserHandle())) {
+            return new ScreenCaptureDisabledDialog(mContext);
+        }
+
         return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
-                ? new ScreenRecordPermissionDialog(context, this, activityStarter,
-                        dialogLaunchAnimator, mUserContextProvider, onStartRecordingClicked)
+                ? new ScreenRecordPermissionDialog(context,  getHostUserHandle(), this,
+                    activityStarter, dialogLaunchAnimator, mUserContextProvider,
+                    onStartRecordingClicked)
                 : new ScreenRecordDialog(context, this, activityStarter,
                 mUserContextProvider, flags, dialogLaunchAnimator, onStartRecordingClicked);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
new file mode 100644
index 0000000..7467805
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.screenrecord
+
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.ScreenRecordTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface ScreenRecordModule {
+    /** Inject ScreenRecordTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(ScreenRecordTile.TILE_SPEC)
+    fun bindScreenRecordTile(screenRecordTile: ScreenRecordTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 44b18ec..dd21be9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -23,6 +23,7 @@
 import android.os.Handler
 import android.os.Looper
 import android.os.ResultReceiver
+import android.os.UserHandle
 import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
@@ -41,6 +42,7 @@
 /** Dialog to select screen recording options */
 class ScreenRecordPermissionDialog(
     context: Context?,
+    private val hostUserHandle: UserHandle,
     private val controller: RecordingController,
     private val activityStarter: ActivityStarter,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
@@ -77,6 +79,12 @@
                     MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
                     CaptureTargetResultReceiver()
                 )
+
+                intent.putExtra(
+                    MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+                    hostUserHandle
+                )
+
                 val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!)
                 if (animationController == null) {
                     dismiss()
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
index 017e57f..310baaf 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
@@ -25,8 +25,22 @@
 import com.android.systemui.R
 
 object ActionIntentCreator {
+    /** @return a chooser intent to share the given URI. */
+    fun createShareIntent(uri: Uri) = createShareIntent(uri, null, null)
+
     /** @return a chooser intent to share the given URI with the optional provided subject. */
-    fun createShareIntent(uri: Uri, subject: String?): Intent {
+    fun createShareIntentWithSubject(uri: Uri, subject: String?) =
+        createShareIntent(uri, subject = subject)
+
+    /** @return a chooser intent to share the given URI with the optional provided extra text. */
+    fun createShareIntentWithExtraText(uri: Uri, extraText: String?) =
+        createShareIntent(uri, extraText = extraText)
+
+    private fun createShareIntent(
+        uri: Uri,
+        subject: String? = null,
+        extraText: String? = null
+    ): Intent {
         // Create a share intent, this will always go through the chooser activity first
         // which should not trigger auto-enter PiP
         val sharingIntent =
@@ -43,6 +57,7 @@
                     )
 
                 putExtra(Intent.EXTRA_SUBJECT, subject)
+                putExtra(Intent.EXTRA_TEXT, extraText)
                 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                 addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index 01e32b7a..aa8e2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -22,7 +22,6 @@
 import android.os.RemoteException
 import android.os.UserHandle
 import android.util.Log
-import android.view.Display
 import android.view.IRemoteAnimationFinishedCallback
 import android.view.IRemoteAnimationRunner
 import android.view.RemoteAnimationAdapter
@@ -33,6 +32,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.DisplayTracker
 import javax.inject.Inject
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineDispatcher
@@ -47,6 +47,7 @@
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val context: Context,
+    private val displayTracker: DisplayTracker
 ) {
     /**
      * Execute the given intent with startActivity while performing operations for screenshot action
@@ -82,7 +83,7 @@
             val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
             try {
                 WindowManagerGlobal.getWindowManagerService()
-                    .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
+                    .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId)
             } catch (e: Exception) {
                 Log.e(TAG, "Error overriding screenshot app transition", e)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java
index 814b8e9..4f5cb72 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.screenshot;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_EDIT;
 import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE;
 import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT;
@@ -35,6 +33,7 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.WindowManagerGlobal;
 
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
@@ -52,14 +51,17 @@
     private final CentralSurfaces mCentralSurfaces;
     private final ActivityManagerWrapper mActivityManagerWrapper;
     private final ScreenshotSmartActions mScreenshotSmartActions;
+    private final DisplayTracker mDisplayTracker;
 
     @Inject
     public ActionProxyReceiver(Optional<CentralSurfaces> centralSurfacesOptional,
             ActivityManagerWrapper activityManagerWrapper,
-            ScreenshotSmartActions screenshotSmartActions) {
+            ScreenshotSmartActions screenshotSmartActions,
+            DisplayTracker displayTracker) {
         mCentralSurfaces = centralSurfacesOptional.orElse(null);
         mActivityManagerWrapper = activityManagerWrapper;
         mScreenshotSmartActions = screenshotSmartActions;
+        mDisplayTracker = displayTracker;
     }
 
     @Override
@@ -78,7 +80,8 @@
                             ScreenshotController.SCREENSHOT_REMOTE_RUNNER, 0, 0);
                     try {
                         WindowManagerGlobal.getWindowManagerService()
-                                .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
+                                .overridePendingAppTransitionRemote(runner,
+                                        mDisplayTracker.getDefaultDisplayId());
                     } catch (Exception e) {
                         Log.e(TAG, "Error overriding screenshot app transition", e);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
new file mode 100644
index 0000000..146e576
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
+import android.app.IAssistDataReceiver;
+import android.app.assist.AssistContent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/**
+ * Can be used to request the AssistContent from a provided task id, useful for getting the web uri
+ * if provided from the task.
+ *
+ * Forked from
+ * packages/apps/Launcher3/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
+ */
+@SysUISingleton
+public class AssistContentRequester {
+    private static final String TAG = "AssistContentRequester";
+    private static final String ASSIST_KEY_CONTENT = "content";
+
+    /** For receiving content, called on the main thread. */
+    public interface Callback {
+        /**
+         * Called when the {@link android.app.assist.AssistContent} of the requested task is
+         * available.
+         **/
+        void onAssistContentAvailable(AssistContent assistContent);
+    }
+
+    private final IActivityTaskManager mActivityTaskManager;
+    private final String mPackageName;
+    private final Executor mCallbackExecutor;
+    private final Executor mSystemInteractionExecutor;
+
+    // If system loses the callback, our internal cache of original callback will also get cleared.
+    private final Map<Object, Callback> mPendingCallbacks =
+            Collections.synchronizedMap(new WeakHashMap<>());
+
+    @Inject
+    public AssistContentRequester(Context context, @Main Executor mainExecutor,
+            @Background Executor bgExecutor) {
+        mActivityTaskManager = ActivityTaskManager.getService();
+        mPackageName = context.getApplicationContext().getPackageName();
+        mCallbackExecutor = mainExecutor;
+        mSystemInteractionExecutor = bgExecutor;
+    }
+
+    /**
+     * Request the {@link AssistContent} from the task with the provided id.
+     *
+     * @param taskId to query for the content.
+     * @param callback to call when the content is available, called on the main thread.
+     */
+    public void requestAssistContent(final int taskId, final Callback callback) {
+        // ActivityTaskManager interaction here is synchronous, so call off the main thread.
+        mSystemInteractionExecutor.execute(() -> {
+            try {
+                mActivityTaskManager.requestAssistDataForTask(
+                        new AssistDataReceiver(callback, this), taskId, mPackageName);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Requesting assist content failed for task: " + taskId, e);
+            }
+        });
+    }
+
+    private void executeOnMainExecutor(Runnable callback) {
+        mCallbackExecutor.execute(callback);
+    }
+
+    private static final class AssistDataReceiver extends IAssistDataReceiver.Stub {
+
+        // The AssistDataReceiver binder callback object is passed to a system server, that may
+        // keep hold of it for longer than the lifetime of the AssistContentRequester object,
+        // potentially causing a memory leak. In the callback passed to the system server, only
+        // keep a weak reference to the parent object and lookup its callback if it still exists.
+        private final WeakReference<AssistContentRequester> mParentRef;
+        private final Object mCallbackKey = new Object();
+
+        AssistDataReceiver(Callback callback, AssistContentRequester parent) {
+            parent.mPendingCallbacks.put(mCallbackKey, callback);
+            mParentRef = new WeakReference<>(parent);
+        }
+
+        @Override
+        public void onHandleAssistData(Bundle data) {
+            if (data == null) {
+                return;
+            }
+
+            final AssistContent content = data.getParcelable(ASSIST_KEY_CONTENT);
+            if (content == null) {
+                Log.e(TAG, "Received AssistData, but no AssistContent found");
+                return;
+            }
+
+            AssistContentRequester requester = mParentRef.get();
+            if (requester != null) {
+                Callback callback = requester.mPendingCallbacks.get(mCallbackKey);
+                if (callback != null) {
+                    requester.executeOnMainExecutor(
+                            () -> callback.onAssistContentAvailable(content));
+                } else {
+                    Log.d(TAG, "Callback received after calling UI was disposed of");
+                }
+            } else {
+                Log.d(TAG, "Callback received after Requester was collected");
+            }
+        }
+
+        @Override
+        public void onHandleAssistScreenshot(Bitmap screenshot) {}
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 5450db9..ca8e101 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -49,6 +49,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
+import com.android.systemui.settings.UserTracker;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -79,6 +80,7 @@
     private final LongScreenshotData mLongScreenshotHolder;
     private final ActionIntentExecutor mActionExecutor;
     private final FeatureFlags mFeatureFlags;
+    private final UserTracker mUserTracker;
 
     private ImageView mPreview;
     private ImageView mTransitionView;
@@ -110,7 +112,7 @@
     public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter,
             @Main Executor mainExecutor, @Background Executor bgExecutor,
             LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags, UserTracker userTracker) {
         mUiEventLogger = uiEventLogger;
         mUiExecutor = mainExecutor;
         mBackgroundExecutor = bgExecutor;
@@ -118,6 +120,7 @@
         mLongScreenshotHolder = longScreenshotHolder;
         mActionExecutor = actionExecutor;
         mFeatureFlags = featureFlags;
+        mUserTracker = userTracker;
     }
 
 
@@ -363,7 +366,7 @@
 
     private void doShare(Uri uri) {
         if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
-            Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri, null);
+            Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri);
             mActionExecutor.launchIntentAsync(shareIntent, null,
                     mScreenshotUserHandle.getIdentifier(), false);
         } else {
@@ -375,7 +378,7 @@
             Intent sharingChooserIntent = Intent.createChooser(intent, null)
                     .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 
-            startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
+            startActivityAsUser(sharingChooserIntent, mUserTracker.getUserHandle());
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
new file mode 100644
index 0000000..ad66514
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -0,0 +1,145 @@
+package com.android.systemui.screenshot
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.os.UserHandle
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.view.ViewTreeObserver
+import android.view.animation.AccelerateDecelerateInterpolator
+import androidx.constraintlayout.widget.Guideline
+import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+/**
+ * MessageContainerController controls the display of content in the screenshot message container.
+ */
+class MessageContainerController
+@Inject
+constructor(
+    private val workProfileMessageController: WorkProfileMessageController,
+    private val screenshotDetectionController: ScreenshotDetectionController,
+    private val featureFlags: FeatureFlags,
+) {
+    private lateinit var container: ViewGroup
+    private lateinit var guideline: Guideline
+    private lateinit var workProfileFirstRunView: ViewGroup
+    private lateinit var detectionNoticeView: ViewGroup
+    private var animateOut: Animator? = null
+
+    fun setView(screenshotView: ViewGroup) {
+        container = screenshotView.requireViewById(R.id.screenshot_message_container)
+        guideline = screenshotView.requireViewById(R.id.guideline)
+
+        workProfileFirstRunView = container.requireViewById(R.id.work_profile_first_run)
+        detectionNoticeView = container.requireViewById(R.id.screenshot_detection_notice)
+
+        // Restore to starting state.
+        container.visibility = View.GONE
+        guideline.setGuidelineEnd(0)
+        workProfileFirstRunView.visibility = View.GONE
+        detectionNoticeView.visibility = View.GONE
+    }
+
+    // Minimal implementation for use when Flags.SCREENSHOT_METADATA isn't turned on.
+    fun onScreenshotTaken(userHandle: UserHandle) {
+        if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+            val workProfileData = workProfileMessageController.onScreenshotTaken(userHandle)
+            if (workProfileData != null) {
+                workProfileFirstRunView.visibility = View.VISIBLE
+                detectionNoticeView.visibility = View.GONE
+
+                workProfileMessageController.populateView(
+                    workProfileFirstRunView,
+                    workProfileData,
+                    this::animateOutMessageContainer
+                )
+                animateInMessageContainer()
+            }
+        }
+    }
+
+    fun onScreenshotTaken(screenshot: ScreenshotData) {
+        if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+            val workProfileData =
+                workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
+            var notifiedApps: List<CharSequence> = listOf()
+            if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
+                notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
+            }
+
+            // If work profile first run needs to show, bias towards that, otherwise show screenshot
+            // detection notification if needed.
+            if (workProfileData != null) {
+                workProfileFirstRunView.visibility = View.VISIBLE
+                detectionNoticeView.visibility = View.GONE
+                workProfileMessageController.populateView(
+                    workProfileFirstRunView,
+                    workProfileData,
+                    this::animateOutMessageContainer
+                )
+                animateInMessageContainer()
+            } else if (notifiedApps.isNotEmpty()) {
+                detectionNoticeView.visibility = View.VISIBLE
+                workProfileFirstRunView.visibility = View.GONE
+                screenshotDetectionController.populateView(detectionNoticeView, notifiedApps)
+                animateInMessageContainer()
+            }
+        }
+    }
+
+    private fun animateInMessageContainer() {
+        if (container.visibility == View.VISIBLE) return
+
+        // Need the container to be fully measured before animating in (to know animation offset
+        // destination)
+        container.visibility = View.VISIBLE
+        container.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    container.viewTreeObserver.removeOnPreDrawListener(this)
+                    getAnimator(true).start()
+                    return false
+                }
+            }
+        )
+    }
+
+    private fun animateOutMessageContainer() {
+        if (animateOut != null) return
+
+        animateOut =
+            getAnimator(false).apply {
+                addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            super.onAnimationEnd(animation)
+                            container.visibility = View.GONE
+                            animateOut = null
+                        }
+                    }
+                )
+                start()
+            }
+    }
+
+    private fun getAnimator(animateIn: Boolean): Animator {
+        val params = container.layoutParams as MarginLayoutParams
+        val offset = container.height + params.topMargin + params.bottomMargin
+        val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f)
+        with(anim) {
+            duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS
+            interpolator = AccelerateDecelerateInterpolator()
+            addUpdateListener { valueAnimator: ValueAnimator ->
+                val interpolation = valueAnimator.animatedValue as Float
+                guideline.setGuidelineEnd((interpolation * offset).toInt())
+                container.alpha = interpolation
+            }
+        }
+        return anim
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 95cc0dc..4db48ac 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -19,33 +19,33 @@
 import android.graphics.Insets
 import android.util.Log
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
-import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
+import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
-import java.util.function.Consumer
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import java.util.function.Consumer
+import javax.inject.Inject
 
 /**
  * Processes a screenshot request sent from {@link ScreenshotHelper}.
  */
 @SysUISingleton
 class RequestProcessor @Inject constructor(
-    private val capture: ImageCapture,
-    private val policy: ScreenshotPolicy,
-    private val flags: FeatureFlags,
-    /** For the Java Async version, to invoke the callback. */
-    @Application private val mainScope: CoroutineScope
+        private val capture: ImageCapture,
+        private val policy: ScreenshotPolicy,
+        private val flags: FeatureFlags,
+        /** For the Java Async version, to invoke the callback. */
+        @Application private val mainScope: CoroutineScope
 ) {
     /**
      * Inspects the incoming request, returning a potentially modified request depending on policy.
      *
      * @param request the request to process
      */
+    // TODO: Delete once SCREENSHOT_METADATA flag is launched
     suspend fun process(request: ScreenshotRequest): ScreenshotRequest {
         var result = request
 
@@ -58,7 +58,7 @@
         // regardless of the managed profile status.
 
         if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
-            flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+                flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
         ) {
 
             val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
@@ -66,17 +66,21 @@
 
             result = if (policy.isManagedProfile(info.user.identifier)) {
                 val image = capture.captureTask(info.taskId)
-                    ?: error("Task snapshot returned a null Bitmap!")
+                        ?: error("Task snapshot returned a null Bitmap!")
 
                 // Provide the task snapshot as the screenshot
-                ScreenshotRequest(
-                    TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source,
-                    HardwareBitmapBundler.hardwareBitmapToBundle(image),
-                    info.bounds, Insets.NONE, info.taskId, info.user.identifier, info.component
-                )
+                ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source)
+                        .setTopComponent(info.component)
+                        .setTaskId(info.taskId)
+                        .setUserId(info.user.identifier)
+                        .setBitmap(image)
+                        .setBoundsOnScreen(info.bounds)
+                        .setInsets(Insets.NONE)
+                        .build()
             } else {
                 // Create a new request of the same type which includes the top component
-                ScreenshotRequest(request.type, request.source, info.component)
+                ScreenshotRequest.Builder(request.type, request.source)
+                        .setTopComponent(info.component).build()
             }
         }
 
@@ -90,12 +94,67 @@
      * @param request the request to process
      * @param callback the callback to provide the processed request, invoked from the main thread
      */
+    // TODO: Delete once SCREENSHOT_METADATA flag is launched
     fun processAsync(request: ScreenshotRequest, callback: Consumer<ScreenshotRequest>) {
         mainScope.launch {
             val result = process(request)
             callback.accept(result)
         }
     }
+
+    /**
+     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
+     *
+     * @param screenshot the screenshot to process
+     */
+    suspend fun process(screenshot: ScreenshotData): ScreenshotData {
+        var result = screenshot
+
+        // Apply work profile screenshots policy:
+        //
+        // If the focused app belongs to a work profile, transforms a full screen
+        // (or partial) screenshot request to a task snapshot (provided image) screenshot.
+
+        // Whenever displayContentInfo is fetched, the topComponent is also populated
+        // regardless of the managed profile status.
+
+        if (screenshot.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
+            flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+        ) {
+            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
+            Log.d(TAG, "findPrimaryContent: $info")
+            result.taskId = info.taskId
+            result.topComponent = info.component
+            result.userHandle = info.user
+
+            if (policy.isManagedProfile(info.user.identifier)) {
+                val image = capture.captureTask(info.taskId)
+                    ?: error("Task snapshot returned a null Bitmap!")
+
+                // Provide the task snapshot as the screenshot
+                result.type = TAKE_SCREENSHOT_PROVIDED_IMAGE
+                result.bitmap = image
+                result.screenBounds = info.bounds
+            }
+        }
+
+        return result
+    }
+
+    /**
+     * Note: This is for compatibility with existing Java. Prefer the suspending function when
+     * calling from a Coroutine context.
+     *
+     * @param screenshot the screenshot to process
+     * @param callback the callback to provide the processed screenshot, invoked from the main
+     *                 thread
+     */
+    fun processAsync(screenshot: ScreenshotData, callback: Consumer<ScreenshotData>) {
+        mainScope.launch {
+            val result = process(screenshot)
+            callback.accept(result)
+        }
+    }
 }
 
 private const val TAG = "RequestProcessor"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index b4934cf..bf5fbd2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -20,8 +20,7 @@
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE;
 import static com.android.systemui.screenshot.LogConfig.logTag;
-import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION;
-import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS;
+import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;
 
 import android.app.ActivityTaskManager;
 import android.app.Notification;
@@ -155,7 +154,8 @@
 
             CompletableFuture<List<Notification.Action>> smartActionsFuture =
                     mScreenshotSmartActions.getSmartActionsFuture(
-                            mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS,
+                            mScreenshotId, uri, image, mSmartActionsProvider,
+                            ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
                             smartActionsEnabled, user);
             List<Notification.Action> smartActions = new ArrayList<>();
             if (smartActionsEnabled) {
@@ -166,7 +166,8 @@
                 smartActions.addAll(buildSmartActions(
                         mScreenshotSmartActions.getSmartActions(
                                 mScreenshotId, smartActionsFuture, timeoutMs,
-                                mSmartActionsProvider, REGULAR_SMART_ACTIONS),
+                                mSmartActionsProvider,
+                                ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
                         mContext));
             }
 
@@ -476,7 +477,7 @@
         CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
                 mScreenshotSmartActions.getSmartActionsFuture(
                         mScreenshotId, null, image, mSmartActionsProvider,
-                        QUICK_SHARE_ACTION,
+                        ScreenshotSmartActionType.QUICK_SHARE_ACTION,
                         true /* smartActionsEnabled */, user);
         int timeoutMs = DeviceConfig.getInt(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -485,7 +486,8 @@
         List<Notification.Action> quickShareActions =
                 mScreenshotSmartActions.getSmartActions(
                         mScreenshotId, quickShareActionsFuture, timeoutMs,
-                        mSmartActionsProvider, QUICK_SHARE_ACTION);
+                        mSmartActionsProvider,
+                        ScreenshotSmartActionType.QUICK_SHARE_ACTION);
         if (!quickShareActions.isEmpty()) {
             mQuickShareData.quickShareAction = quickShareActions.get(0);
             mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 5716a1d72..8721d71 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -17,7 +17,6 @@
 package com.android.systemui.screenshot;
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
 import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
@@ -44,6 +43,7 @@
 import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks;
 import android.app.ICompatCameraControlCallback;
 import android.app.Notification;
+import android.app.assist.AssistContent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -99,8 +99,10 @@
 import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.util.Assert;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -272,6 +274,7 @@
     private final ScrollCaptureClient mScrollCaptureClient;
     private final PhoneWindow mWindow;
     private final DisplayManager mDisplayManager;
+    private final DisplayTracker mDisplayTracker;
     private final ScrollCaptureController mScrollCaptureController;
     private final LongScreenshotData mLongScreenshotHolder;
     private final boolean mIsLowRamDevice;
@@ -280,6 +283,7 @@
     private final TimeoutHandler mScreenshotHandler;
     private final ActionIntentExecutor mActionExecutor;
     private final UserManager mUserManager;
+    private final AssistContentRequester mAssistContentRequester;
 
     private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
         if (DEBUG_INPUT) {
@@ -289,6 +293,7 @@
     };
 
     private ScreenshotView mScreenshotView;
+    private final MessageContainerController mMessageContainerController;
     private Bitmap mScreenBitmap;
     private SaveImageInBackgroundTask mSaveInBgTask;
     private boolean mScreenshotTakenInPortrait;
@@ -326,7 +331,10 @@
             BroadcastSender broadcastSender,
             ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
             ActionIntentExecutor actionExecutor,
-            UserManager userManager
+            UserManager userManager,
+            AssistContentRequester assistContentRequester,
+            MessageContainerController messageContainerController,
+            DisplayTracker displayTracker
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
         mNotificationsController = screenshotNotificationsController;
@@ -352,12 +360,15 @@
         });
 
         mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+        mDisplayTracker = displayTracker;
         final Context displayContext = context.createDisplayContext(getDefaultDisplay());
         mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mFlags = flags;
         mActionExecutor = actionExecutor;
         mUserManager = userManager;
+        mMessageContainerController = messageContainerController;
+        mAssistContentRequester = assistContentRequester;
 
         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
 
@@ -387,16 +398,147 @@
                 ClipboardOverlayController.SELF_PERMISSION, null, Context.RECEIVER_NOT_EXPORTED);
     }
 
+    void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
+            RequestCallback requestCallback) {
+        Assert.isMainThread();
+        mCurrentRequestCallback = requestCallback;
+        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
+            Rect bounds = getFullScreenRect();
+            screenshot.setBitmap(
+                    mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(), bounds));
+            screenshot.setScreenBounds(bounds);
+        }
+
+        if (screenshot.getBitmap() == null) {
+            Log.e(TAG, "handleScreenshot: Screenshot bitmap was null");
+            mNotificationsController.notifyScreenshotError(
+                    R.string.screenshot_failed_to_capture_text);
+            if (mCurrentRequestCallback != null) {
+                mCurrentRequestCallback.reportError();
+            }
+            return;
+        }
+
+        if (!isUserSetupComplete(Process.myUserHandle())) {
+            Log.w(TAG, "User setup not complete, displaying toast only");
+            // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
+            // and sharing shouldn't be exposed to the user.
+            saveScreenshotAndToast(screenshot.getUserHandle(), finisher);
+            return;
+        }
+
+        mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
+                ClipboardOverlayController.SELF_PERMISSION);
+
+        mScreenshotTakenInPortrait =
+                mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
+
+        String oldPackageName = mPackageName;
+        mPackageName = screenshot.getPackageNameString();
+
+        mScreenBitmap = screenshot.getBitmap();
+        // Optimizations
+        mScreenBitmap.setHasAlpha(false);
+        mScreenBitmap.prepareToDraw();
+
+        prepareViewForNewScreenshot(screenshot, oldPackageName);
+
+        saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
+                this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+
+        // The window is focusable by default
+        setWindowFocusable(true);
+        mScreenshotView.requestFocus();
+
+        enqueueScrollCaptureRequest(screenshot.getUserHandle());
+
+        attachWindow();
+
+        boolean showFlash = true;
+        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+            if (screenshot.getScreenBounds() != null
+                    && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(),
+                            screenshot.getScreenBounds())) {
+                showFlash = false;
+            } else {
+                showFlash = true;
+                screenshot.setInsets(Insets.NONE);
+                screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(),
+                        screenshot.getBitmap().getHeight()));
+            }
+        }
+
+        prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> {
+            if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+                mMessageContainerController.onScreenshotTaken(screenshot);
+            }
+        });
+
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
+                    mContext.getDrawable(R.drawable.overlay_badge_background),
+                    screenshot.getUserHandle()));
+        }
+        mScreenshotView.setScreenshot(screenshot);
+
+        if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && screenshot.getTaskId() >= 0) {
+            mAssistContentRequester.requestAssistContent(screenshot.getTaskId(),
+                    new AssistContentRequester.Callback() {
+                        @Override
+                        public void onAssistContentAvailable(AssistContent assistContent) {
+                            screenshot.setContextUrl(assistContent.getWebUri());
+                        }
+                    });
+        }
+
+        if (DEBUG_WINDOW) {
+            Log.d(TAG, "setContentView: " + mScreenshotView);
+        }
+        setContentView(mScreenshotView);
+        // ignore system bar insets for the purpose of window layout
+        mWindow.getDecorView().setOnApplyWindowInsetsListener(
+                (v, insets) -> WindowInsets.CONSUMED);
+        mScreenshotHandler.cancelTimeout(); // restarted after animation
+    }
+
+    void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) {
+        withWindowAttached(() -> {
+            if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+                    && mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
+                mScreenshotView.announceForAccessibility(mContext.getResources().getString(
+                        R.string.screenshot_saving_work_profile_title));
+            } else {
+                mScreenshotView.announceForAccessibility(
+                        mContext.getResources().getString(R.string.screenshot_saving_title));
+            }
+        });
+
+        mScreenshotView.reset();
+
+        if (mScreenshotView.isAttachedToWindow()) {
+            // if we didn't already dismiss for another reason
+            if (!mScreenshotView.isDismissing()) {
+                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0,
+                        oldPackageName);
+            }
+            if (DEBUG_WINDOW) {
+                Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
+                        + "(dismissing=" + mScreenshotView.isDismissing() + ")");
+            }
+        }
+
+        mScreenshotView.setPackageName(mPackageName);
+
+        mScreenshotView.updateOrientation(
+                mWindowManager.getCurrentWindowMetrics().getWindowInsets());
+    }
+
     @MainThread
     void takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher,
             RequestCallback requestCallback) {
         Assert.isMainThread();
         mCurrentRequestCallback = requestCallback;
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        getDefaultDisplay().getRealMetrics(displayMetrics);
-        takeScreenshotInternal(
-                topComponent, finisher,
-                new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
+        takeScreenshotInternal(topComponent, finisher, getFullScreenRect());
     }
 
     @MainThread
@@ -413,10 +555,11 @@
         }
 
         boolean showFlash = false;
-        if (!aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
+        if (screenshotScreenBounds == null
+                || !aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
             showFlash = true;
             visibleInsets = Insets.NONE;
-            screenshotScreenBounds.set(0, 0, screenshot.getWidth(), screenshot.getHeight());
+            screenshotScreenBounds = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight());
         }
         mCurrentRequestCallback = requestCallback;
         saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
@@ -493,6 +636,9 @@
         // Inflate the screenshot layout
         mScreenshotView = (ScreenshotView)
                 LayoutInflater.from(mContext).inflate(R.layout.screenshot, null);
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mMessageContainerController.setView(mScreenshotView);
+        }
         mScreenshotView.addOnAttachStateChangeListener(
                 new View.OnAttachStateChangeListener() {
                     @Override
@@ -533,6 +679,7 @@
                 setWindowFocusable(false);
             }
         }, mActionExecutor, mFlags);
+        mScreenshotView.setDefaultDisplay(mDisplayTracker.getDefaultDisplayId());
         mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
 
         mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
@@ -550,6 +697,10 @@
             Log.d(TAG, "adding OnComputeInternalInsetsListener");
         }
         mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView);
+        if (DEBUG_WINDOW) {
+            Log.d(TAG, "setContentView: " + mScreenshotView);
+        }
+        setContentView(mScreenshotView);
     }
 
     /**
@@ -562,7 +713,8 @@
 
         // copy the input Rect, since SurfaceControl.screenshot can mutate it
         Rect screenRect = new Rect(crop);
-        Bitmap screenshot = mImageCapture.captureDisplay(DEFAULT_DISPLAY, crop);
+        Bitmap screenshot = mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(),
+                crop);
 
         if (screenshot == null) {
             Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null");
@@ -631,7 +783,49 @@
 
         // The window is focusable by default
         setWindowFocusable(true);
+        mScreenshotView.requestFocus();
 
+        enqueueScrollCaptureRequest(owner);
+
+        attachWindow();
+        prepareAnimation(screenRect, showFlash, () -> {
+            if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+                mMessageContainerController.onScreenshotTaken(owner);
+            }
+        });
+
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
+                    mContext.getDrawable(R.drawable.overlay_badge_background), owner));
+        }
+        mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
+        if (DEBUG_WINDOW) {
+            Log.d(TAG, "setContentView: " + mScreenshotView);
+        }
+        setContentView(mScreenshotView);
+        // ignore system bar insets for the purpose of window layout
+        mWindow.getDecorView().setOnApplyWindowInsetsListener(
+                (v, insets) -> WindowInsets.CONSUMED);
+        mScreenshotHandler.cancelTimeout(); // restarted after animation
+    }
+
+    private void prepareAnimation(Rect screenRect, boolean showFlash,
+            Runnable onAnimationComplete) {
+        mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
+                new ViewTreeObserver.OnPreDrawListener() {
+                    @Override
+                    public boolean onPreDraw() {
+                        if (DEBUG_WINDOW) {
+                            Log.d(TAG, "onPreDraw: startAnimation");
+                        }
+                        mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
+                        startAnimation(screenRect, showFlash, onAnimationComplete);
+                        return true;
+                    }
+                });
+    }
+
+    private void enqueueScrollCaptureRequest(UserHandle owner) {
         // Wait until this window is attached to request because it is
         // the reference used to locate the target window (below).
         withWindowAttached(() -> {
@@ -669,34 +863,6 @@
                         }
                     });
         });
-
-        attachWindow();
-        mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
-                new ViewTreeObserver.OnPreDrawListener() {
-                    @Override
-                    public boolean onPreDraw() {
-                        if (DEBUG_WINDOW) {
-                            Log.d(TAG, "onPreDraw: startAnimation");
-                        }
-                        mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
-                        startAnimation(screenRect, showFlash);
-                        return true;
-                    }
-                });
-
-        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
-            mScreenshotView.badgeScreenshot(
-                    mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
-        }
-        mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
-        if (DEBUG_WINDOW) {
-            Log.d(TAG, "setContentView: " + mScreenshotView);
-        }
-        setContentView(mScreenshotView);
-        // ignore system bar insets for the purpose of window layout
-        mWindow.getDecorView().setOnApplyWindowInsetsListener(
-                (v, insets) -> WindowInsets.CONSUMED);
-        mScreenshotHandler.cancelTimeout(); // restarted after animation
     }
 
     private void requestScrollCapture(UserHandle owner) {
@@ -709,7 +875,7 @@
             mLastScrollCaptureRequest.cancel(true);
         }
         final ListenableFuture<ScrollCaptureResponse> future =
-                mScrollCaptureClient.request(DEFAULT_DISPLAY);
+                mScrollCaptureClient.request(mDisplayTracker.getDefaultDisplayId());
         mLastScrollCaptureRequest = future;
         mLastScrollCaptureRequest.addListener(() ->
                 onScrollCaptureResponseReady(future, owner), mMainExecutor);
@@ -740,7 +906,8 @@
             mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
                 DisplayMetrics displayMetrics = new DisplayMetrics();
                 getDefaultDisplay().getRealMetrics(displayMetrics);
-                Bitmap newScreenshot = mImageCapture.captureDisplay(DEFAULT_DISPLAY,
+                Bitmap newScreenshot = mImageCapture.captureDisplay(
+                        mDisplayTracker.getDefaultDisplayId(),
                         new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
 
                 mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
@@ -784,9 +951,9 @@
             mLongScreenshotHolder.setLongScreenshot(longScreenshot);
             mLongScreenshotHolder.setTransitionDestinationCallback(
                     (transitionDestination, onTransitionEnd) -> {
-                            mScreenshotView.startLongScreenshotTransition(
-                                    transitionDestination, onTransitionEnd,
-                                    longScreenshot);
+                        mScreenshotView.startLongScreenshotTransition(
+                                transitionDestination, onTransitionEnd,
+                                longScreenshot);
                         // TODO: Do this via ActionIntentExecutor instead.
                         mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
                     }
@@ -804,7 +971,8 @@
                     SCREENSHOT_REMOTE_RUNNER, 0, 0);
             try {
                 WindowManagerGlobal.getWindowManagerService()
-                        .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
+                        .overridePendingAppTransitionRemote(runner,
+                                mDisplayTracker.getDefaultDisplayId());
             } catch (Exception e) {
                 Log.e(TAG, "Error overriding screenshot app transition", e);
             }
@@ -932,13 +1100,22 @@
     /**
      * Starts the animation after taking the screenshot
      */
-    private void startAnimation(Rect screenRect, boolean showFlash) {
+    private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) {
         if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
             mScreenshotAnimation.cancel();
         }
 
         mScreenshotAnimation =
                 mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash);
+        if (onAnimationComplete != null) {
+            mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    super.onAnimationEnd(animation);
+                    onAnimationComplete.run();
+                }
+            });
+        }
 
         // Play the shutter sound to notify that we've taken a screenshot
         playCameraSound();
@@ -1037,11 +1214,6 @@
 
     private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
         mScreenshotView.setChipIntents(imageData);
-        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
-                && mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
-            // TODO: Read app from configuration
-            mScreenshotView.showWorkProfileMessage("Files");
-        }
     }
 
     /**
@@ -1144,13 +1316,19 @@
     }
 
     private Display getDefaultDisplay() {
-        return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
+        return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
     }
 
     private boolean allowLongScreenshots() {
         return !mIsLowRamDevice;
     }
 
+    private Rect getFullScreenRect() {
+        DisplayMetrics displayMetrics = new DisplayMetrics();
+        getDefaultDisplay().getRealMetrics(displayMetrics);
+        return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
+    }
+
     /** Does the aspect ratio of the bitmap with insets removed match the bounds. */
     private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets,
             Rect screenBounds) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
new file mode 100644
index 0000000..e9be88a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -0,0 +1,52 @@
+package com.android.systemui.screenshot
+
+import android.content.ComponentName
+import android.graphics.Bitmap
+import android.graphics.Insets
+import android.graphics.Rect
+import android.net.Uri
+import android.os.UserHandle
+import android.view.WindowManager.ScreenshotSource
+import android.view.WindowManager.ScreenshotType
+import androidx.annotation.VisibleForTesting
+import com.android.internal.util.ScreenshotRequest
+
+/** ScreenshotData represents the current state of a single screenshot being acquired. */
+data class ScreenshotData(
+    @ScreenshotType var type: Int,
+    @ScreenshotSource var source: Int,
+    /** UserHandle for the owner of the app being screenshotted, if known. */
+    var userHandle: UserHandle?,
+    /** ComponentName of the top-most app in the screenshot. */
+    var topComponent: ComponentName?,
+    var screenBounds: Rect?,
+    var taskId: Int,
+    var insets: Insets,
+    var bitmap: Bitmap?,
+    /** App-provided URL representing the content the user was looking at in the screenshot. */
+    var contextUrl: Uri? = null,
+) {
+    val packageNameString: String
+        get() = if (topComponent == null) "" else topComponent!!.packageName
+
+    companion object {
+        @JvmStatic
+        fun fromRequest(request: ScreenshotRequest): ScreenshotData {
+            return ScreenshotData(
+                request.type,
+                request.source,
+                if (request.userId >= 0) UserHandle.of(request.userId) else null,
+                request.topComponent,
+                request.boundsInScreen,
+                request.taskId,
+                request.insets,
+                request.bitmap,
+            )
+        }
+
+        @VisibleForTesting
+        fun forTesting(): ScreenshotData {
+            return ScreenshotData(0, 0, null, null, null, 0, Insets.NONE, null)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
new file mode 100644
index 0000000..70ea2b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.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.screenshot
+
+import android.content.pm.PackageManager
+import android.view.IWindowManager
+import android.view.ViewGroup
+import android.widget.TextView
+import com.android.systemui.R
+import javax.inject.Inject
+
+class ScreenshotDetectionController
+@Inject
+constructor(
+    private val windowManager: IWindowManager,
+    private val packageManager: PackageManager,
+) {
+    /**
+     * Notify potentially listening apps of the screenshot. Return a list of the names of the apps
+     * notified.
+     */
+    fun maybeNotifyOfScreenshot(data: ScreenshotData): List<CharSequence> {
+        // TODO: actually ask the window manager once API is available.
+        return listOf()
+    }
+
+    fun populateView(view: ViewGroup, appNames: List<CharSequence>) {
+        assert(appNames.isNotEmpty())
+
+        val textView: TextView = view.requireViewById(R.id.screenshot_detection_notice_text)
+        if (appNames.size == 1) {
+            textView.text =
+                view.resources.getString(R.string.screenshot_detected_template, appNames[0])
+        } else {
+            textView.text =
+                view.resources.getString(
+                    R.string.screenshot_detected_multiple_template,
+                    appNames[0]
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
index c891686..fc94aed 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
@@ -46,6 +46,8 @@
     SCREENSHOT_SAVED(306),
     @UiEvent(doc = "screenshot failed to save")
     SCREENSHOT_NOT_SAVED(336),
+    @UiEvent(doc = "failed to capture screenshot")
+    SCREENSHOT_CAPTURE_FAILED(1281),
     @UiEvent(doc = "screenshot preview tapped")
     SCREENSHOT_PREVIEW_TAPPED(307),
     @UiEvent(doc = "screenshot edit button tapped")
@@ -91,7 +93,13 @@
     @UiEvent(doc = "User has discarded the result of a long screenshot")
     SCREENSHOT_LONG_SCREENSHOT_EXIT(911),
     @UiEvent(doc = "A screenshot has been taken and saved to work profile")
-    SCREENSHOT_SAVED_TO_WORK_PROFILE(1240);
+    SCREENSHOT_SAVED_TO_WORK_PROFILE(1240),
+    @UiEvent(doc = "Notes application triggered the screenshot for notes")
+    SCREENSHOT_FOR_NOTE_TRIGGERED(1308),
+    @UiEvent(doc = "User accepted the screenshot to be sent to the notes app")
+    SCREENSHOT_FOR_NOTE_ACCEPTED(1309),
+    @UiEvent(doc = "User cancelled the screenshot for notes app flow")
+    SCREENSHOT_FOR_NOTE_CANCELLED(1310);
 
     private final int mId;
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index 3a35286..21a7310 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -32,12 +32,12 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.util.Log
-import android.view.Display.DEFAULT_DISPLAY
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.infra.ServiceConnector
 import com.android.systemui.SystemUIService
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
 import java.util.Arrays
 import javax.inject.Inject
@@ -52,6 +52,7 @@
     private val userMgr: UserManager,
     private val atmService: IActivityTaskManager,
     @Background val bgDispatcher: CoroutineDispatcher,
+    private val displayTracker: DisplayTracker
 ) : ScreenshotPolicy {
 
     private val proxyConnector: ServiceConnector<IScreenshotProxy> =
@@ -64,7 +65,7 @@
         )
 
     override fun getDefaultDisplayId(): Int {
-        return DEFAULT_DISPLAY
+        return displayTracker.defaultDisplayId
     }
 
     override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index e8ceb52..afba7ad 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -37,6 +37,7 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -80,7 +81,6 @@
 import android.widget.HorizontalScrollView;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
@@ -120,7 +120,7 @@
     private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
     private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
-    private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
+    public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
@@ -132,14 +132,13 @@
     private final AccessibilityManager mAccessibilityManager;
     private final GestureDetector mSwipeDetector;
 
+    private int mDefaultDisplay = Display.DEFAULT_DISPLAY;
     private int mNavMode;
     private boolean mOrientationPortrait;
     private boolean mDirectionLTR;
 
     private ImageView mScrollingScrim;
     private DraggableConstraintLayout mScreenshotStatic;
-    private ViewGroup mMessageContainer;
-    private TextView mMessageContent;
     private ImageView mScreenshotPreview;
     private ImageView mScreenshotBadge;
     private View mScreenshotPreviewBorder;
@@ -164,6 +163,8 @@
 
     private final ArrayList<OverlayActionChip> mSmartChips = new ArrayList<>();
     private PendingInteraction mPendingInteraction;
+    // Should only be set/used if the SCREENSHOT_METADATA flag is set.
+    private ScreenshotData mScreenshotData;
 
     private final InteractionJankMonitor mInteractionJankMonitor;
     private long mDefaultTimeoutOfTimeoutHandler;
@@ -289,8 +290,11 @@
         mDismissButton.getBoundsOnScreen(tmpRect);
         swipeRegion.op(tmpRect, Region.Op.UNION);
 
-        mMessageContainer.findViewById(R.id.message_dismiss_button).getBoundsOnScreen(tmpRect);
-        swipeRegion.op(tmpRect, Region.Op.UNION);
+        View messageDismiss = findViewById(R.id.message_dismiss_button);
+        if (messageDismiss != null) {
+            messageDismiss.getBoundsOnScreen(tmpRect);
+            swipeRegion.op(tmpRect, Region.Op.UNION);
+        }
 
         return swipeRegion;
     }
@@ -321,7 +325,7 @@
 
     private void startInputListening() {
         stopInputListening();
-        mInputMonitor = new InputMonitorCompat("Screenshot", Display.DEFAULT_DISPLAY);
+        mInputMonitor = new InputMonitorCompat("Screenshot", mDefaultDisplay);
         mInputEventReceiver = mInputMonitor.getInputReceiver(
                 Looper.getMainLooper(), Choreographer.getInstance(), ev -> {
                     if (ev instanceof MotionEvent) {
@@ -346,29 +350,11 @@
         }
     }
 
-    /**
-     * Show a notification under the screenshot view indicating that a work profile screenshot has
-     * been taken and which app can be used to view it.
-     *
-     * @param appName The name of the app to use to view screenshots
-     */
-    void showWorkProfileMessage(String appName) {
-        mMessageContent.setText(
-                mContext.getString(R.string.screenshot_work_profile_notification, appName));
-        mMessageContainer.setVisibility(VISIBLE);
-        mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> {
-            mMessageContainer.setVisibility(View.GONE);
-        });
-    }
-
     @Override // View
     protected void onFinishInflate() {
+        super.onFinishInflate();
         mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
         mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static));
-        mMessageContainer =
-                requireNonNull(mScreenshotStatic.findViewById(R.id.screenshot_message_container));
-        mMessageContent =
-                requireNonNull(mMessageContainer.findViewById(R.id.screenshot_message_content));
         mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview));
 
         mScreenshotPreviewBorder = requireNonNull(
@@ -458,10 +444,21 @@
         mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
     }
 
+    void setScreenshot(ScreenshotData screenshot) {
+        mScreenshotData = screenshot;
+        setScreenshot(screenshot.getBitmap(), screenshot.getInsets());
+        mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, screenshot.getBitmap(),
+                screenshot.getInsets()));
+    }
+
     void setPackageName(String packageName) {
         mPackageName = packageName;
     }
 
+    void setDefaultDisplay(int displayId) {
+        mDefaultDisplay = displayId;
+    }
+
     void updateInsets(WindowInsets insets) {
         int orientation = mContext.getResources().getConfiguration().orientation;
         mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
@@ -796,9 +793,17 @@
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
                 prepareSharedTransition();
-                mActionExecutor.launchIntentAsync(
-                        ActionIntentCreator.INSTANCE.createShareIntent(
-                                imageData.uri, imageData.subject),
+
+                Intent shareIntent;
+                if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && mScreenshotData != null
+                        && mScreenshotData.getContextUrl() != null) {
+                    shareIntent = ActionIntentCreator.INSTANCE.createShareIntentWithExtraText(
+                            imageData.uri, mScreenshotData.getContextUrl().toString());
+                } else {
+                    shareIntent = ActionIntentCreator.INSTANCE.createShareIntentWithSubject(
+                            imageData.uri, imageData.subject);
+                }
+                mActionExecutor.launchIntentAsync(shareIntent,
                         imageData.shareTransition.get().bundle,
                         imageData.owner.getIdentifier(), false);
             } else {
@@ -1078,7 +1083,7 @@
         mScreenshotBadge.setVisibility(View.GONE);
         mScreenshotBadge.setImageDrawable(null);
         mPendingSharedTransition = false;
-        mActionsContainerBackground.setVisibility(View.GONE);
+        mActionsContainerBackground.setVisibility(View.INVISIBLE);
         mActionsContainer.setVisibility(View.GONE);
         mDismissButton.setVisibility(View.GONE);
         mScrollingScrim.setVisibility(View.GONE);
@@ -1100,6 +1105,7 @@
         mQuickShareChip = null;
         setAlpha(1);
         mScreenshotStatic.setAlpha(1);
+        mScreenshotData = null;
     }
 
     private void startSharedTransition(ActionTransition transition) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 2176825..111278a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -21,12 +21,12 @@
 
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
-import static com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR;
 import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
 import static com.android.systemui.screenshot.LogConfig.logTag;
+import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED;
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
 
 import android.annotation.MainThread;
@@ -55,11 +55,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
-import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.FlagListenable.FlagEvent;
+import com.android.systemui.flags.Flags;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -122,7 +123,6 @@
         mContext = context;
         mBgExecutor = bgExecutor;
         mFeatureFlags = featureFlags;
-        mFeatureFlags.addListener(SCREENSHOT_REQUEST_PROCESSOR, FlagEvent::requestNoRestart);
         mFeatureFlags.addListener(SCREENSHOT_WORK_PROFILE_POLICY, FlagEvent::requestNoRestart);
         mProcessor = processor;
     }
@@ -188,8 +188,7 @@
         final Consumer<Uri> onSaved = (uri) -> reportUri(replyTo, uri);
         RequestCallback callback = new RequestCallbackImpl(replyTo);
 
-        ScreenshotHelper.ScreenshotRequest request =
-                (ScreenshotHelper.ScreenshotRequest) msg.obj;
+        ScreenshotRequest request = (ScreenshotRequest) msg.obj;
 
         handleRequest(request, onSaved, callback);
         return true;
@@ -197,13 +196,14 @@
 
     @MainThread
     @VisibleForTesting
-    void handleRequest(ScreenshotHelper.ScreenshotRequest request, Consumer<Uri> onSaved,
+    void handleRequest(ScreenshotRequest request, Consumer<Uri> onSaved,
             RequestCallback callback) {
         // If the storage for this user is locked, we have no place to store
         // the screenshot, so skip taking it instead of showing a misleading
         // animation and error notification.
         if (!mUserManager.isUserUnlocked()) {
             Log.w(TAG, "Skipping screenshot because storage is locked!");
+            logFailedRequest(request);
             mNotificationsController.notifyScreenshotError(
                     R.string.screenshot_failed_to_save_user_locked_text);
             callback.reportError();
@@ -214,6 +214,7 @@
             mBgExecutor.execute(() -> {
                 Log.w(TAG, "Skipping screenshot because an IT admin has disabled "
                         + "screenshots on the device");
+                logFailedRequest(request);
                 String blockedByAdminText = mDevicePolicyManager.getResources().getString(
                         SCREENSHOT_BLOCKED_BY_ADMIN,
                         () -> mContext.getString(R.string.screenshot_blocked_by_admin));
@@ -224,22 +225,46 @@
             return;
         }
 
-        if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) {
-            Log.d(TAG, "handleMessage: Using request processor");
-            mProcessor.processAsync(request,
-                    (r) -> dispatchToController(r, onSaved, callback));
-            return;
+        if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_METADATA_REFACTOR)) {
+            Log.d(TAG, "Processing screenshot data");
+            ScreenshotData screenshotData = ScreenshotData.fromRequest(request);
+            try {
+                mProcessor.processAsync(screenshotData,
+                        (data) -> dispatchToController(data, onSaved, callback));
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to process screenshot request!", e);
+                logFailedRequest(request);
+                mNotificationsController.notifyScreenshotError(
+                        R.string.screenshot_failed_to_capture_text);
+                callback.reportError();
+            }
+        } else {
+            try {
+                mProcessor.processAsync(request,
+                        (r) -> dispatchToController(r, onSaved, callback));
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to process screenshot request!", e);
+                logFailedRequest(request);
+                mNotificationsController.notifyScreenshotError(
+                        R.string.screenshot_failed_to_capture_text);
+                callback.reportError();
+            }
         }
-
-        dispatchToController(request, onSaved, callback);
     }
 
-    private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
+    private void dispatchToController(ScreenshotData screenshot,
             Consumer<Uri> uriConsumer, RequestCallback callback) {
+        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
+                screenshot.getPackageNameString());
+        mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
+    }
 
+    private void dispatchToController(ScreenshotRequest request,
+            Consumer<Uri> uriConsumer, RequestCallback callback) {
         ComponentName topComponent = request.getTopComponent();
-        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(request.getSource()), 0,
-                topComponent == null ? "" : topComponent.getPackageName());
+        String packageName = topComponent == null ? "" : topComponent.getPackageName();
+        mUiEventLogger.log(
+                ScreenshotEvent.getScreenshotSource(request.getSource()), 0, packageName);
 
         switch (request.getType()) {
             case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
@@ -252,28 +277,28 @@
                 if (DEBUG_SERVICE) {
                     Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_PROVIDED_IMAGE");
                 }
-                Bitmap screenshot = ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap(
-                        request.getBitmapBundle());
+                Bitmap screenshot = request.getBitmap();
                 Rect screenBounds = request.getBoundsInScreen();
                 Insets insets = request.getInsets();
                 int taskId = request.getTaskId();
                 int userId = request.getUserId();
 
-                if (screenshot == null) {
-                    Log.e(TAG, "Got null bitmap from screenshot message");
-                    mNotificationsController.notifyScreenshotError(
-                            R.string.screenshot_failed_to_capture_text);
-                    callback.reportError();
-                } else {
-                    mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
-                            taskId, userId, topComponent, uriConsumer, callback);
-                }
+                mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
+                        taskId, userId, topComponent, uriConsumer, callback);
                 break;
             default:
-                Log.w(TAG, "Invalid screenshot option: " + request.getType());
+                Log.wtf(TAG, "Invalid screenshot option: " + request.getType());
         }
     }
 
+    private void logFailedRequest(ScreenshotRequest request) {
+        ComponentName topComponent = request.getTopComponent();
+        String packageName = topComponent == null ? "" : topComponent.getPackageName();
+        mUiEventLogger.log(
+                ScreenshotEvent.getScreenshotSource(request.getSource()), 0, packageName);
+        mUiEventLogger.log(SCREENSHOT_CAPTURE_FAILED, 0, packageName);
+    }
+
     private static void sendComplete(Messenger target) {
         try {
             if (DEBUG_CALLBACK) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
new file mode 100644
index 0000000..1b728b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.systemui.R
+import javax.inject.Inject
+
+/**
+ * Handles work profile first run, determining whether a first run UI should be shown and populating
+ * that UI if needed.
+ */
+class WorkProfileMessageController
+@Inject
+constructor(
+    private val context: Context,
+    private val userManager: UserManager,
+    private val packageManager: PackageManager,
+) {
+
+    /**
+     * @return a populated WorkProfileFirstRunData object if a work profile first run message should
+     * be shown
+     */
+    fun onScreenshotTaken(userHandle: UserHandle?): WorkProfileFirstRunData? {
+        if (userHandle == null) return null
+
+        if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) {
+            var badgedIcon: Drawable? = null
+            var label: CharSequence? = null
+            val fileManager = fileManagerComponentName()
+            try {
+                val info =
+                    packageManager.getActivityInfo(
+                        fileManager,
+                        PackageManager.ComponentInfoFlags.of(0)
+                    )
+                val icon = packageManager.getActivityIcon(fileManager)
+                badgedIcon = packageManager.getUserBadgedIcon(icon, userHandle)
+                label = info.loadLabel(packageManager)
+            } catch (e: PackageManager.NameNotFoundException) {
+                Log.w(TAG, "Component $fileManager not found")
+            }
+
+            // If label wasn't loaded, use a default
+            return WorkProfileFirstRunData(label ?: defaultFileAppName(), badgedIcon)
+        }
+        return null
+    }
+
+    /**
+     * Use the provided WorkProfileFirstRunData to populate the work profile first run UI in the
+     * given view.
+     */
+    fun populateView(view: ViewGroup, data: WorkProfileFirstRunData, animateOut: () -> Unit) {
+        if (data.icon != null) {
+            // Replace the default icon if one is provided.
+            val imageView: ImageView = view.requireViewById<ImageView>(R.id.screenshot_message_icon)
+            imageView.setImageDrawable(data.icon)
+        }
+        val messageContent = view.requireViewById<TextView>(R.id.screenshot_message_content)
+        messageContent.text =
+            view.context.getString(R.string.screenshot_work_profile_notification, data.appName)
+        view.requireViewById<View>(R.id.message_dismiss_button).setOnClickListener {
+            animateOut()
+            onMessageDismissed()
+        }
+    }
+
+    private fun messageAlreadyDismissed(): Boolean {
+        return sharedPreference().getBoolean(PREFERENCE_KEY, false)
+    }
+
+    private fun onMessageDismissed() {
+        val editor = sharedPreference().edit()
+        editor.putBoolean(PREFERENCE_KEY, true)
+        editor.apply()
+    }
+
+    private fun sharedPreference() =
+        context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+
+    private fun fileManagerComponentName() =
+        ComponentName.unflattenFromString(
+            context.getString(R.string.config_sceenshotWorkProfileFilesApp)
+        )
+
+    private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name)
+
+    data class WorkProfileFirstRunData constructor(val appName: CharSequence, val icon: Drawable?)
+
+    companion object {
+        const val TAG = "WorkProfileMessageCtrl"
+        const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot"
+        const val PREFERENCE_KEY = "work_profile_first_run"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt
new file mode 100644
index 0000000..bb7f721
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.view.Display
+import java.util.concurrent.Executor
+
+/**
+ * Display tracker for SystemUI.
+ *
+ * This tracker provides async access to display information, as well as callbacks for display
+ * changes.
+ */
+interface DisplayTracker {
+
+    /** The id for the default display for the current SystemUI instance. */
+    val defaultDisplayId: Int
+
+    /** All displays that should be associated with the current SystemUI instance. */
+    val allDisplays: Array<Display>
+
+    /**
+     * Add a [Callback] to be notified of display changes, including additions, removals, and
+     * configuration changes, on a particular [Executor].
+     */
+    fun addDisplayChangeCallback(callback: Callback, executor: Executor)
+
+    /**
+     * Add a [Callback] to be notified of display brightness changes, on a particular [Executor].
+     * This callback will trigger Callback#onDisplayChanged for a display brightness change.
+     */
+    fun addBrightnessChangeCallback(callback: Callback, executor: Executor)
+
+    /** Remove a [Callback] previously added. */
+    fun removeCallback(callback: Callback)
+
+    /** Ćallback for notifying of changes. */
+    interface Callback {
+
+        /** Notifies that a display has been added. */
+        @JvmDefault fun onDisplayAdded(displayId: Int) {}
+
+        /** Notifies that a display has been removed. */
+        @JvmDefault fun onDisplayRemoved(displayId: Int) {}
+
+        /** Notifies a display has been changed */
+        @JvmDefault fun onDisplayChanged(displayId: Int) {}
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
new file mode 100644
index 0000000..5169f88
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS
+import android.os.Handler
+import android.view.Display
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.Assert
+import java.lang.ref.WeakReference
+import java.util.concurrent.Executor
+
+class DisplayTrackerImpl
+internal constructor(
+    val displayManager: DisplayManager,
+    @Background val backgroundHandler: Handler
+) : DisplayTracker {
+    override val defaultDisplayId: Int = Display.DEFAULT_DISPLAY
+    override val allDisplays: Array<Display>
+        get() = displayManager.displays
+
+    @GuardedBy("displayCallbacks")
+    private val displayCallbacks: MutableList<DisplayTrackerDataItem> = ArrayList()
+    @GuardedBy("brightnessCallbacks")
+    private val brightnessCallbacks: MutableList<DisplayTrackerDataItem> = ArrayList()
+
+    @VisibleForTesting
+    val displayChangedListener: DisplayManager.DisplayListener =
+        object : DisplayManager.DisplayListener {
+            override fun onDisplayAdded(displayId: Int) {
+                val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+                onDisplayAdded(displayId, list)
+            }
+
+            override fun onDisplayRemoved(displayId: Int) {
+                val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+                onDisplayRemoved(displayId, list)
+            }
+
+            override fun onDisplayChanged(displayId: Int) {
+                val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+                onDisplayChanged(displayId, list)
+            }
+        }
+
+    @VisibleForTesting
+    val displayBrightnessChangedListener: DisplayManager.DisplayListener =
+        object : DisplayManager.DisplayListener {
+            override fun onDisplayAdded(displayId: Int) {}
+
+            override fun onDisplayRemoved(displayId: Int) {}
+
+            override fun onDisplayChanged(displayId: Int) {
+                val list = synchronized(brightnessCallbacks) { brightnessCallbacks.toList() }
+                onDisplayChanged(displayId, list)
+            }
+        }
+
+    override fun addDisplayChangeCallback(callback: DisplayTracker.Callback, executor: Executor) {
+        synchronized(displayCallbacks) {
+            if (displayCallbacks.isEmpty()) {
+                displayManager.registerDisplayListener(displayChangedListener, backgroundHandler)
+            }
+            displayCallbacks.add(DisplayTrackerDataItem(WeakReference(callback), executor))
+        }
+    }
+
+    override fun addBrightnessChangeCallback(
+        callback: DisplayTracker.Callback,
+        executor: Executor
+    ) {
+        synchronized(brightnessCallbacks) {
+            if (brightnessCallbacks.isEmpty()) {
+                displayManager.registerDisplayListener(
+                    displayBrightnessChangedListener,
+                    backgroundHandler,
+                    EVENT_FLAG_DISPLAY_BRIGHTNESS
+                )
+            }
+            brightnessCallbacks.add(DisplayTrackerDataItem(WeakReference(callback), executor))
+        }
+    }
+
+    override fun removeCallback(callback: DisplayTracker.Callback) {
+        synchronized(displayCallbacks) {
+            val changed = displayCallbacks.removeIf { it.sameOrEmpty(callback) }
+            if (changed && displayCallbacks.isEmpty()) {
+                displayManager.unregisterDisplayListener(displayChangedListener)
+            }
+        }
+
+        synchronized(brightnessCallbacks) {
+            val changed = brightnessCallbacks.removeIf { it.sameOrEmpty(callback) }
+            if (changed && brightnessCallbacks.isEmpty()) {
+                displayManager.unregisterDisplayListener(displayBrightnessChangedListener)
+            }
+        }
+    }
+
+    @WorkerThread
+    private fun onDisplayAdded(displayId: Int, list: List<DisplayTrackerDataItem>) {
+        Assert.isNotMainThread()
+
+        notifySubscribers({ onDisplayAdded(displayId) }, list)
+    }
+
+    @WorkerThread
+    private fun onDisplayRemoved(displayId: Int, list: List<DisplayTrackerDataItem>) {
+        Assert.isNotMainThread()
+
+        notifySubscribers({ onDisplayRemoved(displayId) }, list)
+    }
+
+    @WorkerThread
+    private fun onDisplayChanged(displayId: Int, list: List<DisplayTrackerDataItem>) {
+        Assert.isNotMainThread()
+
+        notifySubscribers({ onDisplayChanged(displayId) }, list)
+    }
+
+    private inline fun notifySubscribers(
+        crossinline action: DisplayTracker.Callback.() -> Unit,
+        list: List<DisplayTrackerDataItem>
+    ) {
+        list.forEach {
+            if (it.callback.get() != null) {
+                it.executor.execute { it.callback.get()?.action() }
+            }
+        }
+    }
+
+    private data class DisplayTrackerDataItem(
+        val callback: WeakReference<DisplayTracker.Callback>,
+        val executor: Executor
+    ) {
+        fun sameOrEmpty(other: DisplayTracker.Callback): Boolean {
+            return callback.get()?.equals(other) ?: true
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
index bfba6df..bb637dc 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
@@ -32,74 +32,62 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.util.concurrency.DelayableExecutor
 import java.io.File
+import java.io.FilenameFilter
 import javax.inject.Inject
 
 /**
- * Implementation for retrieving file paths for file storage of system and secondary users. Files
- * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the
- * conventional {File Directory}
+ * Implementation for retrieving file paths for file storage of system and secondary users. For
+ * non-system users, files will be prepended by a special prefix containing the user id.
  */
 @SysUISingleton
 class UserFileManagerImpl
 @Inject
 constructor(
-    // Context of system process and system user.
     private val context: Context,
     val userManager: UserManager,
     val broadcastDispatcher: BroadcastDispatcher,
     @Background val backgroundExecutor: DelayableExecutor
 ) : UserFileManager, CoreStartable {
     companion object {
-        private const val FILES = "files"
+        private const val PREFIX = "__USER_"
+        private const val TAG = "UserFileManagerImpl"
+        const val ROOT_DIR = "UserFileManager"
+        const val FILES = "files"
         const val SHARED_PREFS = "shared_prefs"
-        @VisibleForTesting internal const val ID = "UserFileManager"
-
-        /** Returns `true` if the given user ID is that for the primary/system user. */
-        fun isPrimaryUser(userId: Int): Boolean {
-            return UserHandle(userId).isSystem
-        }
 
         /**
-         * Returns a [File] pointing to the correct path for a secondary user ID.
-         *
-         * Note that there is no check for the type of user. This should only be called for
-         * secondary users, never for the system user. For that, make sure to call [isPrimaryUser].
-         *
-         * Note also that there is no guarantee that the parent directory structure for the file
-         * exists on disk. For that, call [ensureParentDirExists].
-         *
-         * @param context The context
-         * @param fileName The name of the file
-         * @param directoryName The name of the directory that would contain the file
-         * @param userId The ID of the user to build a file path for
+         * Returns a File object with a relative path, built from the userId for non-system users
          */
-        fun secondaryUserFile(
-            context: Context,
-            fileName: String,
-            directoryName: String,
-            userId: Int,
-        ): File {
-            return Environment.buildPath(
-                context.filesDir,
-                ID,
-                userId.toString(),
-                directoryName,
-                fileName,
-            )
-        }
-
-        /**
-         * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs
-         * recursively.
-         */
-        fun ensureParentDirExists(file: File) {
-            val parent = file.parentFile
-            if (!parent.exists()) {
-                if (!parent.mkdirs()) {
-                    Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}")
-                }
+        fun createFile(fileName: String, userId: Int): File {
+            return if (isSystemUser(userId)) {
+                File(fileName)
+            } else {
+                File(getFilePrefix(userId) + fileName)
             }
         }
+
+        fun createLegacyFile(context: Context, dir: String, fileName: String, userId: Int): File? {
+            return if (isSystemUser(userId)) {
+                null
+            } else {
+                return Environment.buildPath(
+                    context.filesDir,
+                    ROOT_DIR,
+                    userId.toString(),
+                    dir,
+                    fileName
+                )
+            }
+        }
+
+        fun getFilePrefix(userId: Int): String {
+            return PREFIX + userId.toString() + "_"
+        }
+
+        /** Returns `true` if the given user ID is that for the system user. */
+        private fun isSystemUser(userId: Int): Boolean {
+            return UserHandle(userId).isSystem
+        }
     }
 
     private val broadcastReceiver =
@@ -119,64 +107,87 @@
         broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
     }
 
-    /** Return the file based on current user. */
+    /**
+     * Return the file based on current user. Files for all users will exist in [context.filesDir],
+     * but non system user files will be prepended with [getFilePrefix].
+     */
     override fun getFile(fileName: String, userId: Int): File {
-        return if (isPrimaryUser(userId)) {
-            Environment.buildPath(context.filesDir, fileName)
-        } else {
-            val secondaryFile =
-                secondaryUserFile(
-                    context = context,
-                    userId = userId,
-                    directoryName = FILES,
-                    fileName = fileName,
-                )
-            ensureParentDirExists(secondaryFile)
-            secondaryFile
-        }
+        val file = File(context.filesDir, createFile(fileName, userId).path)
+        createLegacyFile(context, FILES, fileName, userId)?.run { migrate(file, this) }
+        return file
     }
 
-    /** Get shared preferences from user. */
+    /**
+     * Get shared preferences from user. Files for all users will exist in the shared_prefs dir, but
+     * non system user files will be prepended with [getFilePrefix].
+     */
     override fun getSharedPreferences(
         fileName: String,
         @Context.PreferencesMode mode: Int,
         userId: Int
     ): SharedPreferences {
-        if (isPrimaryUser(userId)) {
-            return context.getSharedPreferences(fileName, mode)
+        val file = createFile(fileName, userId)
+        createLegacyFile(context, SHARED_PREFS, "$fileName.xml", userId)?.run {
+            val path = Environment.buildPath(context.dataDir, SHARED_PREFS, "${file.path}.xml")
+            migrate(path, this)
         }
-
-        val secondaryUserDir =
-            secondaryUserFile(
-                context = context,
-                fileName = fileName,
-                directoryName = SHARED_PREFS,
-                userId = userId,
-            )
-
-        ensureParentDirExists(secondaryUserDir)
-        return context.getSharedPreferences(secondaryUserDir, mode)
+        return context.getSharedPreferences(file.path, mode)
     }
 
-    /** Remove dirs for deleted users. */
+    /** Remove files for deleted users. */
     @VisibleForTesting
     internal fun clearDeletedUserData() {
         backgroundExecutor.execute {
-            val file = Environment.buildPath(context.filesDir, ID)
-            if (!file.exists()) return@execute
-            val aliveUsers = userManager.aliveUsers.map { it.id.toString() }
-            val dirsToDelete = file.list().filter { !aliveUsers.contains(it) }
+            deleteFiles(context.filesDir)
+            deleteFiles(File(context.dataDir, SHARED_PREFS))
+        }
+    }
 
-            dirsToDelete.forEach { dir ->
+    private fun migrate(dest: File, source: File) {
+        if (source.exists()) {
+            try {
+                val parent = source.getParentFile()
+                source.renameTo(dest)
+
+                deleteParentDirsIfEmpty(parent)
+            } catch (e: Exception) {
+                Log.e(TAG, "Failed to rename and delete ${source.path}", e)
+            }
+        }
+    }
+
+    private fun deleteParentDirsIfEmpty(dir: File?) {
+        if (dir != null && dir.listFiles().size == 0) {
+            val priorParent = dir.parentFile
+            val isRoot = dir.name == ROOT_DIR
+            dir.delete()
+
+            if (!isRoot) {
+                deleteParentDirsIfEmpty(priorParent)
+            }
+        }
+    }
+
+    private fun deleteFiles(parent: File) {
+        val aliveUserFilePrefix = userManager.aliveUsers.map { getFilePrefix(it.id) }
+        val filesToDelete =
+            parent.listFiles(
+                FilenameFilter { _, name ->
+                    name.startsWith(PREFIX) &&
+                        aliveUserFilePrefix.filter { name.startsWith(it) }.isEmpty()
+                }
+            )
+
+        // This can happen in test environments
+        if (filesToDelete == null) {
+            Log.i(TAG, "Empty directory: ${parent.path}")
+        } else {
+            filesToDelete.forEach { file ->
+                Log.i(TAG, "Deleting file: ${file.path}")
                 try {
-                    val dirToDelete =
-                        Environment.buildPath(
-                            file,
-                            dir,
-                        )
-                    dirToDelete.deleteRecursively()
+                    file.delete()
                 } catch (e: Exception) {
-                    Log.e(ID, "Deletion failed.", e)
+                    Log.e(TAG, "Deletion failed.", e)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index 1558ac5..33a3125 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.pm.UserInfo
 import android.os.UserHandle
+import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 
 /**
@@ -62,12 +63,35 @@
     fun removeCallback(callback: Callback)
 
     /**
-     * Ćallback for notifying of changes.
+     * Callback for notifying of changes.
      */
     interface Callback {
 
         /**
+         * Same as {@link onUserChanging(Int, Context, CountDownLatch)} but the latch will be
+         * auto-decremented after the completion of this method.
+         */
+        @JvmDefault
+        fun onUserChanging(newUser: Int, userContext: Context) {}
+
+        /**
+         * Notifies that the current user is being changed.
+         * Override this method to run things while the screen is frozen for the user switch.
+         * Please use {@link #onUserChanged} if the task doesn't need to push the unfreezing of the
+         * screen further. Please be aware that code executed in this callback will lengthen the
+         * user switch duration. When overriding this method, countDown() MUST be called on the
+         * latch once execution is complete.
+         */
+        @JvmDefault
+        fun onUserChanging(newUser: Int, userContext: Context, latch: CountDownLatch) {
+            onUserChanging(newUser, userContext)
+            latch.countDown()
+        }
+
+        /**
          * Notifies that the current user has changed.
+         * Override this method to run things after the screen is unfrozen for the user switch.
+         * Please see {@link #onUserChanging} if you need to hide jank.
          */
         @JvmDefault
         fun onUserChanged(newUser: Int, userContext: Context) {}
@@ -78,4 +102,4 @@
         @JvmDefault
         fun onProfilesChanged(profiles: List<@JvmSuppressWildcards UserInfo>) {}
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 61390c5..8674036 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.settings
 
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
 import android.content.BroadcastReceiver
 import android.content.ContentResolver
 import android.content.Context
@@ -23,6 +25,7 @@
 import android.content.IntentFilter
 import android.content.pm.UserInfo
 import android.os.Handler
+import android.os.IRemoteCallback
 import android.os.UserHandle
 import android.os.UserManager
 import android.util.Log
@@ -34,6 +37,7 @@
 import java.io.PrintWriter
 import java.lang.IllegalStateException
 import java.lang.ref.WeakReference
+import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
@@ -56,6 +60,7 @@
 class UserTrackerImpl internal constructor(
     private val context: Context,
     private val userManager: UserManager,
+    private val iActivityManager: IActivityManager,
     private val dumpManager: DumpManager,
     private val backgroundHandler: Handler
 ) : UserTracker, Dumpable, BroadcastReceiver() {
@@ -107,7 +112,6 @@
         setUserIdInternal(startingUser)
 
         val filter = IntentFilter().apply {
-            addAction(Intent.ACTION_USER_SWITCHED)
             addAction(Intent.ACTION_USER_INFO_CHANGED)
             // These get called when a managed profile goes in or out of quiet mode.
             addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
@@ -118,14 +122,13 @@
         }
         context.registerReceiverForAllUsers(this, filter, null /* permission */, backgroundHandler)
 
+        registerUserSwitchObserver()
+
         dumpManager.registerDumpable(TAG, this)
     }
 
     override fun onReceive(context: Context, intent: Intent) {
         when (intent.action) {
-            Intent.ACTION_USER_SWITCHED -> {
-                handleSwitchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL))
-            }
             Intent.ACTION_USER_INFO_CHANGED,
             Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
             Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
@@ -157,22 +160,56 @@
         return ctx to profiles
     }
 
+    private fun registerUserSwitchObserver() {
+        iActivityManager.registerUserSwitchObserver(object : UserSwitchObserver() {
+            override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
+                backgroundHandler.run {
+                    handleUserSwitching(newUserId)
+                    reply?.sendResult(null)
+                }
+            }
+
+            override fun onUserSwitchComplete(newUserId: Int) {
+                backgroundHandler.run {
+                    handleUserSwitchComplete(newUserId)
+                }
+            }
+        }, TAG)
+    }
+
     @WorkerThread
-    private fun handleSwitchUser(newUser: Int) {
+    private fun handleUserSwitching(newUserId: Int) {
         Assert.isNotMainThread()
-        if (newUser == UserHandle.USER_NULL) {
-            Log.w(TAG, "handleSwitchUser - Couldn't get new id from intent")
-            return
+        Log.i(TAG, "Switching to user $newUserId")
+
+        setUserIdInternal(newUserId)
+
+        val list = synchronized(callbacks) {
+            callbacks.toList()
         }
+        val latch = CountDownLatch(list.size)
+        list.forEach {
+            val callback = it.callback.get()
+            if (callback != null) {
+                it.executor.execute {
+                    callback.onUserChanging(userId, userContext, latch)
+                }
+            } else {
+                latch.countDown()
+            }
+        }
+        latch.await()
+    }
 
-        if (newUser == userId) return
-        Log.i(TAG, "Switching to user $newUser")
+    @WorkerThread
+    private fun handleUserSwitchComplete(newUserId: Int) {
+        Assert.isNotMainThread()
+        Log.i(TAG, "Switched to user $newUserId")
 
-        val (ctx, profiles) = setUserIdInternal(newUser)
-
+        setUserIdInternal(newUserId)
         notifySubscribers {
-            onUserChanged(newUser, ctx)
-            onProfilesChanged(profiles)
+            onUserChanged(newUserId, userContext)
+            onProfilesChanged(userProfiles)
         }
     }
 
@@ -205,6 +242,7 @@
         val list = synchronized(callbacks) {
             callbacks.toList()
         }
+
         list.forEach {
             if (it.callback.get() != null) {
                 it.executor.execute {
@@ -258,4 +296,4 @@
     fun sameOrEmpty(other: UserTracker.Callback): Boolean {
         return callback.get()?.equals(other) ?: true
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 5880003..8089d01 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -27,10 +27,10 @@
 import android.database.ContentObserver;
 import android.hardware.display.BrightnessInfo;
 import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManager.DisplayListener;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
@@ -49,6 +49,7 @@
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 
@@ -78,19 +79,14 @@
     private final ToggleSlider mControl;
     private final DisplayManager mDisplayManager;
     private final UserTracker mUserTracker;
+    private final DisplayTracker mDisplayTracker;
     private final IVrManager mVrManager;
 
     private final Executor mMainExecutor;
     private final Handler mBackgroundHandler;
     private final BrightnessObserver mBrightnessObserver;
 
-    private final DisplayListener mDisplayListener = new DisplayListener() {
-        @Override
-        public void onDisplayAdded(int displayId) {}
-
-        @Override
-        public void onDisplayRemoved(int displayId) {}
-
+    private final DisplayTracker.Callback mBrightnessListener = new DisplayTracker.Callback() {
         @Override
         public void onDisplayChanged(int displayId) {
             mBackgroundHandler.post(mUpdateSliderRunnable);
@@ -143,14 +139,14 @@
             cr.registerContentObserver(
                     BRIGHTNESS_FOR_VR_FLOAT_URI,
                     false, this, UserHandle.USER_ALL);
-            mDisplayManager.registerDisplayListener(mDisplayListener, mHandler,
-                    DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
+            mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener,
+                    new HandlerExecutor(mHandler));
         }
 
         public void stopObserving() {
             final ContentResolver cr = mContext.getContentResolver();
             cr.unregisterContentObserver(this);
-            mDisplayManager.unregisterDisplayListener(mDisplayListener);
+            mDisplayTracker.removeCallback(mBrightnessListener);
         }
 
     }
@@ -218,7 +214,7 @@
             automatic = Settings.System.getIntForUser(mContext.getContentResolver(),
                     Settings.System.SCREEN_BRIGHTNESS_MODE,
                     Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL,
-                    UserHandle.USER_CURRENT);
+                    mUserTracker.getUserId());
             mAutomatic = automatic != Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL;
         }
     };
@@ -292,6 +288,7 @@
             Context context,
             ToggleSlider control,
             UserTracker userTracker,
+            DisplayTracker displayTracker,
             @Main Executor mainExecutor,
             @Background Handler bgHandler) {
         mContext = context;
@@ -300,6 +297,7 @@
         mMainExecutor = mainExecutor;
         mBackgroundHandler = bgHandler;
         mUserTracker = userTracker;
+        mDisplayTracker = displayTracker;
         mBrightnessObserver = new BrightnessObserver(mHandler);
 
         mDisplayId = mContext.getDisplayId();
@@ -450,6 +448,7 @@
     public static class Factory {
         private final Context mContext;
         private final UserTracker mUserTracker;
+        private final DisplayTracker mDisplayTracker;
         private final Executor mMainExecutor;
         private final Handler mBackgroundHandler;
 
@@ -457,10 +456,12 @@
         public Factory(
                 Context context,
                 UserTracker userTracker,
+                DisplayTracker displayTracker,
                 @Main Executor mainExecutor,
                 @Background Handler bgHandler) {
             mContext = context;
             mUserTracker = userTracker;
+            mDisplayTracker = displayTracker;
             mMainExecutor = mainExecutor;
             mBackgroundHandler = bgHandler;
         }
@@ -471,6 +472,7 @@
                     mContext,
                     toggleSlider,
                     mUserTracker,
+                    mDisplayTracker,
                     mMainExecutor,
                     mBackgroundHandler);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index e208be9..8879501 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -36,6 +36,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 
 import java.util.List;
@@ -49,16 +50,19 @@
     private BrightnessController mBrightnessController;
     private final BrightnessSliderController.Factory mToggleSliderFactory;
     private final UserTracker mUserTracker;
+    private final DisplayTracker mDisplayTracker;
     private final Executor mMainExecutor;
     private final Handler mBackgroundHandler;
 
     @Inject
     public BrightnessDialog(
             UserTracker userTracker,
+            DisplayTracker displayTracker,
             BrightnessSliderController.Factory factory,
             @Main Executor mainExecutor,
             @Background Handler bgHandler) {
         mUserTracker = userTracker;
+        mDisplayTracker = displayTracker;
         mToggleSliderFactory = factory;
         mMainExecutor = mainExecutor;
         mBackgroundHandler = bgHandler;
@@ -106,7 +110,7 @@
         frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT);
 
         mBrightnessController = new BrightnessController(
-                this, controller, mUserTracker, mMainExecutor, mBackgroundHandler);
+                this, controller, mUserTracker, mDisplayTracker, mMainExecutor, mBackgroundHandler);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
index 2f62e44..e9a1dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
@@ -17,7 +17,9 @@
 package com.android.systemui.settings.dagger;
 
 import android.app.ActivityManager;
+import android.app.IActivityManager;
 import android.content.Context;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.UserManager;
 
@@ -25,6 +27,8 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.settings.DisplayTrackerImpl;
 import com.android.systemui.settings.UserContentResolverProvider;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserFileManager;
@@ -57,15 +61,26 @@
     static UserTracker provideUserTracker(
             Context context,
             UserManager userManager,
+            IActivityManager iActivityManager,
             DumpManager dumpManager,
             @Background Handler handler
     ) {
         int startingUser = ActivityManager.getCurrentUser();
-        UserTrackerImpl tracker = new UserTrackerImpl(context, userManager, dumpManager, handler);
+        UserTrackerImpl tracker = new UserTrackerImpl(context, userManager, iActivityManager,
+                dumpManager, handler);
         tracker.initialize(startingUser);
         return tracker;
     }
 
+    @SysUISingleton
+    @Provides
+    static DisplayTracker provideDisplayTracker(
+            DisplayManager displayManager,
+            @Background Handler handler
+    ) {
+        return new DisplayTrackerImpl(displayManager, handler);
+    }
+
     @Binds
     @IntoMap
     @ClassKey(UserFileManagerImpl.class)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt
index e360ec2..a78b0aa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt
@@ -31,7 +31,7 @@
 
 /**
  * Contains all changes that need to be performed to the different [ConstraintSet] in
- * [LargeScreenShadeHeaderController].
+ * [ShadeHeaderController].
  */
 data class ConstraintsChanges(
     val qqsConstraintsChanges: ConstraintChange? = null,
@@ -46,7 +46,7 @@
 }
 
 /**
- * Determines [ConstraintChanges] for [LargeScreenShadeHeaderController] based on configurations.
+ * Determines [ConstraintChanges] for [ShadeHeaderController] based on configurations.
  *
  * Given that the number of different scenarios is not that large, having specific methods instead
  * of a full map between state and [ConstraintSet] was preferred.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 5011227..b3d31f2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -69,7 +69,8 @@
         }
         return ConstraintsChanges(
             qqsConstraintsChanges = change,
-            qsConstraintsChanges = change
+            qsConstraintsChanges = change,
+            largeScreenConstraintsChanges = change,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
index ae303eb..fb2ddc1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
@@ -39,6 +39,7 @@
     private final NotificationPanelView mView;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     private final LockIconViewController mLockIconViewController;
+    private final QuickSettingsController mQsController;
     private final Set<Integer> mDebugTextUsedYPositions;
     private final Paint mDebugPaint;
 
@@ -46,12 +47,14 @@
             NotificationPanelViewController notificationPanelViewController,
             NotificationPanelView notificationPanelView,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
-            LockIconViewController lockIconViewController
+            LockIconViewController lockIconViewController,
+            QuickSettingsController quickSettingsController
     ) {
         mNotificationPanelViewController = notificationPanelViewController;
         mView = notificationPanelView;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mLockIconViewController = lockIconViewController;
+        mQsController = quickSettingsController;
         mDebugTextUsedYPositions = new HashSet<>();
         mDebugPaint = new Paint();
     }
@@ -71,12 +74,19 @@
                 Color.RED, "getMaxPanelHeight()");
         drawDebugInfo(canvas, (int) mNotificationPanelViewController.getExpandedHeight(),
                 Color.BLUE, "getExpandedHeight()");
-        drawDebugInfo(canvas, mNotificationPanelViewController.calculatePanelHeightQsExpanded(),
+        drawDebugInfo(canvas, mQsController.calculatePanelHeightExpanded(
+                        mNotificationPanelViewController.getClockPositionResult()
+                                .stackScrollerPadding),
                 Color.GREEN, "calculatePanelHeightQsExpanded()");
-        drawDebugInfo(canvas, mNotificationPanelViewController.calculatePanelHeightQsExpanded(),
+        drawDebugInfo(canvas, mQsController.calculatePanelHeightExpanded(
+                        mNotificationPanelViewController.getClockPositionResult()
+                                .stackScrollerPadding),
                 Color.YELLOW, "calculatePanelHeightShade()");
         drawDebugInfo(canvas,
-                (int) mNotificationPanelViewController.calculateNotificationsTopPadding(),
+                (int) mQsController.calculateNotificationsTopPadding(
+                        mNotificationPanelViewController.isExpanding(),
+                        mNotificationPanelViewController.getKeyguardNotificationStaticPadding(),
+                        mNotificationPanelViewController.getExpandedFraction()),
                 Color.MAGENTA, "calculateNotificationsTopPadding()");
         drawDebugInfo(canvas, mNotificationPanelViewController.getClockPositionResult().clockY,
                 Color.GRAY, "mClockPositionResult.clockY");
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
index 639172f..b445000 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
@@ -48,7 +48,10 @@
                 setOf(
                     ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade),
                     ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade),
-                    ViewIdToTranslate(R.id.rightLayout, END, filterShade),
+                    ViewIdToTranslate(R.id.statusIcons, END, filterShade),
+                    ViewIdToTranslate(R.id.privacy_container, END, filterShade),
+                    ViewIdToTranslate(R.id.batteryRemainingIcon, END, filterShade),
+                    ViewIdToTranslate(R.id.carrier_group, END, filterShade),
                     ViewIdToTranslate(R.id.clock, START, filterShade),
                     ViewIdToTranslate(R.id.date, START, filterShade)),
             progressProvider = progressProvider)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 8f512d0..e53eea9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -23,14 +23,12 @@
 import static androidx.constraintlayout.widget.ConstraintSet.END;
 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
 
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.GENERIC;
-import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -53,7 +51,6 @@
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.Fragment;
 import android.app.StatusBarManager;
 import android.content.ContentResolver;
 import android.content.res.Resources;
@@ -101,10 +98,8 @@
 import androidx.constraintlayout.widget.ConstraintSet;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.ActiveUnlockConfig;
@@ -135,15 +130,22 @@
 import com.android.systemui.dump.DumpsysTableLogger;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -169,7 +171,6 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.QsFrameTranslateController;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -200,7 +201,6 @@
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
@@ -239,6 +239,7 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import kotlin.Unit;
 import kotlinx.coroutines.CoroutineDispatcher;
 
 @CentralSurfacesComponent.CentralSurfacesScope
@@ -255,13 +256,13 @@
     private static final VibrationEffect ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
             VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false);
     /** The parallax amount of the quick settings translation when dragging down the panel. */
-    private static final float QS_PARALLAX_AMOUNT = 0.175f;
+    public static final float QS_PARALLAX_AMOUNT = 0.175f;
     /** Fling expanding QS. */
     public static final int FLING_EXPAND = 0;
     /** Fling collapsing QS, potentially stopping when QS becomes QQS. */
-    private static final int FLING_COLLAPSE = 1;
+    public static final int FLING_COLLAPSE = 1;
     /** Fling until QS is completely hidden. */
-    private static final int FLING_HIDE = 2;
+    public static final int FLING_HIDE = 2;
     /** The delay to reset the hint text when the hint animation is finished running. */
     private static final int HINT_RESET_DELAY_MS = 1200;
     private static final long ANIMATION_DELAY_ICON_FADE_IN =
@@ -283,7 +284,7 @@
     private static final int MAX_TIME_TO_OPEN_WHEN_FLINGING_FROM_LAUNCHER = 300;
     private static final int MAX_DOWN_EVENT_BUFFER_SIZE = 50;
     private static final String COUNTER_PANEL_OPEN = "panel_open";
-    private static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs";
+    public static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs";
     private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
     private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1);
     private static final Rect EMPTY_RECT = new Rect();
@@ -302,14 +303,10 @@
     private final SystemClock mSystemClock;
     private final ShadeLogger mShadeLog;
     private final DozeParameters mDozeParameters;
-    private final Runnable mCollapseExpandAction = this::collapseOrExpand;
-    private final NsslOverscrollTopChangedListener mOnOverscrollTopChangedListener =
-            new NsslOverscrollTopChangedListener();
     private final NotificationStackScrollLayout.OnEmptySpaceClickListener
             mOnEmptySpaceClickListener = (x, y) -> onEmptySpaceClick();
     private final ShadeHeadsUpChangedListener mOnHeadsUpChangedListener =
             new ShadeHeadsUpChangedListener();
-    private final QS.HeightListener mHeightListener = this::onQsHeightChanged;
     private final ConfigurationListener mConfigurationListener = new ConfigurationListener();
     private final SettingsChangeObserver mSettingsChangeObserver;
     private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener();
@@ -319,7 +316,6 @@
     private final ConfigurationController mConfigurationController;
     private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
-    private final InteractionJankMonitor mInteractionJankMonitor;
     private final LayoutInflater mLayoutInflater;
     private final FeatureFlags mFeatureFlags;
     private final PowerManager mPowerManager;
@@ -338,12 +334,9 @@
     private final KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
     private final FragmentService mFragmentService;
     private final ScrimController mScrimController;
-    private final NotificationRemoteInputManager mRemoteInputManager;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    private final ShadeTransitionController mShadeTransitionController;
     private final TapAgainViewController mTapAgainViewController;
-    private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
-    private final RecordingController mRecordingController;
+    private final ShadeHeaderController mShadeHeaderController;
     private final boolean mVibrateOnOpening;
     private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
     private final FlingAnimationUtils mFlingAnimationUtilsClosing;
@@ -355,11 +348,11 @@
     private final Interpolator mBounceInterpolator;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
-    private final QS.ScrollListener mQsScrollListener = this::onQsPanelScrollChanged;
     private final FalsingTapListener mFalsingTapListener = this::falsingAdditionalTapRequired;
-    private final FragmentListener mQsFragmentListener = new QsFragmentListener();
     private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate();
     private final NotificationGutsManager mGutsManager;
+    private final AlternateBouncerInteractor mAlternateBouncerInteractor;
+    private final QuickSettingsController mQsController;
 
     private long mDownTime;
     private boolean mTouchSlopExceededBeforeDown;
@@ -386,9 +379,6 @@
     private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
     private KeyguardStatusBarView mKeyguardStatusBar;
     private KeyguardStatusBarViewController mKeyguardStatusBarViewController;
-    private QS mQs;
-    private FrameLayout mQsFrame;
-    private final QsFrameTranslateController mQsFrameTranslateController;
     private KeyguardStatusViewController mKeyguardStatusViewController;
     private final LockIconViewController mLockIconViewController;
     private NotificationsQuickSettingsContainer mNotificationContainerParent;
@@ -396,47 +386,19 @@
     private final Provider<KeyguardBottomAreaViewController>
             mKeyguardBottomAreaViewControllerProvider;
     private boolean mAnimateNextPositionUpdate;
-    private float mQuickQsHeaderHeight;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-    private int mQsTrackingPointer;
-    private VelocityTracker mQsVelocityTracker;
     private TrackingStartedListener mTrackingStartedListener;
     private OpenCloseListener mOpenCloseListener;
     private GestureRecorder mGestureRecorder;
-    private boolean mQsTracking;
-    /** Whether the ongoing gesture might both trigger the expansion in both the view and QS. */
-    private boolean mConflictingQsExpansionGesture;
     private boolean mPanelExpanded;
 
-    /**
-     * Indicates that QS is in expanded state which can happen by:
-     * - single pane shade: expanding shade and then expanding QS
-     * - split shade: just expanding shade (QS are expanded automatically)
-     */
-    private boolean mQsExpanded;
-    private boolean mQsExpandedWhenExpandingStarted;
-    private boolean mQsFullyExpanded;
-    private boolean mKeyguardShowing;
     private boolean mKeyguardQsUserSwitchEnabled;
     private boolean mKeyguardUserSwitcherEnabled;
     private boolean mDozing;
     private boolean mDozingOnDown;
     private boolean mBouncerShowing;
     private int mBarState;
-    private float mInitialHeightOnTouch;
-    private float mInitialTouchX;
-    private float mInitialTouchY;
-    private float mQsExpansionHeight;
-    private int mQsMinExpansionHeight;
-    private int mQsMaxExpansionHeight;
-    private int mQsPeekHeight;
-    private boolean mStackScrollerOverscrolling;
-    private boolean mQsExpansionFromOverscroll;
-    private float mLastOverscroll;
-    private boolean mQsExpansionEnabledPolicy = true;
-    private boolean mQsExpansionEnabledAmbient = true;
-    private ValueAnimator mQsExpansionAnimator;
     private FlingAnimationUtils mFlingAnimationUtils;
     private int mStatusBarMinHeight;
     private int mStatusBarHeaderHeightKeyguard;
@@ -445,8 +407,7 @@
     private float mDownY;
     private int mDisplayTopInset = 0; // in pixels
     private int mDisplayRightInset = 0; // in pixels
-    private int mLargeScreenShadeHeaderHeight;
-    private int mSplitShadeNotificationsScrimMarginBottom;
+    private int mDisplayLeftInset = 0; // in pixels
 
     private final KeyguardClockPositionAlgorithm
             mClockPositionAlgorithm =
@@ -456,23 +417,7 @@
             new KeyguardClockPositionAlgorithm.Result();
     private boolean mIsExpanding;
 
-    /**
-     * Determines if QS should be already expanded when expanding shade.
-     * Used for split shade, two finger gesture as well as accessibility shortcut to QS.
-     * It needs to be set when movement starts as it resets at the end of expansion/collapse.
-     */
-    private boolean mQsExpandImmediate;
-    private boolean mTwoFingerQsExpandPossible;
     private String mHeaderDebugInfo;
-    /**
-     * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
-     * need to take this into account in our panel height calculation.
-     */
-    private boolean mQsAnimatorExpand;
-    private ValueAnimator mQsSizeChangeAnimator;
-    private boolean mQsScrimEnabled = true;
-    private boolean mQsTouchAboveFalsingThreshold;
-    private int mQsFalsingThreshold;
 
     /**
      * Indicates drag starting height when swiping down or up on heads-up notifications.
@@ -520,7 +465,7 @@
     private int mPanelAlpha;
     private Runnable mPanelAlphaEndAction;
     private float mBottomAreaShadeAlpha;
-    private final ValueAnimator mBottomAreaShadeAlphaAnimator;
+    final ValueAnimator mBottomAreaShadeAlphaAnimator;
     private final AnimatableProperty mPanelAlphaAnimator = AnimatableProperty.from("panelAlpha",
             NotificationPanelView::setPanelAlphaInternal,
             NotificationPanelView::getCurrentPanelAlpha,
@@ -554,51 +499,12 @@
     private Runnable mExpandAfterLayoutRunnable;
     private Runnable mHideExpandedRunnable;
 
-    /**
-     * The padding between the start of notifications and the qs boundary on the lockscreen.
-     * On lockscreen, notifications aren't inset this extra amount, but we still want the
-     * qs boundary to be padded.
-     */
-    private int mLockscreenNotificationQSPadding;
-    /**
-     * The amount of progress we are currently in if we're transitioning to the full shade.
-     * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
-     * shade. This value can also go beyond 1.1 when we're overshooting!
-     */
-    private float mTransitioningToFullShadeProgress;
-    /**
-     * Position of the qs bottom during the full shade transition. This is needed as the toppadding
-     * can change during state changes, which makes it much harder to do animations
-     */
-    private int mTransitionToFullShadeQSPosition;
-    /** Distance a full shade transition takes in order for qs to fully transition to the shade. */
-    private int mDistanceForQSFullShadeTransition;
-    /** The translation amount for QS for the full shade transition. */
-    private float mQsTranslationForFullShadeTransition;
-
     /** The maximum overshoot allowed for the top padding for the full shade transition. */
     private int mMaxOverscrollAmountForPulse;
-    /** Should we animate the next bounds update. */
-    private boolean mAnimateNextNotificationBounds;
-    /** The delay for the next bounds animation. */
-    private long mNotificationBoundsAnimationDelay;
-    /** The duration of the notification bounds animation. */
-    private long mNotificationBoundsAnimationDuration;
 
     /** Whether a collapse that started on the panel should allow the panel to intercept. */
     private boolean mIsPanelCollapseOnQQS;
-    private boolean mAnimatingQS;
-    /** The end bounds of a clipping animation. */
-    private final Rect mQsClippingAnimationEndBounds = new Rect();
-    /** The animator for the qs clipping bounds. */
-    private ValueAnimator mQsClippingAnimation = null;
-    /** Whether the current animator is resetting the qs translation. */
-    private boolean mIsQsTranslationResetAnimator;
 
-    /** Whether the current animator is resetting the pulse expansion after a drag down. */
-    private boolean mIsPulseExpansionResetAnimator;
-    private final Rect mLastQsClipBounds = new Rect();
-    private final Region mQsInterceptRegion = new Region();
     /** Alpha of the views which only show on the keyguard but not in shade / shade locked. */
     private float mKeyguardOnlyContentAlpha = 1.0f;
     /** Y translation of the views that only show on the keyguard but in shade / shade locked. */
@@ -608,15 +514,6 @@
     private boolean mIsGestureNavigation;
     private int mOldLayoutDirection;
     private NotificationShelfController mNotificationShelfController;
-    private int mScrimCornerRadius;
-    private int mScreenCornerRadius;
-    private boolean mQSAnimatingHiddenFromCollapsed;
-    private boolean mUseLargeScreenShadeHeader;
-    private boolean mEnableQsClipping;
-
-    private int mQsClipTop;
-    private int mQsClipBottom;
-    private boolean mQsVisible;
 
     private final ContentResolver mContentResolver;
     private float mMinFraction;
@@ -687,13 +584,19 @@
     private boolean mExpandLatencyTracking;
     private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
     private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+    private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
+    private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
+    private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
 
     private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private final KeyguardInteractor mKeyguardInteractor;
     private CoroutineDispatcher mMainDispatcher;
-    private boolean mIsToLockscreenTransitionRunning = false;
+    private boolean mIsOcclusionTransitionRunning = false;
     private int mDreamingToLockscreenTransitionTranslationY;
     private int mOccludedToLockscreenTransitionTranslationY;
-    private boolean mUnocclusionTransitionFlagEnabled = false;
+    private int mLockscreenToDreamingTransitionTranslationY;
+    private int mGoneToDreamingTransitionTranslationY;
+    private int mLockscreenToOccludedTransitionTranslationY;
 
     private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
             mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -704,20 +607,38 @@
         updatePanelExpansionAndVisibility();
     };
     private final Runnable mMaybeHideExpandedRunnable = () -> {
-        if (getExpansionFraction() == 0.0f) {
+        if (getExpandedFraction() == 0.0f) {
             postToView(mHideExpandedRunnable);
         }
     };
 
     private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
             (TransitionStep step) -> {
-                mIsToLockscreenTransitionRunning =
+                mIsOcclusionTransitionRunning =
                     step.getTransitionState() == TransitionState.RUNNING;
             };
 
     private final Consumer<TransitionStep> mOccludedToLockscreenTransition =
             (TransitionStep step) -> {
-                mIsToLockscreenTransitionRunning =
+                mIsOcclusionTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
+
+    private final Consumer<TransitionStep> mLockscreenToDreamingTransition =
+            (TransitionStep step) -> {
+                mIsOcclusionTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
+
+    private final Consumer<TransitionStep> mGoneToDreamingTransition =
+            (TransitionStep step) -> {
+                mIsOcclusionTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
+
+    private final Consumer<TransitionStep> mLockscreenToOccludedTransition =
+            (TransitionStep step) -> {
+                mIsOcclusionTransitionRunning =
                     step.getTransitionState() == TransitionState.RUNNING;
             };
 
@@ -767,17 +688,16 @@
             TapAgainViewController tapAgainViewController,
             NavigationModeController navigationModeController,
             NavigationBarController navigationBarController,
+            QuickSettingsController quickSettingsController,
             FragmentService fragmentService,
             ContentResolver contentResolver,
             RecordingController recordingController,
-            LargeScreenShadeHeaderController largeScreenShadeHeaderController,
+            ShadeHeaderController shadeHeaderController,
             ScreenOffAnimationController screenOffAnimationController,
             LockscreenGestureLogger lockscreenGestureLogger,
             ShadeExpansionStateManager shadeExpansionStateManager,
             NotificationRemoteInputManager remoteInputManager,
             Optional<SysUIUnfoldComponent> unfoldComponent,
-            InteractionJankMonitor interactionJankMonitor,
-            QsFrameTranslateController qsFrameTranslateController,
             SysUiState sysUiState,
             Provider<KeyguardBottomAreaViewController> keyguardBottomAreaViewControllerProvider,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
@@ -789,11 +709,17 @@
             SystemClock systemClock,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
+            AlternateBouncerInteractor alternateBouncerInteractor,
             DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
             OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
+            LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel,
+            GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel,
+            LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
             @Main CoroutineDispatcher mainDispatcher,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            KeyguardLongPressViewModel keyguardLongPressViewModel,
+            KeyguardInteractor keyguardInteractor) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onKeyguardFadingAwayChanged() {
@@ -810,7 +736,11 @@
         mGutsManager = gutsManager;
         mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
         mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
+        mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel;
+        mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel;
+        mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel;
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+        mKeyguardInteractor = keyguardInteractor;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -827,6 +757,7 @@
 
         mResources = mView.getResources();
         mKeyguardStateController = keyguardStateController;
+        mQsController = quickSettingsController;
         mKeyguardIndicationController = keyguardIndicationController;
         mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -857,7 +788,6 @@
         mVibratorHelper = vibratorHelper;
         mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
-        mInteractionJankMonitor = interactionJankMonitor;
         mSystemClock = systemClock;
         mKeyguardMediaController = keyguardMediaController;
         mMetricsLogger = metricsLogger;
@@ -882,7 +812,7 @@
         mSplitShadeEnabled =
                 LargeScreenUtils.shouldUseSplitNotificationShade(mResources);
         mView.setWillNotDraw(!DEBUG_DRAWABLE);
-        mLargeScreenShadeHeaderController = largeScreenShadeHeaderController;
+        mShadeHeaderController = shadeHeaderController;
         mLayoutInflater = layoutInflater;
         mFeatureFlags = featureFlags;
         mFalsingCollector = falsingCollector;
@@ -893,7 +823,6 @@
         mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
         setPanelAlpha(255, false /* animate */);
         mCommandQueue = commandQueue;
-        mRecordingController = recordingController;
         mDisplayId = displayId;
         mPulseExpansionHandler = pulseExpansionHandler;
         mDozeParameters = dozeParameters;
@@ -902,20 +831,19 @@
         mMediaDataManager = mediaDataManager;
         mTapAgainViewController = tapAgainViewController;
         mSysUiState = sysUiState;
-        pulseExpansionHandler.setPulseExpandAbortListener(() -> {
-            if (mQs != null) {
-                mQs.animateHeaderSlidingOut();
-            }
-        });
         statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged);
         mKeyguardBypassController = bypassController;
         mUpdateMonitor = keyguardUpdateMonitor;
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
-        mShadeTransitionController = shadeTransitionController;
         lockscreenShadeTransitionController.setNotificationPanelController(this);
         shadeTransitionController.setNotificationPanelViewController(this);
         dynamicPrivacyController.addListener(this::onDynamicPrivacyChanged);
-
+        quickSettingsController.setExpansionHeightListener(this::onQsSetExpansionHeightCalled);
+        quickSettingsController.setQsStateUpdateListener(this::onQsStateUpdated);
+        quickSettingsController.setApplyClippingImmediatelyListener(
+                this::onQsClippingImmediatelyApplied);
+        quickSettingsController.setFlingQsWithoutClickListener(this::onFlingQsWithoutClick);
+        quickSettingsController.setExpansionHeightSetToMaxListener(this::onExpansionHeightSetToMax);
         shadeExpansionStateManager.addStateListener(this::onPanelStateChanged);
 
         mBottomAreaShadeAlphaAnimator = ValueAnimator.ofFloat(1f, 0);
@@ -930,7 +858,6 @@
         mLockIconViewController = lockIconViewController;
         mScreenOffAnimationController = screenOffAnimationController;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
-        mRemoteInputManager = remoteInputManager;
         mLastDownEvents = new NPVCDownEventState.Buffer(MAX_DOWN_EVENT_BUFFER_SIZE);
 
         int currentMode = navigationModeController.addListener(
@@ -949,7 +876,8 @@
 
         if (DEBUG_DRAWABLE) {
             mView.getOverlay().add(new DebugDrawable(this, mView,
-                    mNotificationStackScrollLayoutController, mLockIconViewController));
+                    mNotificationStackScrollLayoutController, mLockIconViewController,
+                    mQsController));
         }
 
         mKeyguardUnfoldTransition = unfoldComponent.map(
@@ -957,12 +885,17 @@
         mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
                 SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
 
-        mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
-
-        mQsFrameTranslateController = qsFrameTranslateController;
         updateUserSwitcherFlags();
         mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
         mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
+        KeyguardLongPressViewBinder.bind(
+                mView.requireViewById(R.id.keyguard_long_press),
+                keyguardLongPressViewModel,
+                () -> {
+                    onEmptySpaceClick();
+                    return Unit.INSTANCE;
+                },
+                mFalsingManager);
         onFinishInflate();
         keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
                 new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -980,6 +913,7 @@
                         unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay);
                     }
                 });
+        mAlternateBouncerInteractor = alternateBouncerInteractor;
         dumpManager.registerDumpable(this);
     }
 
@@ -1066,19 +1000,16 @@
         mNotificationStackScrollLayoutController.attach(stackScrollLayout);
         mNotificationStackScrollLayoutController.setOnHeightChangedListener(
                 new NsslHeightChangedListener());
-        mNotificationStackScrollLayoutController.setOverscrollTopChangedListener(
-                mOnOverscrollTopChangedListener);
-        mNotificationStackScrollLayoutController.setOnScrollListener(this::onNotificationScrolled);
-        mNotificationStackScrollLayoutController.setOnStackYChanged(this::onStackYChanged);
         mNotificationStackScrollLayoutController.setOnEmptySpaceClickListener(
                 mOnEmptySpaceClickListener);
+        mQsController.initNotificationStackScrollLayoutController();
+        mShadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
         addTrackingHeadsUpListener(mNotificationStackScrollLayoutController::setTrackingHeadsUp);
         setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
 
         initBottomArea();
 
         mWakeUpCoordinator.setStackScroller(mNotificationStackScrollLayoutController);
-        mQsFrame = mView.findViewById(R.id.qs_frame);
         mPulseExpansionHandler.setUp(mNotificationStackScrollLayoutController);
         mWakeUpCoordinator.addListener(new NotificationWakeUpCoordinator.WakeUpListener() {
             @Override
@@ -1107,34 +1038,55 @@
         }
 
         mTapAgainViewController.init();
-        mLargeScreenShadeHeaderController.init();
+        mShadeHeaderController.init();
         mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
         mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
                 controller.setup(mNotificationContainerParent));
 
-        if (mUnocclusionTransitionFlagEnabled) {
-            // Dreaming->Lockscreen
-            collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
-                    mDreamingToLockscreenTransition, mMainDispatcher);
-            collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
-                    toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
-                    mMainDispatcher);
-            collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
-                    mDreamingToLockscreenTransitionTranslationY),
-                    toLockscreenTransitionY(mNotificationStackScrollLayoutController),
-                    mMainDispatcher);
+        // Dreaming->Lockscreen
+        collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
+                mDreamingToLockscreenTransition, mMainDispatcher);
+        collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
+                setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
+        collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
+                mDreamingToLockscreenTransitionTranslationY),
+                setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher);
 
-            // Occluded->Lockscreen
-            collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
-                    mOccludedToLockscreenTransition, mMainDispatcher);
-            collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
-                    toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
-                    mMainDispatcher);
-            collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(
-                    mOccludedToLockscreenTransitionTranslationY),
-                    toLockscreenTransitionY(mNotificationStackScrollLayoutController),
-                    mMainDispatcher);
-        }
+        // Occluded->Lockscreen
+        collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
+                mOccludedToLockscreenTransition, mMainDispatcher);
+        collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
+                setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
+        collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(
+                mOccludedToLockscreenTransitionTranslationY),
+                setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher);
+
+        // Lockscreen->Dreaming
+        collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(),
+                mLockscreenToDreamingTransition, mMainDispatcher);
+        collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(),
+                setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
+        collectFlow(mView, mLockscreenToDreamingTransitionViewModel.lockscreenTranslationY(
+                mLockscreenToDreamingTransitionTranslationY),
+                setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher);
+
+        // Gone->Dreaming
+        collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
+                mGoneToDreamingTransition, mMainDispatcher);
+        collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
+                setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
+        collectFlow(mView, mGoneToDreamingTransitionViewModel.lockscreenTranslationY(
+                mGoneToDreamingTransitionTranslationY),
+                setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher);
+
+        // Lockscreen->Occluded
+        collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(),
+                mLockscreenToOccludedTransition, mMainDispatcher);
+        collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(),
+                setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
+        collectFlow(mView, mLockscreenToOccludedTransitionViewModel.lockscreenTranslationY(
+                mLockscreenToOccludedTransitionTranslationY),
+                setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher);
     }
 
     @VisibleForTesting
@@ -1148,24 +1100,14 @@
                 .setMaxLengthSeconds(0.4f).build();
         mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
         mStatusBarHeaderHeightKeyguard = Utils.getStatusBarHeaderHeightKeyguard(mView.getContext());
-        mQsPeekHeight = mResources.getDimensionPixelSize(R.dimen.qs_peek_height);
         mClockPositionAlgorithm.loadDimens(mResources);
-        mQsFalsingThreshold = mResources.getDimensionPixelSize(R.dimen.qs_falsing_threshold);
         mIndicationBottomPadding = mResources.getDimensionPixelSize(
                 R.dimen.keyguard_indication_bottom_padding);
         int statusbarHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
         mHeadsUpInset = statusbarHeight + mResources.getDimensionPixelSize(
                 R.dimen.heads_up_status_bar_padding);
-        mDistanceForQSFullShadeTransition = mResources.getDimensionPixelSize(
-                R.dimen.lockscreen_shade_qs_transition_distance);
         mMaxOverscrollAmountForPulse = mResources.getDimensionPixelSize(
                 R.dimen.pulse_expansion_max_top_overshoot);
-        mScrimCornerRadius = mResources.getDimensionPixelSize(
-                R.dimen.notification_scrim_corner_radius);
-        mScreenCornerRadius = (int) ScreenDecorationsUtils.getWindowCornerRadius(
-                mView.getContext());
-        mLockscreenNotificationQSPadding = mResources.getDimensionPixelSize(
-                R.dimen.notification_side_paddings);
         mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
         mSplitShadeScrimTransitionDistance = mResources.getDimensionPixelSize(
                 R.dimen.split_shade_scrim_transition_distance);
@@ -1173,6 +1115,14 @@
                 R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
         mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
                 R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
+        mLockscreenToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
+                R.dimen.lockscreen_to_dreaming_transition_lockscreen_translation_y);
+        mGoneToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
+                R.dimen.gone_to_dreaming_transition_lockscreen_translation_y);
+        mLockscreenToOccludedTransitionTranslationY = mResources.getDimensionPixelSize(
+                R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y);
+        // TODO (b/265193930): remove this and make QsController listen to NotificationPanelViews
+        mQsController.loadDimens();
     }
 
     private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1215,40 +1165,13 @@
     }
 
     public void updateResources() {
-        mSplitShadeNotificationsScrimMarginBottom =
-                mResources.getDimensionPixelSize(
-                        R.dimen.split_shade_notifications_scrim_margin_bottom);
         final boolean newSplitShadeEnabled =
                 LargeScreenUtils.shouldUseSplitNotificationShade(mResources);
         final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled;
         mSplitShadeEnabled = newSplitShadeEnabled;
-
-        if (mQs != null) {
-            mQs.setInSplitShade(mSplitShadeEnabled);
-        }
-
-        mUseLargeScreenShadeHeader =
-                LargeScreenUtils.shouldUseLargeScreenShadeHeader(mView.getResources());
-
-        mLargeScreenShadeHeaderHeight =
-                mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
-        // TODO: When the flag is eventually removed, it means that we have a single view that is
-        // the same height in QQS and in Large Screen (large_screen_shade_header_height). Eventually
-        // the concept of largeScreenHeader or quickQsHeader will disappear outside of the class
-        // that controls the view as the offset needs to be the same regardless.
-        if (mUseLargeScreenShadeHeader || mFeatureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)) {
-            mQuickQsHeaderHeight = mLargeScreenShadeHeaderHeight;
-        } else {
-            mQuickQsHeaderHeight = SystemBarUtils.getQuickQsOffsetHeight(mView.getContext());
-        }
-        int topMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight :
-                mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top);
-        mLargeScreenShadeHeaderController.setLargeScreenActive(mUseLargeScreenShadeHeader);
-        mAmbientState.setStackTopMargin(topMargin);
+        mQsController.updateResources();
         mNotificationsQSContainerController.updateResources();
-
         updateKeyguardStatusViewAlignment(/* animate= */false);
-
         mKeyguardMediaController.refreshMediaPosition();
 
         if (splitShadeChanged) {
@@ -1257,17 +1180,16 @@
 
         mSplitShadeFullTransitionDistance =
                 mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance);
-
-        mEnableQsClipping = mResources.getBoolean(R.bool.qs_enable_clipping);
     }
 
     private void onSplitShadeEnabledChanged() {
+        mShadeLog.logSplitShadeChanged(mSplitShadeEnabled);
         // when we switch between split shade and regular shade we want to enforce setting qs to
         // the default state: expanded for split shade and collapsed otherwise
         if (!isOnKeyguard() && mPanelExpanded) {
-            setQsExpanded(mSplitShadeEnabled);
+            mQsController.setExpanded(mSplitShadeEnabled);
         }
-        if (isOnKeyguard() && mQsExpanded && mSplitShadeEnabled) {
+        if (isOnKeyguard() && mQsController.getExpanded() && mSplitShadeEnabled) {
             // In single column keyguard - when you swipe from the top - QS is fully expanded and
             // StatusBarState is KEYGUARD. That state doesn't make sense for split shade,
             // where notifications are always visible and we effectively go to fully expanded
@@ -1277,7 +1199,7 @@
             mStatusBarStateController.setState(StatusBarState.SHADE_LOCKED, /* force= */false);
         }
         updateClockAppearance();
-        updateQsState();
+        mQsController.updateQsState();
         mNotificationStackScrollLayoutController.updateFooter();
     }
 
@@ -1385,11 +1307,6 @@
         mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
     }
 
-    @VisibleForTesting
-    void setQs(QS qs) {
-        mQs = qs;
-    }
-
     private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
         mKeyguardMediaController.attachSplitShadeContainer(container);
     }
@@ -1421,7 +1338,7 @@
             if (SPEW_LOGCAT) Log.d(TAG, "Skipping computeMaxKeyguardNotifications() by request");
         }
 
-        if (mKeyguardShowing && !mKeyguardBypassController.getBypassEnabled()) {
+        if (getKeyguardShowing() && !mKeyguardBypassController.getBypassEnabled()) {
             mNotificationStackScrollLayoutController.setMaxDisplayedNotifications(
                     mMaxAllowedKeyguardNotifications);
             mNotificationStackScrollLayoutController.setKeyguardBottomPaddingForDebug(
@@ -1470,39 +1387,14 @@
         mIsFullWidth = isFullWidth;
         mScrimController.setClipsQsScrim(isFullWidth);
         mNotificationStackScrollLayoutController.setIsFullWidth(isFullWidth);
-        if (mQs != null) {
-            mQs.setIsNotificationPanelFullWidth(isFullWidth);
-        }
-    }
-
-    private void startQsSizeChangeAnimation(int oldHeight, final int newHeight) {
-        if (mQsSizeChangeAnimator != null) {
-            oldHeight = (int) mQsSizeChangeAnimator.getAnimatedValue();
-            mQsSizeChangeAnimator.cancel();
-        }
-        mQsSizeChangeAnimator = ValueAnimator.ofInt(oldHeight, newHeight);
-        mQsSizeChangeAnimator.setDuration(300);
-        mQsSizeChangeAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        mQsSizeChangeAnimator.addUpdateListener(animation -> {
-            requestScrollerTopPaddingUpdate(false /* animate */);
-            updateExpandedHeightToMaxHeight();
-            int height = (int) mQsSizeChangeAnimator.getAnimatedValue();
-            mQs.setHeightOverride(height);
-        });
-        mQsSizeChangeAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mQsSizeChangeAnimator = null;
-            }
-        });
-        mQsSizeChangeAnimator.start();
+        mQsController.setNotificationPanelFullWidth(isFullWidth);
     }
 
     /**
      * Positions the clock and notifications dynamically depending on how many notifications are
      * showing.
      */
-    private void positionClockAndNotifications() {
+    void positionClockAndNotifications() {
         positionClockAndNotifications(false /* forceUpdate */);
     }
 
@@ -1527,7 +1419,7 @@
                 // so we should not add a padding for them
                 stackScrollerPadding = 0;
             } else {
-                stackScrollerPadding = getUnlockedStackScrollerPadding();
+                stackScrollerPadding = mQsController.getUnlockedStackScrollerPadding();
             }
         } else {
             stackScrollerPadding = mClockPositionResult.stackScrollerPaddingExpanded;
@@ -1575,8 +1467,9 @@
                 userSwitcherHeight,
                 userSwitcherPreferredY,
                 darkAmount, mOverStretchAmount,
-                bypassEnabled, getUnlockedStackScrollerPadding(),
-                computeQsExpansionFraction(),
+                bypassEnabled,
+                mQsController.getUnlockedStackScrollerPadding(),
+                mQsController.computeExpansionFraction(),
                 mDisplayTopInset,
                 mSplitShadeEnabled,
                 udfpsAodTopLocation,
@@ -1728,19 +1621,16 @@
         return mDozing && mDozeParameters.getAlwaysOn();
     }
 
+    boolean isDozing() {
+        return mDozing;
+    }
+
     private boolean hasVisibleNotifications() {
         return mNotificationStackScrollLayoutController
                 .getVisibleNotificationCount() != 0
                 || mMediaDataManager.hasActiveMediaOrRecommendation();
     }
 
-    /**
-     * @return the padding of the stackscroller when unlocked
-     */
-    private int getUnlockedStackScrollerPadding() {
-        return (mQs != null ? mQs.getHeader().getHeight() : 0) + mQsPeekHeight;
-    }
-
     /** Returns space between top of lock icon and bottom of NotificationStackScrollLayout. */
     private float getLockIconPadding() {
         float lockIconPadding = 0f;
@@ -1836,7 +1726,7 @@
     }
 
     private void updateClock() {
-        if (mIsToLockscreenTransitionRunning) {
+        if (mIsOcclusionTransitionRunning) {
             return;
         }
         float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
@@ -1858,23 +1748,23 @@
         mAnimateNextPositionUpdate = true;
     }
 
-    private void setQsExpansionEnabled() {
-        if (mQs == null) return;
-        mQs.setHeaderClickable(isQsExpansionEnabled());
-    }
+    /** Animate QS closing. */
+    public void animateCloseQs(boolean animateAway) {
+        if (mSplitShadeEnabled) {
+            collapsePanel(true, false, 1.0f);
+        } else {
+            mQsController.animateCloseQs(animateAway);
+        }
 
-    public void setQsExpansionEnabledPolicy(boolean qsExpansionEnabledPolicy) {
-        mQsExpansionEnabledPolicy = qsExpansionEnabledPolicy;
-        setQsExpansionEnabled();
     }
 
     public void resetViews(boolean animate) {
         mGutsManager.closeAndSaveGuts(true /* leavebehind */, true /* force */,
                 true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
         if (animate && !isFullyCollapsed()) {
-            animateCloseQs(true /* animateAway */);
+            animateCloseQs(true);
         } else {
-            closeQs();
+            closeQsIfPossible();
         }
         mNotificationStackScrollLayoutController.setOverScrollAmount(0f, true /* onTop */, animate,
                 !animate /* cancelAnimators */);
@@ -1905,8 +1795,8 @@
             return;
         }
 
-        if (mQsExpanded) {
-            setQsExpandImmediate(true);
+        if (mQsController.getExpanded()) {
+            mQsController.setExpandImmediate(true);
             setShowShelfOnly(true);
         }
         debugLog("collapse: %s", this);
@@ -1925,33 +1815,11 @@
         }
     }
 
-    @VisibleForTesting
-    void setQsExpandImmediate(boolean expandImmediate) {
-        if (expandImmediate != mQsExpandImmediate) {
-            mQsExpandImmediate = expandImmediate;
-            mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate);
-        }
-    }
-
-    @VisibleForTesting
-    boolean isQsExpandImmediate() {
-        return mQsExpandImmediate;
-    }
-
     private void setShowShelfOnly(boolean shelfOnly) {
         mNotificationStackScrollLayoutController.setShouldShowShelfOnly(
                 shelfOnly && !mSplitShadeEnabled);
     }
 
-    public void closeQs() {
-        cancelQsAnimation();
-        setQsExpansionHeight(mQsMinExpansionHeight);
-        // qsExpandImmediate is a safety latch in case we're calling closeQS while we're in the
-        // middle of animation - we need to make sure that value is always false when shade if
-        // fully collapsed or expanded
-        setQsExpandImmediate(false);
-    }
-
     @VisibleForTesting
     void cancelHeightAnimator() {
         if (mHeightAnimator != null) {
@@ -1967,39 +1835,9 @@
         mView.animate().cancel();
     }
 
-    /**
-     * Animate QS closing by flinging it.
-     * If QS is expanded, it will collapse into QQS and stop.
-     * If in split shade, it will collapse the whole shade.
-     *
-     * @param animateAway Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
-     */
-    public void animateCloseQs(boolean animateAway) {
-        if (mSplitShadeEnabled) {
-            collapsePanel(
-                    /* animate= */true, /* delayed= */false, /* speedUpFactor= */1.0f);
-            return;
-        }
-
-        if (mQsExpansionAnimator != null) {
-            if (!mQsAnimatorExpand) {
-                return;
-            }
-            float height = mQsExpansionHeight;
-            mQsExpansionAnimator.cancel();
-            setQsExpansionHeight(height);
-        }
-        flingSettings(0 /* vel */, animateAway ? FLING_HIDE : FLING_COLLAPSE);
-    }
-
-    private boolean isQsExpansionEnabled() {
-        return mQsExpansionEnabledPolicy && mQsExpansionEnabledAmbient
-                && !mRemoteInputManager.isRemoteInputActive();
-    }
-
     public void expandWithQs() {
-        if (isQsExpansionEnabled()) {
-            setQsExpandImmediate(true);
+        if (mQsController.isExpansionEnabled()) {
+            mQsController.setExpandImmediate(true);
             setShowShelfOnly(true);
         }
         if (mSplitShadeEnabled && isOnKeyguard()) {
@@ -2015,14 +1853,24 @@
         } else if (isFullyCollapsed()) {
             expand(true /* animate */);
         } else {
-            traceQsJank(true /* startTracing */, false /* wasCancelled */);
-            flingSettings(0 /* velocity */, FLING_EXPAND);
+            mQsController.traceQsJank(true /* startTracing */, false /* wasCancelled */);
+            mQsController.flingQs(0, FLING_EXPAND);
         }
     }
 
-    public void expandWithoutQs() {
-        if (isQsExpanded()) {
-            flingSettings(0 /* velocity */, FLING_COLLAPSE);
+    /**
+     * Expand shade so that notifications are visible.
+     * Non-split shade: just expanding shade or collapsing QS when they're expanded.
+     * Split shade: only expanding shade, notifications are always visible
+     *
+     * Called when `adb shell cmd statusbar expand-notifications` is executed.
+     */
+    public void expandShadeToNotifications() {
+        if (mSplitShadeEnabled && (isShadeFullyOpen() || isExpanding())) {
+            return;
+        }
+        if (mQsController.getExpanded()) {
+            mQsController.flingQs(0, FLING_COLLAPSE);
         } else {
             expand(true /* animate */);
         }
@@ -2039,6 +1887,7 @@
     @VisibleForTesting
     void flingToHeight(float vel, boolean expand, float target,
             float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+        mQsController.setLastShadeFlingWasExpanding(expand);
         mHeadsUpTouchHelper.notifyFling(!expand);
         mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
         setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
@@ -2052,7 +1901,6 @@
         // we want to perform an overshoot animation when flinging open
         final boolean addOverscroll =
                 expand
-                        && !mSplitShadeEnabled // Split shade has its own overscroll logic
                         && mStatusBarStateController.getState() != KEYGUARD
                         && mOverExpansion == 0.0f
                         && vel >= 0;
@@ -2070,6 +1918,7 @@
         }
         ValueAnimator animator = createHeightAnimator(target, overshootAmount);
         if (expand) {
+            maybeVibrateOnOpening(true /* openingWithTouch */);
             if (expandBecauseOfFalsing && vel < 0) {
                 vel = 0;
             }
@@ -2080,6 +1929,7 @@
                 animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
             }
         } else {
+            mHasVibratedOnOpen = false;
             if (shouldUseDismissingAnimation()) {
                 if (vel == 0) {
                     animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
@@ -2108,7 +1958,7 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 if (!mStatusBarStateController.isDozing()) {
-                    beginJankMonitoring();
+                    mQsController.beginJankMonitoring(isFullyCollapsed());
                 }
             }
 
@@ -2140,117 +1990,15 @@
         setAnimator(null);
         mKeyguardStateController.notifyPanelFlingEnd();
         if (!cancelled) {
-            endJankMonitoring();
+            mQsController.endJankMonitoring();
             notifyExpandingFinished();
         } else {
-            cancelJankMonitoring();
+            mQsController.cancelJankMonitoring();
         }
         updatePanelExpansionAndVisibility();
         mNotificationStackScrollLayoutController.setPanelFlinging(false);
     }
 
-    private boolean onQsIntercept(MotionEvent event) {
-        debugLog("onQsIntercept");
-        int pointerIndex = event.findPointerIndex(mQsTrackingPointer);
-        if (pointerIndex < 0) {
-            pointerIndex = 0;
-            mQsTrackingPointer = event.getPointerId(pointerIndex);
-        }
-        final float x = event.getX(pointerIndex);
-        final float y = event.getY(pointerIndex);
-
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN:
-                mInitialTouchY = y;
-                mInitialTouchX = x;
-                initVelocityTracker();
-                trackMovement(event);
-                float qsExpansionFraction = computeQsExpansionFraction();
-                // Intercept the touch if QS is between fully collapsed and fully expanded state
-                if (!mSplitShadeEnabled
-                        && qsExpansionFraction > 0.0 && qsExpansionFraction < 1.0) {
-                    mShadeLog.logMotionEvent(event,
-                            "onQsIntercept: down action, QS partially expanded/collapsed");
-                    return true;
-                }
-                if (mKeyguardShowing
-                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
-                    // Dragging down on the lockscreen statusbar should prohibit other interactions
-                    // immediately, otherwise we'll wait on the touchslop. This is to allow
-                    // dragging down to expanded quick settings directly on the lockscreen.
-                    mView.getParent().requestDisallowInterceptTouchEvent(true);
-                }
-                if (mQsExpansionAnimator != null) {
-                    mInitialHeightOnTouch = mQsExpansionHeight;
-                    mShadeLog.logMotionEvent(event,
-                            "onQsIntercept: down action, QS tracking enabled");
-                    mQsTracking = true;
-                    traceQsJank(true /* startTracing */, false /* wasCancelled */);
-                    mNotificationStackScrollLayoutController.cancelLongPress();
-                }
-                break;
-            case MotionEvent.ACTION_POINTER_UP:
-                final int upPointer = event.getPointerId(event.getActionIndex());
-                if (mQsTrackingPointer == upPointer) {
-                    // gesture is ongoing, find a new pointer to track
-                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
-                    mQsTrackingPointer = event.getPointerId(newIndex);
-                    mInitialTouchX = event.getX(newIndex);
-                    mInitialTouchY = event.getY(newIndex);
-                }
-                break;
-
-            case MotionEvent.ACTION_MOVE:
-                final float h = y - mInitialTouchY;
-                trackMovement(event);
-                if (mQsTracking) {
-
-                    // Already tracking because onOverscrolled was called. We need to update here
-                    // so we don't stop for a frame until the next touch event gets handled in
-                    // onTouchEvent.
-                    setQsExpansionHeight(h + mInitialHeightOnTouch);
-                    trackMovement(event);
-                    return true;
-                } else {
-                    mShadeLog.logMotionEvent(event,
-                            "onQsIntercept: move ignored because qs tracking disabled");
-                }
-                float touchSlop = getTouchSlop(event);
-                if ((h > touchSlop || (h < -touchSlop && mQsExpanded))
-                        && Math.abs(h) > Math.abs(x - mInitialTouchX)
-                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
-                    mView.getParent().requestDisallowInterceptTouchEvent(true);
-                    mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
-                    mQsTracking = true;
-                    traceQsJank(true /* startTracing */, false /* wasCancelled */);
-                    onQsExpansionStarted();
-                    notifyExpandingFinished();
-                    mInitialHeightOnTouch = mQsExpansionHeight;
-                    mInitialTouchY = y;
-                    mInitialTouchX = x;
-                    mNotificationStackScrollLayoutController.cancelLongPress();
-                    return true;
-                } else {
-                    mShadeLog.logQsTrackingNotStarted(mInitialTouchY, y, h, touchSlop, mQsExpanded,
-                            mCollapsedOnDown, mKeyguardShowing, isQsExpansionEnabled());
-                }
-                break;
-
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                trackMovement(event);
-                mShadeLog.logMotionEvent(event, "onQsIntercept: up action, QS tracking disabled");
-                mQsTracking = false;
-                break;
-        }
-        return false;
-    }
-
-    @VisibleForTesting
-    boolean isQsTracking() {
-        return mQsTracking;
-    }
-
     private boolean isInContentBounds(float x, float y) {
         float stackScrollerX = mNotificationStackScrollLayoutController.getX();
         return !mNotificationStackScrollLayoutController
@@ -2259,39 +2007,24 @@
                 && x < stackScrollerX + mNotificationStackScrollLayoutController.getWidth();
     }
 
-    private void traceQsJank(boolean startTracing, boolean wasCancelled) {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        if (startTracing) {
-            mInteractionJankMonitor.begin(mView, CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE);
-        } else {
-            if (wasCancelled) {
-                mInteractionJankMonitor.cancel(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE);
-            } else {
-                mInteractionJankMonitor.end(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE);
-            }
-        }
-    }
-
     private void initDownStates(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
-            mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
             mDozingOnDown = mDozing;
             mDownX = event.getX();
             mDownY = event.getY();
             mCollapsedOnDown = isFullyCollapsed();
-            mIsPanelCollapseOnQQS = canPanelCollapseOnQQS(mDownX, mDownY);
+            mQsController.setCollapsedOnDown(mCollapsedOnDown);
+            mIsPanelCollapseOnQQS = mQsController.canPanelCollapseOnQQS(mDownX, mDownY);
             mListenForHeadsUp = mCollapsedOnDown && mHeadsUpManager.hasPinnedHeadsUp();
             mAllowExpandForSmallExpansion = mExpectingSynthesizedDown;
             mTouchSlopExceededBeforeDown = mExpectingSynthesizedDown;
             // When false, down but not synthesized motion event.
             mLastEventSynthesizedDown = mExpectingSynthesizedDown;
             mLastDownEvents.insert(
-                    mSystemClock.currentTimeMillis(),
+                    event.getEventTime(),
                     mDownX,
                     mDownY,
-                    mQsTouchAboveFalsingThreshold,
+                    mQsController.updateAndGetTouchAboveFalsingThreshold(),
                     mDozingOnDown,
                     mCollapsedOnDown,
                     mIsPanelCollapseOnQQS,
@@ -2306,84 +2039,14 @@
         }
     }
 
-    /**
-     * Can the panel collapse in this motion because it was started on QQS?
-     *
-     * @param downX the x location where the touch started
-     * @param downY the y location where the touch started
-     * @return true if the panel could be collapsed because it stared on QQS
-     */
-    private boolean canPanelCollapseOnQQS(float downX, float downY) {
-        if (mCollapsedOnDown || mKeyguardShowing || mQsExpanded) {
-            return false;
-        }
-        View header = mQs == null ? mKeyguardStatusBar : mQs.getHeader();
-        return downX >= mQsFrame.getX() && downX <= mQsFrame.getX() + mQsFrame.getWidth()
-                && downY <= header.getBottom();
-
-    }
-
-    private void flingQsWithCurrentVelocity(float y, boolean isCancelMotionEvent) {
-        float vel = getCurrentQSVelocity();
-        boolean expandsQs = flingExpandsQs(vel);
-        if (expandsQs) {
-            if (mFalsingManager.isUnlockingDisabled() || isFalseTouch()) {
-                expandsQs = false;
-            } else {
-                logQsSwipeDown(y);
-            }
-        } else if (vel < 0) {
-            mFalsingManager.isFalseTouch(QS_COLLAPSE);
-        }
-
-        int flingType;
-        if (expandsQs && !isCancelMotionEvent) {
-            flingType = FLING_EXPAND;
-        } else if (mSplitShadeEnabled) {
-            flingType = FLING_HIDE;
-        } else {
-            flingType = FLING_COLLAPSE;
-        }
-        flingSettings(vel, flingType);
-    }
-
-    private void logQsSwipeDown(float y) {
-        float vel = getCurrentQSVelocity();
-        final int
-                gesture =
-                mBarState == KEYGUARD ? MetricsEvent.ACTION_LS_QS
-                        : MetricsEvent.ACTION_SHADE_QS_PULL;
-        mLockscreenGestureLogger.write(gesture,
-                (int) ((y - mInitialTouchY) / mCentralSurfaces.getDisplayDensity()),
-                (int) (vel / mCentralSurfaces.getDisplayDensity()));
-    }
-
-    private boolean flingExpandsQs(float vel) {
+    boolean flingExpandsQs(float vel) {
         if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-            return computeQsExpansionFraction() > 0.5f;
+            return mQsController.computeExpansionFraction() > 0.5f;
         } else {
             return vel > 0;
         }
     }
 
-    private boolean isFalseTouch() {
-        if (mFalsingManager.isClassifierEnabled()) {
-            return mFalsingManager.isFalseTouch(Classifier.QUICK_SETTINGS);
-        }
-        return !mQsTouchAboveFalsingThreshold;
-    }
-
-    private float computeQsExpansionFraction() {
-        if (mQSAnimatingHiddenFromCollapsed) {
-            // When hiding QS from collapsed state, the expansion can sometimes temporarily
-            // be larger than 0 because of the timing, leading to flickers.
-            return 0.0f;
-        }
-        return Math.min(
-                1f, (mQsExpansionHeight - mQsMinExpansionHeight) / (mQsMaxExpansionHeight
-                        - mQsMinExpansionHeight));
-    }
-
     private boolean shouldExpandWhenNotFlinging() {
         if (getExpandedFraction() > 0.5f) {
             return true;
@@ -2401,121 +2064,13 @@
         return mNotificationStackScrollLayoutController.getOpeningHeight();
     }
 
-
-    private boolean handleQsTouch(MotionEvent event) {
-        if (isSplitShadeAndTouchXOutsideQs(event.getX())) {
-            return false;
-        }
-        final int action = event.getActionMasked();
-        boolean collapsedQs = !mQsExpanded && !mSplitShadeEnabled;
-        boolean expandedShadeCollapsedQs = getExpandedFraction() == 1f && mBarState != KEYGUARD
-                && collapsedQs && isQsExpansionEnabled();
-        if (action == MotionEvent.ACTION_DOWN && expandedShadeCollapsedQs) {
-            // Down in the empty area while fully expanded - go to QS.
-            mShadeLog.logMotionEvent(event, "handleQsTouch: down action, QS tracking enabled");
-            mQsTracking = true;
-            traceQsJank(true /* startTracing */, false /* wasCancelled */);
-            mConflictingQsExpansionGesture = true;
-            onQsExpansionStarted();
-            mInitialHeightOnTouch = mQsExpansionHeight;
-            mInitialTouchY = event.getY();
-            mInitialTouchX = event.getX();
-        }
-        if (!isFullyCollapsed()) {
-            handleQsDown(event);
-        }
-        // defer touches on QQS to shade while shade is collapsing. Added margin for error
-        // as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
-        if (!mSplitShadeEnabled
-                && computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
-            mShadeLog.logMotionEvent(event,
-                    "handleQsTouch: QQS touched while shade collapsing, QS tracking disabled");
-            mQsTracking = false;
-        }
-        if (!mQsExpandImmediate && mQsTracking) {
-            onQsTouch(event);
-            if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) {
-                mShadeLog.logMotionEvent(event,
-                        "handleQsTouch: not immediate expand or conflicting gesture");
-                return true;
-            }
-        }
-        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
-            mConflictingQsExpansionGesture = false;
-        }
-        if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed() && isQsExpansionEnabled()) {
-            mTwoFingerQsExpandPossible = true;
-        }
-        if (mTwoFingerQsExpandPossible && isOpenQsEvent(event) && event.getY(event.getActionIndex())
-                < mStatusBarMinHeight) {
-            mMetricsLogger.count(COUNTER_PANEL_OPEN_QS, 1);
-            setQsExpandImmediate(true);
-            setShowShelfOnly(true);
-            updateExpandedHeightToMaxHeight();
-
-            // Normally, we start listening when the panel is expanded, but here we need to start
-            // earlier so the state is already up to date when dragging down.
-            setListening(true);
-        }
-        return false;
+    float getDisplayDensity() {
+        return mCentralSurfaces.getDisplayDensity();
     }
 
-    /** Returns whether split shade is enabled and an x coordinate is outside of the QS frame. */
-    private boolean isSplitShadeAndTouchXOutsideQs(float touchX) {
-        return mSplitShadeEnabled && (touchX < mQsFrame.getX()
-                || touchX > mQsFrame.getX() + mQsFrame.getWidth());
-    }
-
-    private boolean isInQsArea(float x, float y) {
-        if (isSplitShadeAndTouchXOutsideQs(x)) {
-            return false;
-        }
-        // Let's reject anything at the very bottom around the home handle in gesture nav
-        if (mIsGestureNavigation && y > mView.getHeight() - mNavigationBarBottomHeight) {
-            return false;
-        }
-        return y <= mNotificationStackScrollLayoutController.getBottomMostNotificationBottom()
-                || y <= mQs.getView().getY() + mQs.getView().getHeight();
-    }
-
-    private boolean isOpenQsEvent(MotionEvent event) {
-        final int pointerCount = event.getPointerCount();
-        final int action = event.getActionMasked();
-
-        final boolean
-                twoFingerDrag =
-                action == MotionEvent.ACTION_POINTER_DOWN && pointerCount == 2;
-
-        final boolean
-                stylusButtonClickDrag =
-                action == MotionEvent.ACTION_DOWN && (event.isButtonPressed(
-                        MotionEvent.BUTTON_STYLUS_PRIMARY) || event.isButtonPressed(
-                        MotionEvent.BUTTON_STYLUS_SECONDARY));
-
-        final boolean
-                mouseButtonClickDrag =
-                action == MotionEvent.ACTION_DOWN && (event.isButtonPressed(
-                        MotionEvent.BUTTON_SECONDARY) || event.isButtonPressed(
-                        MotionEvent.BUTTON_TERTIARY));
-
-        return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag;
-    }
-
-    private void handleQsDown(MotionEvent event) {
-        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && shouldQuickSettingsIntercept(
-                event.getX(), event.getY(), -1)) {
-            debugLog("handleQsDown");
-            mFalsingCollector.onQsDown();
-            mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled");
-            mQsTracking = true;
-            onQsExpansionStarted();
-            mInitialHeightOnTouch = mQsExpansionHeight;
-            mInitialTouchY = event.getY();
-            mInitialTouchX = event.getX();
-
-            // If we interrupt an expansion gesture here, make sure to update the state correctly.
-            notifyExpandingFinished();
-        }
+    /** Return whether a touch is near the gesture handle at the bottom of screen */
+    public boolean isInGestureNavHomeHandleArea(float x, float y) {
+        return mIsGestureNavigation && y > mView.getHeight() - mNavigationBarBottomHeight;
     }
 
     /** Input focus transfer is about to happen. */
@@ -2572,7 +2127,7 @@
         }
 
         // If we are already running a QS expansion, make sure that we keep the panel open.
-        if (mQsExpansionAnimator != null) {
+        if (mQsController.isExpansionAnimating()) {
             expands = true;
         }
         return expands;
@@ -2586,124 +2141,9 @@
         return isFullyCollapsed() || mBarState != StatusBarState.SHADE;
     }
 
-    private void onQsTouch(MotionEvent event) {
-        int pointerIndex = event.findPointerIndex(mQsTrackingPointer);
-        if (pointerIndex < 0) {
-            pointerIndex = 0;
-            mQsTrackingPointer = event.getPointerId(pointerIndex);
-        }
-        final float y = event.getY(pointerIndex);
-        final float x = event.getX(pointerIndex);
-        final float h = y - mInitialTouchY;
-
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN:
-                mShadeLog.logMotionEvent(event, "onQsTouch: down action, QS tracking enabled");
-                mQsTracking = true;
-                traceQsJank(true /* startTracing */, false /* wasCancelled */);
-                mInitialTouchY = y;
-                mInitialTouchX = x;
-                onQsExpansionStarted();
-                mInitialHeightOnTouch = mQsExpansionHeight;
-                initVelocityTracker();
-                trackMovement(event);
-                break;
-
-            case MotionEvent.ACTION_POINTER_UP:
-                final int upPointer = event.getPointerId(event.getActionIndex());
-                if (mQsTrackingPointer == upPointer) {
-                    // gesture is ongoing, find a new pointer to track
-                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
-                    final float newY = event.getY(newIndex);
-                    final float newX = event.getX(newIndex);
-                    mQsTrackingPointer = event.getPointerId(newIndex);
-                    mInitialHeightOnTouch = mQsExpansionHeight;
-                    mInitialTouchY = newY;
-                    mInitialTouchX = newX;
-                }
-                break;
-
-            case MotionEvent.ACTION_MOVE:
-                debugLog("onQSTouch move");
-                mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion");
-                setQsExpansionHeight(h + mInitialHeightOnTouch);
-                if (h >= getFalsingThreshold()) {
-                    mQsTouchAboveFalsingThreshold = true;
-                }
-                trackMovement(event);
-                break;
-
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                mShadeLog.logMotionEvent(event,
-                        "onQsTouch: up/cancel action, QS tracking disabled");
-                mQsTracking = false;
-                mQsTrackingPointer = -1;
-                trackMovement(event);
-                float fraction = computeQsExpansionFraction();
-                if (fraction != 0f || y >= mInitialTouchY) {
-                    flingQsWithCurrentVelocity(y,
-                            event.getActionMasked() == MotionEvent.ACTION_CANCEL);
-                } else {
-                    traceQsJank(false /* startTracing */,
-                            event.getActionMasked() == MotionEvent.ACTION_CANCEL);
-                }
-                if (mQsVelocityTracker != null) {
-                    mQsVelocityTracker.recycle();
-                    mQsVelocityTracker = null;
-                }
-                break;
-        }
-    }
-
-    private int getFalsingThreshold() {
+    int getFalsingThreshold() {
         float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
-        return (int) (mQsFalsingThreshold * factor);
-    }
-
-    private void setOverScrolling(boolean overscrolling) {
-        mStackScrollerOverscrolling = overscrolling;
-        if (mQs == null) return;
-        mQs.setOverscrolling(overscrolling);
-    }
-
-    private void onQsExpansionStarted() {
-        cancelQsAnimation();
-        cancelHeightAnimator();
-
-        // Reset scroll position and apply that position to the expanded height.
-        float height = mQsExpansionHeight;
-        setQsExpansionHeight(height);
-        mNotificationStackScrollLayoutController.checkSnoozeLeavebehind();
-
-        // When expanding QS, let's authenticate the user if possible,
-        // this will speed up notification actions.
-        if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) {
-            mUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.QS_EXPANDED);
-        }
-    }
-
-    @VisibleForTesting
-    void setQsExpanded(boolean expanded) {
-        boolean changed = mQsExpanded != expanded;
-        if (changed) {
-            mQsExpanded = expanded;
-            updateQsState();
-            updateExpandedHeightToMaxHeight();
-            setStatusAccessibilityImportance(expanded
-                    ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                    : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-            updateSystemUiStateFlags();
-            NavigationBarView navigationBarView =
-                    mNavigationBarController.getNavigationBarView(mDisplayId);
-            if (navigationBarView != null) {
-                navigationBarView.onStatusBarPanelStateChanged();
-            }
-            mShadeExpansionStateManager.onQsExpansionChanged(expanded);
-            mShadeLog.logQsExpansionChanged("QS Expansion Changed.", expanded,
-                    mQsMinExpansionHeight, mQsMaxExpansionHeight, mStackScrollerOverscrolling,
-                    mDozing, mQsAnimatorExpand, mAnimatingQS);
-        }
+        return (int) (mQsController.getFalsingThreshold() * factor);
     }
 
     private void maybeAnimateBottomAreaAlpha() {
@@ -2727,7 +2167,7 @@
         } else if (statusBarState == KEYGUARD
                 || statusBarState == StatusBarState.SHADE_LOCKED) {
             mKeyguardBottomArea.setVisibility(View.VISIBLE);
-            if (!mIsToLockscreenTransitionRunning) {
+            if (!mIsOcclusionTransitionRunning) {
                 mKeyguardBottomArea.setAlpha(1f);
             }
         } else {
@@ -2735,386 +2175,29 @@
         }
     }
 
-    private void updateQsState() {
-        boolean qsFullScreen = mQsExpanded && !mSplitShadeEnabled;
-        mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
-        mNotificationStackScrollLayoutController.setScrollingEnabled(
-                mBarState != KEYGUARD && (!qsFullScreen || mQsExpansionFromOverscroll));
-
-        if (mKeyguardUserSwitcherController != null && mQsExpanded
-                && !mStackScrollerOverscrolling) {
-            mKeyguardUserSwitcherController.closeSwitcherIfOpenAndNotSimple(true);
-        }
-        if (mQs == null) return;
-        mQs.setExpanded(mQsExpanded);
-    }
-
-    void setQsExpansionHeight(float height) {
-        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
-        mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0;
-        boolean qsAnimatingAway = !mQsAnimatorExpand && mAnimatingQS;
-        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling
-                && !mDozing && !qsAnimatingAway) {
-            setQsExpanded(true);
-        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
-            setQsExpanded(false);
-        }
-        mQsExpansionHeight = height;
-        updateQsExpansion();
-        requestScrollerTopPaddingUpdate(false /* animate */);
-        mKeyguardStatusBarViewController.updateViewState();
-        if (mBarState == StatusBarState.SHADE_LOCKED || mBarState == KEYGUARD) {
-            updateKeyguardBottomAreaAlpha();
-            positionClockAndNotifications();
-        }
-
-        if (mAccessibilityManager.isEnabled()) {
-            mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
-        }
-
-        if (!mFalsingManager.isUnlockingDisabled() && mQsFullyExpanded
-                && mFalsingCollector.shouldEnforceBouncer()) {
-            mCentralSurfaces.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
-                    false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
-        }
-        if (DEBUG_DRAWABLE) {
-            mView.invalidate();
-        }
-    }
-
-    private void updateQsExpansion() {
-        if (mQs == null) return;
-        final float squishiness;
-        if ((mQsExpandImmediate || mQsExpanded) && !mSplitShadeEnabled) {
-            squishiness = 1;
-        } else if (mTransitioningToFullShadeProgress > 0.0f) {
-            squishiness = mLockscreenShadeTransitionController.getQsSquishTransitionFraction();
-        } else {
-            squishiness = mNotificationStackScrollLayoutController
-                    .getNotificationSquishinessFraction();
-        }
-        final float qsExpansionFraction = computeQsExpansionFraction();
-        final float adjustedExpansionFraction = mSplitShadeEnabled
-                ? 1f : computeQsExpansionFraction();
-        mQs.setQsExpansion(adjustedExpansionFraction, getExpandedFraction(), getHeaderTranslation(),
-                squishiness);
-        mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
-        int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
-        mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
-        setQSClippingBounds();
-
-        if (mSplitShadeEnabled) {
-            // In split shade we want to pretend that QS are always collapsed so their behaviour and
-            // interactions don't influence notifications as they do in portrait. But we want to set
-            // 0 explicitly in case we're rotating from non-split shade with QS expansion of 1.
-            mNotificationStackScrollLayoutController.setQsExpansionFraction(0);
-        } else {
-            mNotificationStackScrollLayoutController.setQsExpansionFraction(qsExpansionFraction);
-        }
-
-        mDepthController.setQsPanelExpansion(qsExpansionFraction);
-        mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction);
-
-        float shadeExpandedFraction = isOnKeyguard()
-                ? getLockscreenShadeDragProgress()
-                : getExpandedFraction();
-        mLargeScreenShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction);
-        mLargeScreenShadeHeaderController.setQsExpandedFraction(qsExpansionFraction);
-        mLargeScreenShadeHeaderController.setQsVisible(mQsVisible);
-    }
-
-    private float getLockscreenShadeDragProgress() {
+    /** */
+    public float getLockscreenShadeDragProgress() {
         // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade
         // transition. If that's not the case we should follow QS expansion fraction for when
         // user is pulling from the same top to go directly to expanded QS
-        return mTransitioningToFullShadeProgress > 0
+        return mQsController.getTransitioningToFullShadeProgress() > 0
                 ? mLockscreenShadeTransitionController.getQSDragProgress()
-                : computeQsExpansionFraction();
+                : mQsController.computeExpansionFraction();
     }
 
-    private void onStackYChanged(boolean shouldAnimate) {
-        if (mQs != null) {
-            if (shouldAnimate) {
-                animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_STANDARD,
-                        0 /* delay */);
-                mNotificationBoundsAnimationDelay = 0;
-            }
-            setQSClippingBounds();
-        }
-    }
-
-    private void onNotificationScrolled(int newScrollPosition) {
-        updateQSExpansionEnabledAmbient();
-    }
-
-    private void updateQSExpansionEnabledAmbient() {
-        final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsHeaderHeight;
-        mQsExpansionEnabledAmbient = mSplitShadeEnabled
-                || (mAmbientState.getScrollY() <= scrollRangeToTop);
-        setQsExpansionEnabled();
-    }
-
-    /**
-     * Updates scrim bounds, QS clipping, notifications clipping and keyguard status view clipping
-     * as well based on the bounds of the shade and QS state.
-     */
-    private void setQSClippingBounds() {
-        float qsExpansionFraction = computeQsExpansionFraction();
-        final int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
-        final boolean qsVisible = (qsExpansionFraction > 0 || qsPanelBottomY > 0);
-        checkCorrectScrimVisibility(qsExpansionFraction);
-
-        int top = calculateTopQsClippingBound(qsPanelBottomY);
-        int bottom = calculateBottomQsClippingBound(top);
-        int left = calculateLeftQsClippingBound();
-        int right = calculateRightQsClippingBound();
-        // top should never be lower than bottom, otherwise it will be invisible.
-        top = Math.min(top, bottom);
-        applyQSClippingBounds(left, top, right, bottom, qsVisible);
-    }
-
-    private void checkCorrectScrimVisibility(float expansionFraction) {
-        // issues with scrims visible on keyguard occur only in split shade
-        if (mSplitShadeEnabled) {
-            boolean keyguardViewsVisible = mBarState == KEYGUARD && mKeyguardOnlyContentAlpha == 1;
-            // expansionFraction == 1 means scrims are fully visible as their size/visibility depend
-            // on QS expansion
-            if (expansionFraction == 1 && keyguardViewsVisible) {
-                Log.wtf(TAG,
-                        "Incorrect state, scrim is visible at the same time when clock is visible");
-            }
-        }
-    }
-
-    private int calculateTopQsClippingBound(int qsPanelBottomY) {
-        int top;
-        if (mSplitShadeEnabled) {
-            top = Math.min(qsPanelBottomY, mLargeScreenShadeHeaderHeight);
-        } else {
-            if (mTransitioningToFullShadeProgress > 0.0f) {
-                // If we're transitioning, let's use the actual value. The else case
-                // can be wrong during transitions when waiting for the keyguard to unlock
-                top = mTransitionToFullShadeQSPosition;
-            } else {
-                final float notificationTop = getQSEdgePosition();
-                if (isOnKeyguard()) {
-                    if (mKeyguardBypassController.getBypassEnabled()) {
-                        // When bypassing on the keyguard, let's use the panel bottom.
-                        // this should go away once we unify the stackY position and don't have
-                        // to do this min anymore below.
-                        top = qsPanelBottomY;
-                    } else {
-                        top = (int) Math.min(qsPanelBottomY, notificationTop);
-                    }
-                } else {
-                    top = (int) notificationTop;
-                }
-            }
-            top += mOverStretchAmount;
-            // Correction for instant expansion caused by HUN pull down/
-            if (mMinFraction > 0f && mMinFraction < 1f) {
-                float realFraction =
-                        (getExpandedFraction() - mMinFraction) / (1f - mMinFraction);
-                top *= MathUtils.saturate(realFraction / mMinFraction);
-            }
-        }
-        return top;
-    }
-
-    private int calculateBottomQsClippingBound(int top) {
-        if (mSplitShadeEnabled) {
-            return top + mNotificationStackScrollLayoutController.getHeight()
-                    + mSplitShadeNotificationsScrimMarginBottom;
-        } else {
-            return mView.getBottom();
-        }
-    }
-
-    private int calculateLeftQsClippingBound() {
-        if (mIsFullWidth) {
-            // left bounds can ignore insets, it should always reach the edge of the screen
-            return 0;
-        } else {
-            return mNotificationStackScrollLayoutController.getLeft();
-        }
-    }
-
-    private int calculateRightQsClippingBound() {
-        if (mIsFullWidth) {
-            return mView.getRight() + mDisplayRightInset;
-        } else {
-            return mNotificationStackScrollLayoutController.getRight();
-        }
-    }
-
-    /**
-     * Applies clipping to quick settings, notifications layout and
-     * updates bounds of the notifications background (notifications scrim).
-     *
-     * The parameters are bounds of the notifications area rectangle, this function
-     * calculates bounds for the QS clipping based on the notifications bounds.
-     */
-    private void applyQSClippingBounds(int left, int top, int right, int bottom,
-            boolean qsVisible) {
-        if (!mAnimateNextNotificationBounds || mLastQsClipBounds.isEmpty()) {
-            if (mQsClippingAnimation != null) {
-                // update the end position of the animator
-                mQsClippingAnimationEndBounds.set(left, top, right, bottom);
-            } else {
-                applyQSClippingImmediately(left, top, right, bottom, qsVisible);
-            }
-        } else {
-            mQsClippingAnimationEndBounds.set(left, top, right, bottom);
-            final int startLeft = mLastQsClipBounds.left;
-            final int startTop = mLastQsClipBounds.top;
-            final int startRight = mLastQsClipBounds.right;
-            final int startBottom = mLastQsClipBounds.bottom;
-            if (mQsClippingAnimation != null) {
-                mQsClippingAnimation.cancel();
-            }
-            mQsClippingAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
-            mQsClippingAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mQsClippingAnimation.setDuration(mNotificationBoundsAnimationDuration);
-            mQsClippingAnimation.setStartDelay(mNotificationBoundsAnimationDelay);
-            mQsClippingAnimation.addUpdateListener(animation -> {
-                float fraction = animation.getAnimatedFraction();
-                int animLeft = (int) MathUtils.lerp(startLeft,
-                        mQsClippingAnimationEndBounds.left, fraction);
-                int animTop = (int) MathUtils.lerp(startTop,
-                        mQsClippingAnimationEndBounds.top, fraction);
-                int animRight = (int) MathUtils.lerp(startRight,
-                        mQsClippingAnimationEndBounds.right, fraction);
-                int animBottom = (int) MathUtils.lerp(startBottom,
-                        mQsClippingAnimationEndBounds.bottom, fraction);
-                applyQSClippingImmediately(animLeft, animTop, animRight, animBottom,
-                        qsVisible /* qsVisible */);
-            });
-            mQsClippingAnimation.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mQsClippingAnimation = null;
-                    mIsQsTranslationResetAnimator = false;
-                    mIsPulseExpansionResetAnimator = false;
-                }
-            });
-            mQsClippingAnimation.start();
-        }
-        mAnimateNextNotificationBounds = false;
-        mNotificationBoundsAnimationDelay = 0;
-    }
-
-    private void applyQSClippingImmediately(int left, int top, int right, int bottom,
-            boolean qsVisible) {
-        int radius = mScrimCornerRadius;
-        boolean clipStatusView = false;
-        mLastQsClipBounds.set(left, top, right, bottom);
-        if (mIsFullWidth) {
-            clipStatusView = qsVisible;
-            float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius;
-            radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius,
-                    Math.min(top / (float) mScrimCornerRadius, 1f));
-        }
-        if (mQs != null) {
-            float qsTranslation = 0;
-            boolean pulseExpanding = mPulseExpansionHandler.isExpanding();
-            if (mTransitioningToFullShadeProgress > 0.0f || pulseExpanding
-                    || (mQsClippingAnimation != null
-                    && (mIsQsTranslationResetAnimator || mIsPulseExpansionResetAnimator))) {
-                if (pulseExpanding || mIsPulseExpansionResetAnimator) {
-                    // qsTranslation should only be positive during pulse expansion because it's
-                    // already translating in from the top
-                    qsTranslation = Math.max(0, (top - mQs.getHeader().getHeight()) / 2.0f);
-                } else if (!mSplitShadeEnabled) {
-                    qsTranslation = (top - mQs.getHeader().getHeight()) * QS_PARALLAX_AMOUNT;
-                }
-            }
-            mQsTranslationForFullShadeTransition = qsTranslation;
-            updateQsFrameTranslation();
-            float currentTranslation = mQsFrame.getTranslationY();
-            mQsClipTop = mEnableQsClipping
-                    ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
-            mQsClipBottom = mEnableQsClipping
-                    ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
-            mQsVisible = qsVisible;
-            mQs.setQsVisible(mQsVisible);
-            mQs.setFancyClipping(
-                    mQsClipTop,
-                    mQsClipBottom,
-                    radius,
-                    qsVisible && !mSplitShadeEnabled);
-        }
-        // The padding on this area is large enough that we can use a cheaper clipping strategy
-        mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
-        if (!qsVisible && mSplitShadeEnabled) {
-            // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
-            // be visible, otherwise you can see the bounds once swiping up to see bouncer
-            mScrimController.setNotificationsBounds(0, 0, 0, 0);
-        } else {
-            // Increase the height of the notifications scrim when not in split shade
-            // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
-            // in this case they are rendered off-screen
-            final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
-            mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
-        }
-
-        if (mSplitShadeEnabled) {
-            mKeyguardStatusBarViewController.setNoTopClipping();
-        } else {
-            mKeyguardStatusBarViewController.updateTopClipping(top);
-        }
-
-        mScrimController.setScrimCornerRadius(radius);
-
-        // Convert global clipping coordinates to local ones,
-        // relative to NotificationStackScrollLayout
-        int nsslLeft = left - mNotificationStackScrollLayoutController.getLeft();
-        int nsslRight = right - mNotificationStackScrollLayoutController.getLeft();
-        int nsslTop = getNotificationsClippingTopBounds(top);
-        int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
-        int bottomRadius = mSplitShadeEnabled ? radius : 0;
-        int topRadius = mSplitShadeEnabled && mExpandingFromHeadsUp ? 0 : radius;
-        mNotificationStackScrollLayoutController.setRoundedClippingBounds(
-                nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
-    }
-
-    private int getNotificationsClippingTopBounds(int qsTop) {
-        if (mSplitShadeEnabled && mExpandingFromHeadsUp) {
-            // in split shade nssl has extra top margin so clipping at top 0 is not enough, we need
-            // to set top clipping bound to negative value to allow HUN to go up to the top edge of
-            // the screen without clipping.
-            return -mAmbientState.getStackTopMargin();
-        } else {
-            return qsTop - mNotificationStackScrollLayoutController.getTop();
-        }
-    }
-
-    private float getQSEdgePosition() {
-        // TODO: replace StackY with unified calculation
-        return Math.max(mQuickQsHeaderHeight * mAmbientState.getExpansionFraction(),
-                mAmbientState.getStackY()
-                        // need to adjust for extra margin introduced by large screen shade header
-                        + mAmbientState.getStackTopMargin() * mAmbientState.getExpansionFraction()
-                        - mAmbientState.getScrollY());
-    }
-
-    private int calculateQsBottomPosition(float qsExpansionFraction) {
-        if (mTransitioningToFullShadeProgress > 0.0f) {
-            return mTransitionToFullShadeQSPosition;
-        } else {
-            int qsBottomYFrom = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
-            int expandedTopMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : 0;
-            int qsBottomYTo = mQs.getDesiredHeight() + expandedTopMargin;
-            return (int) MathUtils.lerp(qsBottomYFrom, qsBottomYTo, qsExpansionFraction);
-        }
-    }
-
-    private String determineAccessibilityPaneTitle() {
-        if (mQs != null && mQs.isCustomizing()) {
+    String determineAccessibilityPaneTitle() {
+        if (mQsController != null && mQsController.isCustomizing()) {
             return mResources.getString(R.string.accessibility_desc_quick_settings_edit);
-        } else if (mQsExpansionHeight != 0.0f && mQsFullyExpanded) {
+        } else if (mQsController != null && mQsController.getExpansionHeight() != 0.0f
+                && mQsController.getFullyExpanded()) {
             // Upon initialisation when we are not layouted yet we don't want to announce that we
             // are fully expanded, hence the != 0.0f check.
-            return mResources.getString(R.string.accessibility_desc_quick_settings);
+            if (mSplitShadeEnabled) {
+                // In split shade, QS is expanded but it also shows notifications
+                return mResources.getString(R.string.accessibility_desc_qs_notification_shade);
+            } else {
+                return mResources.getString(R.string.accessibility_desc_quick_settings);
+            }
         } else if (mBarState == KEYGUARD) {
             return mResources.getString(R.string.accessibility_desc_lock_screen);
         } else {
@@ -3122,42 +2205,27 @@
         }
     }
 
-    float calculateNotificationsTopPadding() {
-        if (mSplitShadeEnabled) {
-            return mKeyguardShowing ? getKeyguardNotificationStaticPadding() : 0;
+    /** Returns the topPadding of notifications when on keyguard not respecting QS expansion. */
+    public int getKeyguardNotificationStaticPadding() {
+        if (!getKeyguardShowing()) {
+            return 0;
         }
-        if (mKeyguardShowing && (mQsExpandImmediate
-                || mIsExpanding && mQsExpandedWhenExpandingStarted)) {
-
-            // Either QS pushes the notifications down when fully expanded, or QS is fully above the
-            // notifications (mostly on tablets). maxNotificationPadding denotes the normal top
-            // padding on Keyguard, maxQsPadding denotes the top padding from the quick settings
-            // panel. We need to take the maximum and linearly interpolate with the panel expansion
-            // for a nice motion.
-            int maxNotificationPadding = getKeyguardNotificationStaticPadding();
-            int maxQsPadding = mQsMaxExpansionHeight;
-            int max = mBarState == KEYGUARD ? Math.max(
-                    maxNotificationPadding, maxQsPadding) : maxQsPadding;
-            return (int) MathUtils.lerp((float) mQsMinExpansionHeight, (float) max,
-                    getExpandedFraction());
-        } else if (mQsSizeChangeAnimator != null) {
-            return Math.max(
-                    (int) mQsSizeChangeAnimator.getAnimatedValue(),
-                    getKeyguardNotificationStaticPadding());
-        } else if (mKeyguardShowing) {
-            // We can only do the smoother transition on Keyguard when we also are not collapsing
-            // from a scrolled quick settings.
-            return MathUtils.lerp((float) getKeyguardNotificationStaticPadding(),
-                    (float) (mQsMaxExpansionHeight),
-                    computeQsExpansionFraction());
+        if (!mKeyguardBypassController.getBypassEnabled()) {
+            return mClockPositionResult.stackScrollerPadding;
+        }
+        int collapsedPosition = mHeadsUpInset;
+        if (!mNotificationStackScrollLayoutController.isPulseExpanding()) {
+            return collapsedPosition;
         } else {
-            return mQsFrameTranslateController.getNotificationsTopPadding(mQsExpansionHeight,
-                    mNotificationStackScrollLayoutController);
+            int expandedPosition =
+                    mClockPositionResult.stackScrollerPadding;
+            return (int) MathUtils.lerp(collapsedPosition, expandedPosition,
+                    mNotificationStackScrollLayoutController.calculateAppearFractionBypass());
         }
     }
 
     public boolean getKeyguardShowing() {
-        return mKeyguardShowing;
+        return mBarState == KEYGUARD;
     }
 
     public float getKeyguardNotificationTopPadding() {
@@ -3168,94 +2236,18 @@
         return mKeyguardNotificationBottomPadding;
     }
 
-    /** Returns the topPadding of notifications when on keyguard not respecting QS expansion. */
-    private int getKeyguardNotificationStaticPadding() {
-        if (!mKeyguardShowing) {
-            return 0;
-        }
-        if (!mKeyguardBypassController.getBypassEnabled()) {
-            return mClockPositionResult.stackScrollerPadding;
-        }
-        int collapsedPosition = mHeadsUpInset;
-        if (!mNotificationStackScrollLayoutController.isPulseExpanding()) {
-            return collapsedPosition;
-        } else {
-            int expandedPosition = mClockPositionResult.stackScrollerPadding;
-            return (int) MathUtils.lerp(collapsedPosition, expandedPosition,
-                    mNotificationStackScrollLayoutController.calculateAppearFractionBypass());
-        }
-    }
-
-    private void requestScrollerTopPaddingUpdate(boolean animate) {
+    void requestScrollerTopPaddingUpdate(boolean animate) {
         mNotificationStackScrollLayoutController.updateTopPadding(
-                calculateNotificationsTopPadding(), animate);
-        if (mKeyguardShowing && mKeyguardBypassController.getBypassEnabled()) {
+                mQsController.calculateNotificationsTopPadding(mIsExpanding,
+                        getKeyguardNotificationStaticPadding(), mExpandedFraction), animate);
+        if (getKeyguardShowing()
+                && mKeyguardBypassController.getBypassEnabled()) {
             // update the position of the header
-            updateQsExpansion();
+            mQsController.updateExpansion();
         }
     }
 
     /**
-     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
-     * shade. 0.0f means we're not transitioning yet.
-     */
-    public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) {
-        if (animate && mIsFullWidth) {
-            animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE,
-                    delay);
-            mIsQsTranslationResetAnimator = mQsTranslationForFullShadeTransition > 0.0f;
-        }
-        float endPosition = 0;
-        if (pxAmount > 0.0f) {
-            if (mSplitShadeEnabled) {
-                float qsHeight = MathUtils.lerp(mQsMinExpansionHeight, mQsMaxExpansionHeight,
-                        mLockscreenShadeTransitionController.getQSDragProgress());
-                setQsExpansionHeight(qsHeight);
-            }
-            if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0
-                    && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
-                // No notifications are visible, let's animate to the height of qs instead
-                if (mQs != null) {
-                    // Let's interpolate to the header height instead of the top padding,
-                    // because the toppadding is way too low because of the large clock.
-                    // we still want to take into account the edgePosition though as that nicely
-                    // overshoots in the stackscroller
-                    endPosition = getQSEdgePosition()
-                            - mNotificationStackScrollLayoutController.getTopPadding()
-                            + mQs.getHeader().getHeight();
-                }
-            } else {
-                // Interpolating to the new bottom edge position!
-                endPosition = getQSEdgePosition()
-                        + mNotificationStackScrollLayoutController.getFullShadeTransitionInset();
-                if (isOnKeyguard()) {
-                    endPosition -= mLockscreenNotificationQSPadding;
-                }
-            }
-        }
-
-        // Calculate the overshoot amount such that we're reaching the target after our desired
-        // distance, but only reach it fully once we drag a full shade length.
-        mTransitioningToFullShadeProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
-                MathUtils.saturate(pxAmount / mDistanceForQSFullShadeTransition));
-
-        int position = (int) MathUtils.lerp((float) 0, endPosition,
-                mTransitioningToFullShadeProgress);
-        if (mTransitioningToFullShadeProgress > 0.0f) {
-            // we want at least 1 pixel otherwise the panel won't be clipped
-            position = Math.max(1, position);
-        }
-        mTransitionToFullShadeQSPosition = position;
-        updateQsExpansion();
-    }
-
-    /** Called when pulse expansion has finished and this is going to the full shade. */
-    public void onPulseExpansionFinished() {
-        animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 0);
-        mIsPulseExpansionResetAnimator = true;
-    }
-
-    /**
      * Set the alpha and translationY of the keyguard elements which only show on the lockscreen,
      * but not in shade locked / shade. This is used when dragging down to the full shade.
      */
@@ -3279,154 +2271,14 @@
         mKeyguardStatusBarViewController.setAlpha(alpha);
     }
 
-    private void trackMovement(MotionEvent event) {
-        if (mQsVelocityTracker != null) mQsVelocityTracker.addMovement(event);
-    }
-
-    private void initVelocityTracker() {
-        if (mQsVelocityTracker != null) {
-            mQsVelocityTracker.recycle();
-        }
-        mQsVelocityTracker = VelocityTracker.obtain();
-    }
-
-    private float getCurrentQSVelocity() {
-        if (mQsVelocityTracker == null) {
-            return 0;
-        }
-        mQsVelocityTracker.computeCurrentVelocity(1000);
-        return mQsVelocityTracker.getYVelocity();
-    }
-
-    private void cancelQsAnimation() {
-        if (mQsExpansionAnimator != null) {
-            mQsExpansionAnimator.cancel();
-        }
-    }
-
-    /** @see #flingSettings(float, int, Runnable, boolean) */
-    public void flingSettings(float vel, int type) {
-        flingSettings(vel, type, null /* onFinishRunnable */, false /* isClick */);
-    }
-
-    /**
-     * Animates QS or QQS as if the user had swiped up or down.
-     *
-     * @param vel              Finger velocity or 0 when not initiated by touch events.
-     * @param type             Either {@link #FLING_EXPAND}, {@link #FLING_COLLAPSE} or {@link
-     *                         #FLING_HIDE}.
-     * @param onFinishRunnable Runnable to be executed at the end of animation.
-     * @param isClick          If originated by click (different interpolator and duration.)
-     */
-    private void flingSettings(float vel, int type, final Runnable onFinishRunnable,
-            boolean isClick) {
-        float target;
-        switch (type) {
-            case FLING_EXPAND:
-                target = mQsMaxExpansionHeight;
-                break;
-            case FLING_COLLAPSE:
-                target = mQsMinExpansionHeight;
-                break;
-            case FLING_HIDE:
-            default:
-                if (mQs != null) {
-                    mQs.closeDetail();
-                }
-                target = 0;
-        }
-        if (target == mQsExpansionHeight) {
-            if (onFinishRunnable != null) {
-                onFinishRunnable.run();
-            }
-            traceQsJank(false /* startTracing */, type != FLING_EXPAND /* wasCancelled */);
-            return;
-        }
-
-        // If we move in the opposite direction, reset velocity and use a different duration.
-        boolean oppositeDirection = false;
-        boolean expanding = type == FLING_EXPAND;
-        if (vel > 0 && !expanding || vel < 0 && expanding) {
-            vel = 0;
-            oppositeDirection = true;
-        }
-        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
-        if (isClick) {
-            animator.setInterpolator(Interpolators.TOUCH_RESPONSE);
-            animator.setDuration(368);
-        } else {
-            mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
-        }
-        if (oppositeDirection) {
-            animator.setDuration(350);
-        }
-        animator.addUpdateListener(
-                animation -> setQsExpansionHeight((Float) animation.getAnimatedValue()));
-        animator.addListener(new AnimatorListenerAdapter() {
-            private boolean mIsCanceled;
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                notifyExpandingStarted();
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mIsCanceled = true;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mQSAnimatingHiddenFromCollapsed = false;
-                mAnimatingQS = false;
-                notifyExpandingFinished();
-                mNotificationStackScrollLayoutController.resetCheckSnoozeLeavebehind();
-                mQsExpansionAnimator = null;
-                if (onFinishRunnable != null) {
-                    onFinishRunnable.run();
-                }
-                traceQsJank(false /* startTracing */, mIsCanceled /* wasCancelled */);
-            }
-        });
-        // Let's note that we're animating QS. Moving the animator here will cancel it immediately,
-        // so we need a separate flag.
-        mAnimatingQS = true;
-        animator.start();
-        mQsExpansionAnimator = animator;
-        mQsAnimatorExpand = expanding;
-        mQSAnimatingHiddenFromCollapsed = computeQsExpansionFraction() == 0.0f && target == 0;
-    }
-
-    /**
-     * @return Whether we should intercept a gesture to open Quick Settings.
-     */
-    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
-        if (!isQsExpansionEnabled() || mCollapsedOnDown
-                || (mKeyguardShowing && mKeyguardBypassController.getBypassEnabled())
-                || mSplitShadeEnabled) {
-            return false;
-        }
-        View header = mKeyguardShowing || mQs == null ? mKeyguardStatusBar : mQs.getHeader();
-        int frameTop = mKeyguardShowing || mQs == null ? 0 : mQsFrame.getTop();
-        mQsInterceptRegion.set(
-                /* left= */ (int) mQsFrame.getX(),
-                /* top= */ header.getTop() + frameTop,
-                /* right= */ (int) mQsFrame.getX() + mQsFrame.getWidth(),
-                /* bottom= */ header.getBottom() + frameTop);
-        // Also allow QS to intercept if the touch is near the notch.
-        mStatusBarTouchableRegionManager.updateRegionForNotch(mQsInterceptRegion);
-        final boolean onHeader = mQsInterceptRegion.contains((int) x, (int) y);
-
-        if (mQsExpanded) {
-            return onHeader || (yDiff < 0 && isInQsArea(x, y));
-        } else {
-            return onHeader;
-        }
+    /** */
+    public float getKeyguardOnlyContentAlpha() {
+        return mKeyguardOnlyContentAlpha;
     }
 
     @VisibleForTesting
     boolean canCollapsePanelOnTouch() {
-        if (!isInSettings() && mBarState == KEYGUARD) {
+        if (!mQsController.getExpanded() && mBarState == KEYGUARD) {
             return true;
         }
 
@@ -3434,20 +2286,22 @@
             return true;
         }
 
-        return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS);
+        return !mSplitShadeEnabled && (mQsController.getExpanded() || mIsPanelCollapseOnQQS);
     }
 
     int getMaxPanelHeight() {
         int min = mStatusBarMinHeight;
         if (!(mBarState == KEYGUARD)
                 && mNotificationStackScrollLayoutController.getNotGoneChildCount() == 0) {
-            int minHeight = mQsMinExpansionHeight;
+            int minHeight = mQsController.getMinExpansionHeight();
             min = Math.max(min, minHeight);
         }
         int maxHeight;
-        if (mQsExpandImmediate || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted
+        if (mQsController.isExpandImmediate() || mQsController.getExpanded()
+                || mIsExpanding && mQsController.getExpandedWhenExpandingStarted()
                 || mPulsing || mSplitShadeEnabled) {
-            maxHeight = calculatePanelHeightQsExpanded();
+            maxHeight = mQsController.calculatePanelHeightExpanded(
+                    mClockPositionResult.stackScrollerPadding);
         } else {
             maxHeight = calculatePanelHeightShade();
         }
@@ -3455,17 +2309,15 @@
         if (maxHeight == 0) {
             Log.wtf(TAG, "maxPanelHeight is invalid. mOverExpansion: "
                     + mOverExpansion + ", calculatePanelHeightQsExpanded: "
-                    + calculatePanelHeightQsExpanded() + ", calculatePanelHeightShade: "
-                    + calculatePanelHeightShade() + ", mStatusBarMinHeight = "
-                    + mStatusBarMinHeight + ", mQsMinExpansionHeight = " + mQsMinExpansionHeight);
+                    + mQsController.calculatePanelHeightExpanded(
+                            mClockPositionResult.stackScrollerPadding)
+                    + ", calculatePanelHeightShade: " + calculatePanelHeightShade()
+                    + ", mStatusBarMinHeight = " + mStatusBarMinHeight
+                    + ", mQsMinExpansionHeight = " + mQsController.getMinExpansionHeight());
         }
         return maxHeight;
     }
 
-    public boolean isInSettings() {
-        return mQsExpanded;
-    }
-
     public boolean isExpanding() {
         return mIsExpanding;
     }
@@ -3478,7 +2330,8 @@
             mShadeLog.logExpansionChanged("onHeightUpdated: fully expanded.",
                     mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
         }
-        if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
+        if (!mQsController.getExpanded() || mQsController.isExpandImmediate()
+                || mIsExpanding && mQsController.getExpandedWhenExpandingStarted()) {
             // Updating the clock position will set the top padding which might
             // trigger a new panel height and re-position the clock.
             // This is a circular dependency and should be avoided, otherwise we'll have
@@ -3489,14 +2342,8 @@
                 positionClockAndNotifications();
             }
         }
-        // Below is true when QS are expanded and we swipe up from the same bottom of panel to
-        // close the whole shade with one motion. Also this will be always true when closing
-        // split shade as there QS are always expanded so every collapsing motion is motion from
-        // expanded QS to closed panel
-        boolean collapsingShadeFromExpandedQs = mQsExpanded && !mQsTracking
-                && mQsExpansionAnimator == null && !mQsExpansionFromOverscroll;
         boolean goingBetweenClosedShadeAndExpandedQs =
-                mQsExpandImmediate || collapsingShadeFromExpandedQs;
+                mQsController.isGoingBetweenClosedShadeAndExpandedQs();
         // in split shade we react when HUN is visible only if shade height is over HUN start
         // height - which means user is swiping down. Otherwise shade QS will either not show at all
         // with HUN movement or it will blink when touching HUN initially
@@ -3506,7 +2353,7 @@
             float qsExpansionFraction;
             if (mSplitShadeEnabled) {
                 qsExpansionFraction = 1;
-            } else if (mKeyguardShowing) {
+            } else if (getKeyguardShowing()) {
                 // On Keyguard, interpolate the QS expansion linearly to the panel expansion
                 qsExpansionFraction = expandedHeight / (getMaxPanelHeight());
             } else {
@@ -3515,13 +2362,15 @@
                 float panelHeightQsCollapsed =
                         mNotificationStackScrollLayoutController.getIntrinsicPadding()
                                 + mNotificationStackScrollLayoutController.getLayoutMinHeight();
-                float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
+                float panelHeightQsExpanded = mQsController.calculatePanelHeightExpanded(
+                        mClockPositionResult.stackScrollerPadding);
                 qsExpansionFraction = (expandedHeight - panelHeightQsCollapsed)
                         / (panelHeightQsExpanded - panelHeightQsCollapsed);
             }
-            float targetHeight = mQsMinExpansionHeight
-                    + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
-            setQsExpansionHeight(targetHeight);
+            float targetHeight = mQsController.getMinExpansionHeight() + qsExpansionFraction
+                    * (mQsController.getMaxExpansionHeight()
+                    - mQsController.getMinExpansionHeight());
+            mQsController.setExpansionHeight(targetHeight);
         }
         updateExpandedHeight(expandedHeight);
         updateHeader();
@@ -3538,8 +2387,8 @@
         if (mPanelExpanded != isExpanded) {
             mPanelExpanded = isExpanded;
             mShadeExpansionStateManager.onShadeExpansionFullyChanged(isExpanded);
-            if (!isExpanded && mQs != null && mQs.isCustomizing()) {
-                mQs.closeCustomizer();
+            if (!isExpanded) {
+                mQsController.closeQsCustomizer();
             }
         }
     }
@@ -3561,42 +2410,8 @@
         }
     }
 
-    int calculatePanelHeightQsExpanded() {
-        float
-                notificationHeight =
-                mNotificationStackScrollLayoutController.getHeight()
-                        - mNotificationStackScrollLayoutController.getEmptyBottomMargin()
-                        - mNotificationStackScrollLayoutController.getTopPadding();
-
-        // When only empty shade view is visible in QS collapsed state, simulate that we would have
-        // it in expanded QS state as well so we don't run into troubles when fading the view in/out
-        // and expanding/collapsing the whole panel from/to quick settings.
-        if (mNotificationStackScrollLayoutController.getNotGoneChildCount() == 0
-                && mNotificationStackScrollLayoutController.isShowingEmptyShadeView()) {
-            notificationHeight = mNotificationStackScrollLayoutController.getEmptyShadeViewHeight();
-        }
-        int maxQsHeight = mQsMaxExpansionHeight;
-
-        // If an animation is changing the size of the QS panel, take the animated value.
-        if (mQsSizeChangeAnimator != null) {
-            maxQsHeight = (int) mQsSizeChangeAnimator.getAnimatedValue();
-        }
-        float totalHeight = Math.max(maxQsHeight,
-                mBarState == KEYGUARD ? mClockPositionResult.stackScrollerPadding
-                        : 0) + notificationHeight
-                + mNotificationStackScrollLayoutController.getTopPaddingOverflow();
-        if (totalHeight > mNotificationStackScrollLayoutController.getHeight()) {
-            float
-                    fullyCollapsedHeight =
-                    maxQsHeight + mNotificationStackScrollLayoutController.getLayoutMinHeight();
-            totalHeight = Math.max(fullyCollapsedHeight,
-                    mNotificationStackScrollLayoutController.getHeight());
-        }
-        return (int) totalHeight;
-    }
-
     private void updateNotificationTranslucency() {
-        if (mIsToLockscreenTransitionRunning) {
+        if (mIsOcclusionTransitionRunning) {
             return;
         }
         float alpha = 1f;
@@ -3613,10 +2428,10 @@
 
     private float getFadeoutAlpha() {
         float alpha;
-        if (mQsMinExpansionHeight == 0) {
+        if (mQsController.getMinExpansionHeight() == 0) {
             return 1.0f;
         }
-        alpha = getExpandedHeight() / mQsMinExpansionHeight;
+        alpha = getExpandedHeight() / mQsController.getMinExpansionHeight();
         alpha = Math.max(0, Math.min(alpha, 1));
         alpha = (float) Math.pow(alpha, 0.75);
         return alpha;
@@ -3627,34 +2442,11 @@
         if (mBarState == KEYGUARD) {
             mKeyguardStatusBarViewController.updateViewState();
         }
-        updateQsExpansion();
-    }
-
-    private float getHeaderTranslation() {
-        if (mSplitShadeEnabled) {
-            // in split shade QS don't translate, just (un)squish and overshoot
-            return 0;
-        }
-        if (mBarState == KEYGUARD && !mKeyguardBypassController.getBypassEnabled()) {
-            return -mQs.getQsMinExpansionHeight();
-        }
-        float appearAmount = mNotificationStackScrollLayoutController
-                .calculateAppearFraction(mExpandedHeight);
-        float startHeight = -mQsExpansionHeight;
-        if (mBarState == StatusBarState.SHADE) {
-            // Small parallax as we pull down and clip QS
-            startHeight = -mQsExpansionHeight * QS_PARALLAX_AMOUNT;
-        }
-        if (mKeyguardBypassController.getBypassEnabled() && isOnKeyguard()) {
-            appearAmount = mNotificationStackScrollLayoutController.calculateAppearFractionBypass();
-            startHeight = -mQs.getQsMinExpansionHeight();
-        }
-        float translation = MathUtils.lerp(startHeight, 0, Math.min(1.0f, appearAmount));
-        return Math.min(0, translation);
+        mQsController.updateExpansion();
     }
 
     private void updateKeyguardBottomAreaAlpha() {
-        if (mIsToLockscreenTransitionRunning) {
+        if (mIsOcclusionTransitionRunning) {
             return;
         }
         // There are two possible panel expansion behaviors:
@@ -3664,40 +2456,22 @@
         //   change due to "unlock hint animation." In this case, fading out the bottom area
         //   would also hide the message that says "swipe to unlock," we don't want to do that.
         float expansionAlpha = MathUtils.map(
-                isUnlockHintRunning() ? 0 : KeyguardBouncer.ALPHA_EXPANSION_THRESHOLD, 1f, 0f, 1f,
+                isUnlockHintRunning() ? 0 : KeyguardBouncerConstants.ALPHA_EXPANSION_THRESHOLD, 1f,
+                0f, 1f,
                 getExpandedFraction());
-        float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
+        float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction());
         alpha *= mBottomAreaShadeAlpha;
         mKeyguardBottomAreaInteractor.setAlpha(alpha);
         mLockIconViewController.setAlpha(alpha);
     }
 
-    private void onExpandingStarted() {
-        mNotificationStackScrollLayoutController.onExpansionStarted();
-        mIsExpanding = true;
-        mQsExpandedWhenExpandingStarted = mQsFullyExpanded;
-        mMediaHierarchyManager.setCollapsingShadeFromQS(mQsExpandedWhenExpandingStarted &&
-                /* We also start expanding when flinging closed Qs. Let's exclude that */
-                !mAnimatingQS);
-        if (mQsExpanded) {
-            onQsExpansionStarted();
-        }
-        // Since there are QS tiles in the header now, we need to make sure we start listening
-        // immediately so they can be up to date.
-        if (mQs == null) return;
-        mQs.setHeaderListening(true);
-    }
-
     private void onExpandingFinished() {
-        if (!mUnocclusionTransitionFlagEnabled) {
-            mScrimController.onExpandingFinished();
-        }
         mNotificationStackScrollLayoutController.onExpansionStopped();
         mHeadsUpManager.onExpandingFinished();
         mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
         mIsExpanding = false;
         mMediaHierarchyManager.setCollapsingShadeFromQS(false);
-        mMediaHierarchyManager.setQsExpanded(mQsExpanded);
+        mMediaHierarchyManager.setQsExpanded(mQsController.getExpanded());
         if (isFullyCollapsed()) {
             DejankUtils.postAfterTraversal(() -> setListening(false));
 
@@ -3712,10 +2486,10 @@
         if (mBarState != SHADE) {
             // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but
             // on keyguard panel state is always OPEN so we need to have that extra update
-            setQsExpandImmediate(false);
+            mQsController.setExpandImmediate(false);
         }
         setShowShelfOnly(false);
-        mTwoFingerQsExpandPossible = false;
+        mQsController.setTwoFingerExpandPossible(false);
         updateTrackingHeadsUp(null);
         mExpandingFromHeadsUp = false;
         setPanelScrimMinFraction(0.0f);
@@ -3738,8 +2512,7 @@
 
     private void setListening(boolean listening) {
         mKeyguardStatusBarViewController.setBatteryListening(listening);
-        if (mQs == null) return;
-        mQs.setListening(listening);
+        mQsController.setListening(listening);
     }
 
     public void expand(boolean animate) {
@@ -3773,7 +2546,7 @@
                                         this);
                                 if (mAnimateAfterExpanding) {
                                     notifyExpandingStarted();
-                                    beginJankMonitoring();
+                                    mQsController.beginJankMonitoring(isFullyCollapsed());
                                     fling(0  /* expand */);
                                 } else {
                                     mShadeHeightLogger.logFunctionCall("expand");
@@ -3800,18 +2573,17 @@
             return;
         }
         mOverExpansion = overExpansion;
-        // Translating the quick settings by half the overexpansion to center it in the background
-        // frame
-        updateQsFrameTranslation();
+        if (mSplitShadeEnabled) {
+            mQsController.setOverScrollAmount((int) overExpansion);
+            mScrimController.setNotificationsOverScrollAmount((int) overExpansion);
+        } else {
+            // Translating the quick settings by half the overexpansion to center it in the
+            // background frame
+            mQsController.updateQsFrameTranslation();
+        }
         mNotificationStackScrollLayoutController.setOverExpansion(overExpansion);
     }
 
-    private void updateQsFrameTranslation() {
-        mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs,
-                mNavigationBarBottomHeight + mAmbientState.getStackTopMargin());
-
-    }
-
     private void falsingAdditionalTapRequired() {
         if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
             mTapAgainViewController.show();
@@ -3838,8 +2610,8 @@
         notifyExpandingStarted();
         updatePanelExpansionAndVisibility();
         mScrimController.onTrackingStarted();
-        if (mQsFullyExpanded) {
-            setQsExpandImmediate(true);
+        if (mQsController.getFullyExpanded()) {
+            mQsController.setExpandImmediate(true);
             setShowShelfOnly(true);
         }
         mNotificationStackScrollLayoutController.onPanelTrackingStarted();
@@ -3916,8 +2688,8 @@
         // the required distance to be a specific and constant value, to make sure the expansion
         // motion has the expected speed. We also only want this on non-lockscreen for now.
         if (mSplitShadeEnabled && mBarState == SHADE) {
-            boolean transitionFromHeadsUp =
-                    mHeadsUpManager.isTrackingHeadsUp() || mExpandingFromHeadsUp;
+            boolean transitionFromHeadsUp = (mHeadsUpManager != null
+                    && mHeadsUpManager.isTrackingHeadsUp()) || mExpandingFromHeadsUp;
             // heads-up starting height is too close to mSplitShadeFullTransitionDistance and
             // when dragging HUN transition is already 90% complete. It makes shade become
             // immediately visible when starting to drag. We want to set distance so that
@@ -3935,25 +2707,6 @@
         }
     }
 
-    @VisibleForTesting
-    boolean isTrackingBlocked() {
-        return mConflictingQsExpansionGesture && mQsExpanded || mBlockingExpansionForCurrentTouch;
-    }
-
-    public boolean isQsExpanded() {
-        return mQsExpanded;
-    }
-
-    /** Returns whether the QS customizer is currently active. */
-    public boolean isQsCustomizing() {
-        return mQs.isCustomizing();
-    }
-
-    /** Close the QS customizer if it is open. */
-    public void closeQsCustomizer() {
-        mQs.closeCustomizer();
-    }
-
     public void setIsLaunchAnimationRunning(boolean running) {
         boolean wasRunning = mIsLaunchAnimationRunning;
         mIsLaunchAnimationRunning = running;
@@ -3978,14 +2731,6 @@
         }
     }
 
-    public void setQsScrimEnabled(boolean qsScrimEnabled) {
-        boolean changed = mQsScrimEnabled != qsScrimEnabled;
-        mQsScrimEnabled = qsScrimEnabled;
-        if (changed) {
-            updateQsState();
-        }
-    }
-
     public void onScreenTurningOn() {
         mKeyguardStatusViewController.dozeTimeTick();
     }
@@ -4002,7 +2747,7 @@
 
                     if (didFaceAuthRun) {
                         mUpdateMonitor.requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
                                 "lockScreenEmptySpaceTap");
                     } else {
                         mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
@@ -4014,7 +2759,7 @@
                 }
                 break;
             case StatusBarState.SHADE_LOCKED:
-                if (!mQsExpanded) {
+                if (!mQsController.getExpanded()) {
                     mStatusBarStateController.setState(KEYGUARD);
                 }
                 break;
@@ -4093,9 +2838,7 @@
     }
 
     private void updateStatusBarIcons() {
-        boolean showIconsWhenExpanded =
-                (isPanelVisibleBecauseOfHeadsUp() || mIsFullWidth)
-                        && getExpandedHeight() < getOpeningHeight();
+        boolean showIconsWhenExpanded = getExpandedHeight() < getOpeningHeight();
         if (showIconsWhenExpanded && isOnKeyguard()) {
             showIconsWhenExpanded = false;
         }
@@ -4162,67 +2905,7 @@
                 && mHeadsUpAppearanceController.shouldBeVisible()) {
             return false;
         }
-        return !mIsFullWidth || !mShowIconsWhenExpanded;
-    }
-
-    private void onQsPanelScrollChanged(int scrollY) {
-        mLargeScreenShadeHeaderController.setQsScrollY(scrollY);
-        if (scrollY > 0 && !mQsFullyExpanded) {
-            debugLog("Scrolling while not expanded. Forcing expand");
-            // If we are scrolling QS, we should be fully expanded.
-            expandWithQs();
-        }
-    }
-
-    private final class QsFragmentListener implements FragmentListener {
-        @Override
-        public void onFragmentViewCreated(String tag, Fragment fragment) {
-            mQs = (QS) fragment;
-            mQs.setPanelView(mHeightListener);
-            mQs.setCollapseExpandAction(mCollapseExpandAction);
-            mQs.setHeaderClickable(isQsExpansionEnabled());
-            mQs.setOverscrolling(mStackScrollerOverscrolling);
-            mQs.setInSplitShade(mSplitShadeEnabled);
-            mQs.setIsNotificationPanelFullWidth(mIsFullWidth);
-
-            // recompute internal state when qspanel height changes
-            mQs.getView().addOnLayoutChangeListener(
-                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                        final int height = bottom - top;
-                        final int oldHeight = oldBottom - oldTop;
-                        if (height != oldHeight) {
-                            onQsHeightChanged();
-                        }
-                    });
-            mQs.setCollapsedMediaVisibilityChangedListener((visible) -> {
-                if (mQs.getHeader().isShown()) {
-                    animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_STANDARD,
-                            0 /* delay */);
-                    mNotificationStackScrollLayoutController.animateNextTopPaddingChange();
-                }
-            });
-            mLockscreenShadeTransitionController.setQS(mQs);
-            mShadeTransitionController.setQs(mQs);
-            mNotificationStackScrollLayoutController.setQsHeader((ViewGroup) mQs.getHeader());
-            mQs.setScrollListener(mQsScrollListener);
-            updateQsExpansion();
-        }
-
-        @Override
-        public void onFragmentViewDestroyed(String tag, Fragment fragment) {
-            // Manual handling of fragment lifecycle is only required because this bridges
-            // non-fragment and fragment code. Once we are using a fragment for the notification
-            // panel, mQs will not need to be null cause it will be tied to the same lifecycle.
-            if (fragment == mQs) {
-                mQs = null;
-            }
-        }
-    }
-
-    private void animateNextNotificationBounds(long duration, long delay) {
-        mAnimateNextNotificationBounds = true;
-        mNotificationBoundsAnimationDuration = duration;
-        mNotificationBoundsAnimationDelay = delay;
+        return !mShowIconsWhenExpanded;
     }
 
     public void setTouchAndAnimationDisabled(boolean disabled) {
@@ -4247,9 +2930,11 @@
         if (dozing == mDozing) return;
         mView.setDozing(dozing);
         mDozing = dozing;
+        // TODO (b/) make listeners for this
         mNotificationStackScrollLayoutController.setDozing(mDozing, animate);
         mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
         mKeyguardStatusBarViewController.setDozing(mDozing);
+        mQsController.setDozing(mDozing);
 
         if (dozing) {
             mBottomAreaShadeAlphaAnimator.cancel();
@@ -4440,33 +3125,13 @@
         ipw.print("mMaxAllowedKeyguardNotifications=");
         ipw.println(mMaxAllowedKeyguardNotifications);
         ipw.print("mAnimateNextPositionUpdate="); ipw.println(mAnimateNextPositionUpdate);
-        ipw.print("mQuickQsHeaderHeight="); ipw.println(mQuickQsHeaderHeight);
-        ipw.print("mQsTrackingPointer="); ipw.println(mQsTrackingPointer);
-        ipw.print("mQsTracking="); ipw.println(mQsTracking);
-        ipw.print("mConflictingQsExpansionGesture="); ipw.println(mConflictingQsExpansionGesture);
         ipw.print("mPanelExpanded="); ipw.println(mPanelExpanded);
-        ipw.print("mQsExpanded="); ipw.println(mQsExpanded);
-        ipw.print("mQsExpandedWhenExpandingStarted="); ipw.println(mQsExpandedWhenExpandingStarted);
-        ipw.print("mQsFullyExpanded="); ipw.println(mQsFullyExpanded);
-        ipw.print("mKeyguardShowing="); ipw.println(mKeyguardShowing);
         ipw.print("mKeyguardQsUserSwitchEnabled="); ipw.println(mKeyguardQsUserSwitchEnabled);
         ipw.print("mKeyguardUserSwitcherEnabled="); ipw.println(mKeyguardUserSwitcherEnabled);
         ipw.print("mDozing="); ipw.println(mDozing);
         ipw.print("mDozingOnDown="); ipw.println(mDozingOnDown);
         ipw.print("mBouncerShowing="); ipw.println(mBouncerShowing);
         ipw.print("mBarState="); ipw.println(mBarState);
-        ipw.print("mInitialHeightOnTouch="); ipw.println(mInitialHeightOnTouch);
-        ipw.print("mInitialTouchX="); ipw.println(mInitialTouchX);
-        ipw.print("mInitialTouchY="); ipw.println(mInitialTouchY);
-        ipw.print("mQsExpansionHeight="); ipw.println(mQsExpansionHeight);
-        ipw.print("mQsMinExpansionHeight="); ipw.println(mQsMinExpansionHeight);
-        ipw.print("mQsMaxExpansionHeight="); ipw.println(mQsMaxExpansionHeight);
-        ipw.print("mQsPeekHeight="); ipw.println(mQsPeekHeight);
-        ipw.print("mStackScrollerOverscrolling="); ipw.println(mStackScrollerOverscrolling);
-        ipw.print("mQsExpansionFromOverscroll="); ipw.println(mQsExpansionFromOverscroll);
-        ipw.print("mLastOverscroll="); ipw.println(mLastOverscroll);
-        ipw.print("mQsExpansionEnabledPolicy="); ipw.println(mQsExpansionEnabledPolicy);
-        ipw.print("mQsExpansionEnabledAmbient="); ipw.println(mQsExpansionEnabledAmbient);
         ipw.print("mStatusBarMinHeight="); ipw.println(mStatusBarMinHeight);
         ipw.print("mStatusBarHeaderHeightKeyguard="); ipw.println(mStatusBarHeaderHeightKeyguard);
         ipw.print("mOverStretchAmount="); ipw.println(mOverStretchAmount);
@@ -4474,17 +3139,9 @@
         ipw.print("mDownY="); ipw.println(mDownY);
         ipw.print("mDisplayTopInset="); ipw.println(mDisplayTopInset);
         ipw.print("mDisplayRightInset="); ipw.println(mDisplayRightInset);
-        ipw.print("mLargeScreenShadeHeaderHeight="); ipw.println(mLargeScreenShadeHeaderHeight);
-        ipw.print("mSplitShadeNotificationsScrimMarginBottom=");
-        ipw.println(mSplitShadeNotificationsScrimMarginBottom);
+        ipw.print("mDisplayLeftInset="); ipw.println(mDisplayLeftInset);
         ipw.print("mIsExpanding="); ipw.println(mIsExpanding);
-        ipw.print("mQsExpandImmediate="); ipw.println(mQsExpandImmediate);
-        ipw.print("mTwoFingerQsExpandPossible="); ipw.println(mTwoFingerQsExpandPossible);
         ipw.print("mHeaderDebugInfo="); ipw.println(mHeaderDebugInfo);
-        ipw.print("mQsAnimatorExpand="); ipw.println(mQsAnimatorExpand);
-        ipw.print("mQsScrimEnabled="); ipw.println(mQsScrimEnabled);
-        ipw.print("mQsTouchAboveFalsingThreshold="); ipw.println(mQsTouchAboveFalsingThreshold);
-        ipw.print("mQsFalsingThreshold="); ipw.println(mQsFalsingThreshold);
         ipw.print("mHeadsUpStartHeight="); ipw.println(mHeadsUpStartHeight);
         ipw.print("mListenForHeadsUp="); ipw.println(mListenForHeadsUp);
         ipw.print("mNavigationBarBottomHeight="); ipw.println(mNavigationBarBottomHeight);
@@ -4510,40 +3167,14 @@
         ipw.print("mHeadsUpInset="); ipw.println(mHeadsUpInset);
         ipw.print("mHeadsUpPinnedMode="); ipw.println(mHeadsUpPinnedMode);
         ipw.print("mAllowExpandForSmallExpansion="); ipw.println(mAllowExpandForSmallExpansion);
-        ipw.print("mLockscreenNotificationQSPadding=");
-        ipw.println(mLockscreenNotificationQSPadding);
-        ipw.print("mTransitioningToFullShadeProgress=");
-        ipw.println(mTransitioningToFullShadeProgress);
-        ipw.print("mTransitionToFullShadeQSPosition=");
-        ipw.println(mTransitionToFullShadeQSPosition);
-        ipw.print("mDistanceForQSFullShadeTransition=");
-        ipw.println(mDistanceForQSFullShadeTransition);
-        ipw.print("mQsTranslationForFullShadeTransition=");
-        ipw.println(mQsTranslationForFullShadeTransition);
         ipw.print("mMaxOverscrollAmountForPulse="); ipw.println(mMaxOverscrollAmountForPulse);
-        ipw.print("mAnimateNextNotificationBounds="); ipw.println(mAnimateNextNotificationBounds);
-        ipw.print("mNotificationBoundsAnimationDelay=");
-        ipw.println(mNotificationBoundsAnimationDelay);
-        ipw.print("mNotificationBoundsAnimationDuration=");
-        ipw.println(mNotificationBoundsAnimationDuration);
         ipw.print("mIsPanelCollapseOnQQS="); ipw.println(mIsPanelCollapseOnQQS);
-        ipw.print("mAnimatingQS="); ipw.println(mAnimatingQS);
-        ipw.print("mIsQsTranslationResetAnimator="); ipw.println(mIsQsTranslationResetAnimator);
-        ipw.print("mIsPulseExpansionResetAnimator="); ipw.println(mIsPulseExpansionResetAnimator);
         ipw.print("mKeyguardOnlyContentAlpha="); ipw.println(mKeyguardOnlyContentAlpha);
         ipw.print("mKeyguardOnlyTransitionTranslationY=");
         ipw.println(mKeyguardOnlyTransitionTranslationY);
         ipw.print("mUdfpsMaxYBurnInOffset="); ipw.println(mUdfpsMaxYBurnInOffset);
         ipw.print("mIsGestureNavigation="); ipw.println(mIsGestureNavigation);
         ipw.print("mOldLayoutDirection="); ipw.println(mOldLayoutDirection);
-        ipw.print("mScrimCornerRadius="); ipw.println(mScrimCornerRadius);
-        ipw.print("mScreenCornerRadius="); ipw.println(mScreenCornerRadius);
-        ipw.print("mQSAnimatingHiddenFromCollapsed="); ipw.println(mQSAnimatingHiddenFromCollapsed);
-        ipw.print("mUseLargeScreenShadeHeader="); ipw.println(mUseLargeScreenShadeHeader);
-        ipw.print("mEnableQsClipping="); ipw.println(mEnableQsClipping);
-        ipw.print("mQsClipTop="); ipw.println(mQsClipTop);
-        ipw.print("mQsClipBottom="); ipw.println(mQsClipBottom);
-        ipw.print("mQsVisible="); ipw.println(mQsVisible);
         ipw.print("mMinFraction="); ipw.println(mMinFraction);
         ipw.print("mStatusViewCentered="); ipw.println(mStatusViewCentered);
         ipw.print("mSplitShadeFullTransitionDistance=");
@@ -4689,7 +3320,7 @@
     }
 
     public void disable(int state1, int state2, boolean animated) {
-        mLargeScreenShadeHeaderController.disable(state1, state2, animated);
+        mShadeHeaderController.disable(state1, state2, animated);
     }
 
     /**
@@ -4728,12 +3359,12 @@
     public void updateSystemUiStateFlags() {
         if (SysUiState.DEBUG) {
             Log.d(TAG, "Updating panel sysui state flags: fullyExpanded="
-                    + isFullyExpanded() + " inQs=" + isInSettings());
+                    + isFullyExpanded() + " inQs=" + mQsController.getExpanded());
         }
         mSysUiState.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
-                        isFullyExpanded() && !isInSettings())
-                .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isFullyExpanded() && isInSettings())
-                .commitUpdate(mDisplayId);
+                        isFullyExpanded() && !mQsController.getExpanded())
+                .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
+                        isFullyExpanded() && mQsController.getExpanded()).commitUpdate(mDisplayId);
     }
 
     private void debugLog(String fmt, Object... args) {
@@ -4746,11 +3377,11 @@
     void notifyExpandingStarted() {
         if (!mExpanding) {
             mExpanding = true;
-            onExpandingStarted();
+            mIsExpanding = true;
+            mQsController.onExpandingStarted(mQsController.getFullyExpanded());
         }
     }
 
-    @VisibleForTesting
     void notifyExpandingFinished() {
         endClosing();
         if (mExpanding) {
@@ -4759,7 +3390,7 @@
         }
     }
 
-    private float getTouchSlop(MotionEvent event) {
+    float getTouchSlop(MotionEvent event) {
         // Adjust the touch slop if another gesture may be being performed.
         return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
                 ? mTouchSlop * mSlopMultiplier
@@ -4833,11 +3464,15 @@
     public void startExpandMotion(float newX, float newY, boolean startTracking,
             float expandedHeight) {
         if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
-            beginJankMonitoring();
+            mQsController.beginJankMonitoring(isFullyCollapsed());
         }
         mInitialOffsetOnTouch = expandedHeight;
-        mInitialExpandY = newY;
-        mInitialExpandX = newX;
+        if (!mTracking || isFullyCollapsed()) {
+            mInitialExpandY = newY;
+            mInitialExpandX = newX;
+        } else {
+            mShadeLog.d("not setting mInitialExpandY in startExpandMotion");
+        }
         mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
         if (startTracking) {
             mTouchSlopExceeded = true;
@@ -4902,7 +3537,7 @@
                 mUpdateFlingVelocity = vel;
             }
         } else if (!mCentralSurfaces.isBouncerShowing()
-                && !mStatusBarKeyguardViewManager.isShowingAlternateBouncer()
+                && !mAlternateBouncerInteractor.isVisibleState()
                 && !mKeyguardStateController.isKeyguardGoingAway()) {
             onEmptySpaceClick();
             onTrackingStopped(true);
@@ -4947,7 +3582,7 @@
 
     private void fling(float vel, boolean expand, float collapseSpeedUpFactor,
             boolean expandBecauseOfFalsing) {
-        float target = expand ? getMaxPanelHeight() : 0;
+        float target = expand ? getMaxPanelTransitionDistance() : 0;
         if (!expand) {
             setClosing(true);
         }
@@ -4991,7 +3626,8 @@
         setExpandedHeightInternal(height);
     }
 
-    private void updateExpandedHeightToMaxHeight() {
+    /** Try to set expanded height to max. */
+    void updateExpandedHeightToMaxHeight() {
         float currentMaxPanelHeight = getMaxPanelHeight();
 
         if (isFullyCollapsed()) {
@@ -5002,7 +3638,8 @@
             return;
         }
 
-        if (mTracking && !isTrackingBlocked()) {
+        if (mTracking && !(mBlockingExpansionForCurrentTouch
+                || mQsController.isTrackingBlocked())) {
             return;
         }
 
@@ -5030,7 +3667,7 @@
             float maxPanelHeight = getMaxPanelTransitionDistance();
             if (mHeightAnimator == null) {
                 // Split shade has its own overscroll logic
-                if (mTracking && !mSplitShadeEnabled) {
+                if (mTracking) {
                     float overExpansionPixels = Math.max(0, h - maxPanelHeight);
                     setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
                 }
@@ -5044,6 +3681,7 @@
                     mHeightAnimator.end();
                 }
             }
+            mQsController.setShadeExpandedHeight(mExpandedHeight);
             mExpansionDragDownAmountPx = h;
             mExpandedFraction = Math.min(1f,
                     maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
@@ -5083,7 +3721,7 @@
         return mExpandedHeight;
     }
 
-    private float getExpandedFraction() {
+    float getExpandedFraction() {
         return mExpandedFraction;
     }
 
@@ -5114,7 +3752,7 @@
             return true;
         } else {
             // case of two finger swipe from the top of keyguard
-            return computeQsExpansionFraction() == 1;
+            return mQsController.computeExpansionFraction() == 1;
         }
     }
 
@@ -5211,6 +3849,11 @@
         }
     }
 
+    /** Returns whether a shade or QS expansion animation is running */
+    public boolean isShadeOrQsHeightAnimationRunning() {
+        return mHeightAnimator != null && !mHintAnimationRunning && !mIsSpringBackAnimation;
+    }
+
     /**
      * Phase 2: Bounce down.
      */
@@ -5342,48 +3985,135 @@
         return mView.isEnabled();
     }
 
-    private void beginJankMonitoring() {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        InteractionJankMonitor.Configuration.Builder builder =
-                InteractionJankMonitor.Configuration.Builder.withView(
-                                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
-                                mView)
-                        .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
-        mInteractionJankMonitor.begin(builder);
+    int getDisplayRightInset() {
+        return mDisplayRightInset;
     }
 
-    private void endJankMonitoring() {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        InteractionJankMonitor.getInstance().end(
-                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+    int getDisplayLeftInset() {
+        return mDisplayLeftInset;
     }
 
-    private void cancelJankMonitoring() {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        InteractionJankMonitor.getInstance().cancel(
-                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+    float getOverStretchAmount() {
+        return mOverStretchAmount;
     }
 
-    private float getExpansionFraction() {
-        return mExpandedFraction;
+    float getMinFraction() {
+        return mMinFraction;
+    }
+
+    boolean getCollapsedOnDown() {
+        return mCollapsedOnDown;
+    }
+
+    int getNavigationBarBottomHeight() {
+        return mNavigationBarBottomHeight;
+    }
+
+    boolean isExpandingFromHeadsUp() {
+        return mExpandingFromHeadsUp;
+    }
+
+    /**
+     * We don't always want to close QS when requested as shade might be in a different state
+     * already e.g. when going from collapse to expand very quickly. In that case StatusBar
+     * window might send signal to collapse QS but we might be already expanding and in split
+     * shade QS are always expanded
+     */
+    private void closeQsIfPossible() {
+        boolean openOrOpening = isShadeFullyOpen() || isExpanding();
+        if (!(mSplitShadeEnabled && openOrOpening)) {
+            mQsController.closeQs();
+        }
+    }
+
+    /** TODO: remove need for this delegate (b/254870148) */
+    public void setQsScrimEnabled(boolean qsScrimEnabled) {
+        mQsController.setScrimEnabled(qsScrimEnabled);
     }
 
     private ShadeExpansionStateManager getShadeExpansionStateManager() {
         return mShadeExpansionStateManager;
     }
 
+    private void onQsExpansionChanged(boolean expanded) {
+        updateExpandedHeightToMaxHeight();
+        setStatusAccessibilityImportance(expanded
+                ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+        updateSystemUiStateFlags();
+        NavigationBarView navigationBarView =
+                mNavigationBarController.getNavigationBarView(mDisplayId);
+        if (navigationBarView != null) {
+            navigationBarView.onStatusBarPanelStateChanged();
+        }
+    }
+
+    @VisibleForTesting
+    void onQsSetExpansionHeightCalled(boolean qsFullyExpanded) {
+        requestScrollerTopPaddingUpdate(false);
+        mKeyguardStatusBarViewController.updateViewState();
+        int barState = getBarState();
+        if (barState == SHADE_LOCKED || barState == KEYGUARD) {
+            updateKeyguardBottomAreaAlpha();
+            positionClockAndNotifications();
+        }
+
+        if (mAccessibilityManager.isEnabled()) {
+            mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
+        }
+
+        if (!mFalsingManager.isUnlockingDisabled() && qsFullyExpanded
+                && mFalsingCollector.shouldEnforceBouncer()) {
+            mCentralSurfaces.executeRunnableDismissingKeyguard(null, null,
+                    false, true, false);
+        }
+        if (DEBUG_DRAWABLE) {
+            mView.invalidate();
+        }
+    }
+
+    private void onQsStateUpdated(boolean qsExpanded, boolean isStackScrollerOverscrolling) {
+        if (mKeyguardUserSwitcherController != null && qsExpanded
+                && !isStackScrollerOverscrolling) {
+            mKeyguardUserSwitcherController.closeSwitcherIfOpenAndNotSimple(true);
+        }
+    }
+
+    private void onQsClippingImmediatelyApplied(boolean clipStatusView,
+            Rect lastQsClipBounds, int top, boolean qsFragmentCreated, boolean qsVisible) {
+        if (qsFragmentCreated) {
+            mKeyguardInteractor.setQuickSettingsVisible(qsVisible);
+        }
+
+        // The padding on this area is large enough that
+        // we can use a cheaper clipping strategy
+        mKeyguardStatusViewController.setClipBounds(
+                clipStatusView ? lastQsClipBounds : null);
+        if (mSplitShadeEnabled) {
+            mKeyguardStatusBarViewController.setNoTopClipping();
+        } else {
+            mKeyguardStatusBarViewController.updateTopClipping(top);
+        }
+    }
+
+    private void onFlingQsWithoutClick(ValueAnimator animator, float qsExpansionHeight,
+            float target, float vel) {
+        mFlingAnimationUtils.apply(animator, qsExpansionHeight, target, vel);
+    }
+
+    private void onExpansionHeightSetToMax(boolean requestPaddingUpdate) {
+        if (requestPaddingUpdate) {
+            requestScrollerTopPaddingUpdate(false /* animate */);
+        }
+        updateExpandedHeightToMaxHeight();
+    }
+
     private final class NsslHeightChangedListener implements
             ExpandableView.OnHeightChangedListener {
         @Override
         public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
             // Block update if we are in QS and just the top padding changed (i.e. view == null).
-            if (view == null && mQsExpanded) {
+            if (view == null && mQsController.getExpanded()) {
                 return;
             }
             if (needsAnimation && mInterpolatedDarkAmount == 0) {
@@ -5399,7 +4129,7 @@
                     == firstRow))) {
                 requestScrollerTopPaddingUpdate(false /* animate */);
             }
-            if (mKeyguardShowing) {
+            if (getKeyguardShowing()) {
                 updateMaxDisplayedNotifications(true);
             }
             updateExpandedHeightToMaxHeight();
@@ -5409,62 +4139,6 @@
         public void onReset(ExpandableView view) {}
     }
 
-    private void collapseOrExpand() {
-        onQsExpansionStarted();
-        if (mQsExpanded) {
-            flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */,
-                    true /* isClick */);
-        } else if (isQsExpansionEnabled()) {
-            mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0);
-            flingSettings(0 /* vel */, FLING_EXPAND, null /* onFinishRunnable */,
-                    true /* isClick */);
-        }
-    }
-
-    private final class NsslOverscrollTopChangedListener implements
-            NotificationStackScrollLayout.OnOverscrollTopChangedListener {
-        @Override
-        public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
-            // When in split shade, overscroll shouldn't carry through to QS
-            if (mSplitShadeEnabled) {
-                return;
-            }
-            cancelQsAnimation();
-            if (!isQsExpansionEnabled()) {
-                amount = 0f;
-            }
-            float rounded = amount >= 1f ? amount : 0f;
-            setOverScrolling(rounded != 0f && isRubberbanded);
-            mQsExpansionFromOverscroll = rounded != 0f;
-            mLastOverscroll = rounded;
-            updateQsState();
-            setQsExpansionHeight(mQsMinExpansionHeight + rounded);
-        }
-
-        @Override
-        public void flingTopOverscroll(float velocity, boolean open) {
-            // in split shade mode we want to expand/collapse QS only when touch happens within QS
-            if (isSplitShadeAndTouchXOutsideQs(mInitialTouchX)) {
-                return;
-            }
-            mLastOverscroll = 0f;
-            mQsExpansionFromOverscroll = false;
-            if (open) {
-                // During overscrolling, qsExpansion doesn't actually change that the qs is
-                // becoming expanded. Any layout could therefore reset the position again. Let's
-                // make sure we can expand
-                setOverScrolling(false);
-            }
-            setQsExpansionHeight(mQsExpansionHeight);
-            boolean canExpand = isQsExpansionEnabled();
-            flingSettings(!canExpand && open ? 0f : velocity,
-                    open && canExpand ? FLING_EXPAND : FLING_COLLAPSE, () -> {
-                        setOverScrolling(false);
-                        updateQsState();
-                    }, false /* isClick */);
-        }
-    }
-
     private void onDynamicPrivacyChanged() {
         // Do not request animation when pulsing or waking up, otherwise the clock will be out
         // of sync with the notification panel.
@@ -5514,19 +4188,6 @@
         }
     }
 
-    private void onQsHeightChanged() {
-        mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0;
-        if (mQsExpanded && mQsFullyExpanded) {
-            mQsExpansionHeight = mQsMaxExpansionHeight;
-            requestScrollerTopPaddingUpdate(false /* animate */);
-            updateExpandedHeightToMaxHeight();
-        }
-        if (mAccessibilityManager.isEnabled()) {
-            mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
-        }
-        mNotificationStackScrollLayoutController.setMaxTopPadding(mQsMaxExpansionHeight);
-    }
-
     private final class ConfigurationListener implements
             ConfigurationController.ConfigurationListener {
         @Override
@@ -5602,8 +4263,9 @@
 
             setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
 
+            // TODO: maybe add a listener for barstate
             mBarState = statusBarState;
-            mKeyguardShowing = keyguardShowing;
+            mQsController.setBarState(statusBarState);
 
             boolean fromShadeToKeyguard = statusBarState == KEYGUARD
                     && (oldState == SHADE || oldState == SHADE_LOCKED);
@@ -5611,7 +4273,7 @@
                 // user can go to keyguard from different shade states and closing animation
                 // may not fully run - we always want to make sure we close QS when that happens
                 // as we never need QS open in fresh keyguard state
-                closeQs();
+                mQsController.closeQs();
             }
 
             if (oldState == KEYGUARD && (goingToFullShade
@@ -5627,7 +4289,7 @@
                     duration = StackStateAnimator.ANIMATION_DURATION_STANDARD;
                 }
                 mKeyguardStatusBarViewController.animateKeyguardStatusBarOut(startDelay, duration);
-                updateQSMinHeight();
+                mQsController.updateMinHeight();
             } else if (oldState == StatusBarState.SHADE_LOCKED
                     && statusBarState == KEYGUARD) {
                 mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
@@ -5655,9 +4317,7 @@
                             keyguardShowing ? View.VISIBLE : View.INVISIBLE);
                 }
                 if (keyguardShowing && oldState != mBarState) {
-                    if (mQs != null) {
-                        mQs.hideImmediately();
-                    }
+                    mQsController.hideQsImmediately();
                 }
             }
             mKeyguardStatusBarViewController.updateForHeadsUp();
@@ -5669,7 +4329,7 @@
             // The update needs to happen after the headerSlide in above, otherwise the translation
             // would reset
             maybeAnimateBottomAreaAlpha();
-            updateQsState();
+            mQsController.updateQsState();
         }
 
         @Override
@@ -5746,7 +4406,7 @@
         @Override
         public void onViewAttachedToWindow(View v) {
             mFragmentService.getFragmentHostManager(mView)
-                    .addTagListener(QS.TAG, mQsFragmentListener);
+                    .addTagListener(QS.TAG, mQsController.getQsFragmentListener());
             mStatusBarStateController.addCallback(mStatusBarStateListener);
             mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState());
             mConfigurationController.addCallback(mConfigurationListener);
@@ -5763,7 +4423,7 @@
         public void onViewDetachedFromWindow(View v) {
             mContentResolver.unregisterContentObserver(mSettingsChangeObserver);
             mFragmentService.getFragmentHostManager(mView)
-                    .removeTagListener(QS.TAG, mQsFragmentListener);
+                    .removeTagListener(QS.TAG, mQsController.getQsFragmentListener());
             mStatusBarStateController.removeCallback(mStatusBarStateListener);
             mConfigurationController.removeCallback(mConfigurationListener);
             mFalsingManager.removeTapListener(mFalsingTapListener);
@@ -5788,28 +4448,9 @@
             // Update Clock Pivot (used by anti-burnin transformations)
             mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight());
 
-            // Calculate quick setting heights.
-            int oldMaxHeight = mQsMaxExpansionHeight;
-            if (mQs != null) {
-                updateQSMinHeight();
-                mQsMaxExpansionHeight = mQs.getDesiredHeight();
-                mNotificationStackScrollLayoutController.setMaxTopPadding(mQsMaxExpansionHeight);
-            }
+            int oldMaxHeight = mQsController.updateHeightsOnShadeLayoutChange();
             positionClockAndNotifications();
-            if (mQsExpanded && mQsFullyExpanded) {
-                mQsExpansionHeight = mQsMaxExpansionHeight;
-                requestScrollerTopPaddingUpdate(false /* animate */);
-                updateExpandedHeightToMaxHeight();
-
-                // Size has changed, start an animation.
-                if (mQsMaxExpansionHeight != oldMaxHeight) {
-                    startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight);
-                }
-            } else if (!mQsExpanded && mQsExpansionAnimator == null) {
-                setQsExpansionHeight(mQsMinExpansionHeight + mLastOverscroll);
-            } else {
-                mShadeLog.v("onLayoutChange: qs expansion not set");
-            }
+            mQsController.handleShadeLayoutChanged(oldMaxHeight);
             updateExpandedHeight(getExpandedHeight());
             updateHeader();
 
@@ -5818,9 +4459,8 @@
             // container the desired height so when closing the QS detail, it stays smaller after
             // the size change animation is finished but the detail view is still being animated
             // away (this animation takes longer than the size change animation).
-            if (mQsSizeChangeAnimator == null && mQs != null) {
-                mQs.setHeightOverride(mQs.getDesiredHeight());
-            }
+            mQsController.setHeightOverrideToDesiredHeight();
+
             updateMaxHeadsUpTranslation();
             updateGestureExclusionRect();
             if (mExpandAfterLayoutRunnable != null) {
@@ -5831,18 +4471,6 @@
         }
     }
 
-    private void updateQSMinHeight() {
-        float previousMin = mQsMinExpansionHeight;
-        if (mKeyguardShowing || mSplitShadeEnabled) {
-            mQsMinExpansionHeight = 0;
-        } else {
-            mQsMinExpansionHeight = mQs.getQsMinExpansionHeight();
-        }
-        if (mQsExpansionHeight == previousMin) {
-            mQsExpansionHeight = mQsMinExpansionHeight;
-        }
-    }
-
     @NonNull
     private WindowInsets onApplyShadeWindowInsets(WindowInsets insets) {
         // the same types of insets that are handled in NotificationShadeWindowView
@@ -5850,6 +4478,8 @@
         Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes);
         mDisplayTopInset = combinedInsets.top;
         mDisplayRightInset = combinedInsets.right;
+        mDisplayLeftInset = combinedInsets.left;
+        mQsController.setDisplayInsets(mDisplayRightInset, mDisplayLeftInset);
 
         mNavigationBarBottomHeight = insets.getStableInsetBottom();
         updateMaxHeadsUpTranslation();
@@ -5862,10 +4492,10 @@
     }
 
     private void onPanelStateChanged(@PanelState int state) {
-        updateQSExpansionEnabledAmbient();
+        mQsController.updateExpansionEnabledAmbient();
 
         if (state == STATE_OPEN && mCurrentPanelState != state) {
-            setQsExpandImmediate(false);
+            mQsController.setExpandImmediate(false);
             mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
         }
         if (state == STATE_OPENING) {
@@ -5873,12 +4503,12 @@
             // to locked will trigger this event and we're not actually in the process of opening
             // the shade, lockscreen is just always expanded
             if (mSplitShadeEnabled && !isOnKeyguard()) {
-                setQsExpandImmediate(true);
+                mQsController.setExpandImmediate(true);
             }
             mOpenCloseListener.onOpenStarted();
         }
         if (state == STATE_CLOSED) {
-            setQsExpandImmediate(false);
+            mQsController.setExpandImmediate(false);
             // Close the status bar in the next frame so we can show the end of the
             // animation.
             mView.post(mMaybeHideExpandedRunnable);
@@ -5886,7 +4516,7 @@
         mCurrentPanelState = state;
     }
 
-    private Consumer<Float> toLockscreenTransitionAlpha(
+    private Consumer<Float> setTransitionAlpha(
             NotificationStackScrollLayoutController stackScroller) {
         return (Float alpha) -> {
             mKeyguardStatusViewController.setAlpha(alpha);
@@ -5904,7 +4534,7 @@
         };
     }
 
-    private Consumer<Float> toLockscreenTransitionY(
+    private Consumer<Float> setTransitionY(
                 NotificationStackScrollLayoutController stackScroller) {
         return (Float translationY) -> {
             mKeyguardStatusViewController.setTranslationY(translationY,  /* excludeMedia= */false);
@@ -5944,7 +4574,7 @@
         /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */
         public boolean onInterceptTouchEvent(MotionEvent event) {
             mShadeLog.logMotionEvent(event, "NPVC onInterceptTouchEvent");
-            if (mQs.disallowPanelTouches()) {
+            if (mQsController.disallowTouches()) {
                 mShadeLog.logMotionEvent(event,
                         "NPVC not intercepting touch, panel touches disallowed");
                 return false;
@@ -5966,14 +4596,14 @@
                         + "HeadsUpTouchHelper");
                 return true;
             }
-            if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
+            if (!mQsController.shouldQuickSettingsIntercept(mDownX, mDownY, 0)
                     && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
                 mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
                         + "PulseExpansionHandler");
                 return true;
             }
 
-            if (!isFullyCollapsed() && onQsIntercept(event)) {
+            if (!isFullyCollapsed() && mQsController.onIntercept(event)) {
                 debugLog("onQsIntercept true");
                 mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
                         + "QsIntercept");
@@ -6019,8 +4649,12 @@
                                 + " false");
                         return true;
                     }
-                    mInitialExpandY = y;
-                    mInitialExpandX = x;
+                    if (!mTracking || isFullyCollapsed()) {
+                        mInitialExpandY = y;
+                        mInitialExpandX = x;
+                    } else {
+                        mShadeLog.d("not setting mInitialExpandY in onInterceptTouch");
+                    }
                     mTouchStartedInEmptyArea = !isInContentBounds(x, y);
                     mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
                     mMotionAborted = false;
@@ -6093,8 +4727,7 @@
                 mLastTouchDownTime = event.getDownTime();
             }
 
-
-            if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
+            if (mQsController.isFullyExpandedAndTouchesDisallowed()) {
                 mShadeLog.logMotionEvent(event,
                         "onTouch: ignore touch, panel touches disallowed and qs fully expanded");
                 return false;
@@ -6125,7 +4758,7 @@
             // If pulse is expanding already, let's give it the touch. There are situations
             // where the panel starts expanding even though we're also pulsing
             boolean pulseShouldGetTouch = (!mIsExpanding
-                    && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
+                    && !mQsController.shouldQuickSettingsIntercept(mDownX, mDownY, 0))
                     || mPulseExpansionHandler.isExpanding();
             if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
                 // We're expanding all the other ones shouldn't get this anymore
@@ -6143,7 +4776,8 @@
             }
             boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
 
-            if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
+            if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && mQsController.handleTouch(
+                    event, isFullyCollapsed(), isShadeOrQsHeightAnimationRunning())) {
                 mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
                 return true;
             }
@@ -6209,7 +4843,8 @@
             final float x = event.getX(pointerIndex);
             final float y = event.getY(pointerIndex);
 
-            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN
+                    || event.getActionMasked() == MotionEvent.ACTION_MOVE) {
                 mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
                 mIgnoreXTouchSlop = true;
             }
@@ -6228,8 +4863,7 @@
                     mCollapsedAndHeadsUpOnDown =
                             isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
                     addMovement(event);
-                    boolean regularHeightAnimationRunning = mHeightAnimator != null
-                            && !mHintAnimationRunning && !mIsSpringBackAnimation;
+                    boolean regularHeightAnimationRunning = isShadeOrQsHeightAnimationRunning();
                     if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
                         mTouchSlopExceeded = regularHeightAnimationRunning
                                 || mTouchSlopExceededBeforeDown;
@@ -6272,7 +4906,7 @@
                         mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction);
                     }
                     addMovement(event);
-                    if (!isFullyCollapsed()) {
+                    if (!isFullyCollapsed() && !isOnKeyguard()) {
                         maybeVibrateOnOpening(true /* openingWithTouch */);
                     }
                     float h = y - mInitialExpandY;
@@ -6298,7 +4932,9 @@
                         mTouchAboveFalsingThreshold = true;
                         mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
                     }
-                    if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
+                    if ((!mGestureWaitForTouchSlop || mTracking)
+                            && !(mBlockingExpansionForCurrentTouch
+                            || mQsController.isTrackingBlocked())) {
                         // Count h==0 as part of swipe-up,
                         // otherwise {@link NotificationStackScrollLayout}
                         // wrongly enables stack height updates at the start of lockscreen swipe-up
@@ -6316,9 +4952,9 @@
                     // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
                     if (mHeightAnimator == null) {
                         if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                            endJankMonitoring();
+                            mQsController.endJankMonitoring();
                         } else {
-                            cancelJankMonitoring();
+                            mQsController.cancelJankMonitoring();
                         }
                     }
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 8314ec7..156e4fd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -19,6 +19,7 @@
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
@@ -52,13 +53,13 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.DumpsysTableLogger;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -72,10 +73,8 @@
 import java.lang.ref.Reference;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -89,6 +88,7 @@
         Dumpable, ConfigurationListener {
 
     private static final String TAG = "NotificationShadeWindowController";
+    private static final int MAX_STATE_CHANGES_BUFFER_SIZE = 100;
 
     private final Context mContext;
     private final WindowManager mWindowManager;
@@ -108,7 +108,7 @@
     private boolean mHasTopUi;
     private boolean mHasTopUiChanged;
     private float mScreenBrightnessDoze;
-    private final State mCurrentState = new State();
+    private final NotificationShadeWindowState mCurrentState = new NotificationShadeWindowState();
     private OtherwisedCollapsedListener mListener;
     private ForcePluginOpenListener mForcePluginOpenListener;
     private Consumer<Integer> mScrimsVisibilityListener;
@@ -125,6 +125,9 @@
     private int mDeferWindowLayoutParams;
     private boolean mLastKeyguardRotationAllowed;
 
+    private final NotificationShadeWindowState.Buffer mStateBuffer =
+            new NotificationShadeWindowState.Buffer(MAX_STATE_CHANGES_BUFFER_SIZE);
+
     @Inject
     public NotificationShadeWindowControllerImpl(Context context, WindowManager windowManager,
             IActivityManager activityManager, DozeParameters dozeParameters,
@@ -210,8 +213,8 @@
 
     @VisibleForTesting
     void onShadeExpansionFullyChanged(Boolean isExpanded) {
-        if (mCurrentState.mPanelExpanded != isExpanded) {
-            mCurrentState.mPanelExpanded = isExpanded;
+        if (mCurrentState.panelExpanded != isExpanded) {
+            mCurrentState.panelExpanded = isExpanded;
             apply(mCurrentState);
         }
     }
@@ -251,6 +254,7 @@
         mLp.setTitle("NotificationShade");
         mLp.packageName = mContext.getPackageName();
         mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        mLp.privateFlags |= PRIVATE_FLAG_OPTIMIZE_MEASURE;
 
         // We use BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE here, however, there is special logic in
         // window manager which disables the transient show behavior.
@@ -296,10 +300,10 @@
         mNotificationShadeView.setSystemUiVisibility(vis);
     }
 
-    private void applyKeyguardFlags(State state) {
-        final boolean keyguardOrAod = state.mKeyguardShowing
-                || (state.mDozing && mDozeParameters.getAlwaysOn());
-        if ((keyguardOrAod && !state.mBackdropShowing && !state.mLightRevealScrimOpaque)
+    private void applyKeyguardFlags(NotificationShadeWindowState state) {
+        final boolean keyguardOrAod = state.keyguardShowing
+                || (state.dozing && mDozeParameters.getAlwaysOn());
+        if ((keyguardOrAod && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque)
                 || mKeyguardViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()) {
             // Show the wallpaper if we're on keyguard/AOD and the wallpaper is not occluded by a
             // solid backdrop. Also, show it if we are currently animating between the
@@ -310,28 +314,31 @@
             mLpChanged.flags &= ~LayoutParams.FLAG_SHOW_WALLPAPER;
         }
 
-        if (state.mDozing) {
+        if (state.dozing) {
             mLpChanged.privateFlags |= LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
         } else {
             mLpChanged.privateFlags &= ~LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
         }
 
         if (mKeyguardPreferredRefreshRate > 0) {
-            boolean onKeyguard = state.mStatusBarState == StatusBarState.KEYGUARD
-                    && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
+            boolean onKeyguard = state.statusBarState == StatusBarState.KEYGUARD
+                    && !state.keyguardFadingAway && !state.keyguardGoingAway;
             if (onKeyguard
                     && mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) {
+                // both max and min display refresh rate must be set to take effect:
                 mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate;
+                mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate;
             } else {
                 mLpChanged.preferredMaxDisplayRefreshRate = 0;
+                mLpChanged.preferredMinDisplayRefreshRate = 0;
             }
             Trace.setCounter("display_set_preferred_refresh_rate",
                     (long) mKeyguardPreferredRefreshRate);
         } else if (mKeyguardMaxRefreshRate > 0) {
             boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled()
-                    && state.mStatusBarState == StatusBarState.KEYGUARD
-                    && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
-            if (state.mDozing || bypassOnKeyguard) {
+                    && state.statusBarState == StatusBarState.KEYGUARD
+                    && !state.keyguardFadingAway && !state.keyguardGoingAway;
+            if (state.dozing || bypassOnKeyguard) {
                 mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardMaxRefreshRate;
             } else {
                 mLpChanged.preferredMaxDisplayRefreshRate = 0;
@@ -340,7 +347,7 @@
                     (long) mLpChanged.preferredMaxDisplayRefreshRate);
         }
 
-        if (state.mBouncerShowing && !isDebuggable()) {
+        if (state.bouncerShowing && !isDebuggable()) {
             mLpChanged.flags |= LayoutParams.FLAG_SECURE;
         } else {
             mLpChanged.flags &= ~LayoutParams.FLAG_SECURE;
@@ -351,8 +358,8 @@
         return Build.IS_DEBUGGABLE;
     }
 
-    private void adjustScreenOrientation(State state) {
-        if (state.mBouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.mDozing) {
+    private void adjustScreenOrientation(NotificationShadeWindowState state) {
+        if (state.bouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.dozing) {
             if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) {
                 mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
             } else {
@@ -363,10 +370,10 @@
         }
     }
 
-    private void applyFocusableFlag(State state) {
-        boolean panelFocusable = state.mNotificationShadeFocusable && state.mPanelExpanded;
-        if (state.mBouncerShowing && (state.mKeyguardOccluded || state.mKeyguardNeedsInput)
-                || ENABLE_REMOTE_INPUT && state.mRemoteInputActive
+    private void applyFocusableFlag(NotificationShadeWindowState state) {
+        boolean panelFocusable = state.notificationShadeFocusable && state.panelExpanded;
+        if (state.bouncerShowing && (state.keyguardOccluded || state.keyguardNeedsInput)
+                || ENABLE_REMOTE_INPUT && state.remoteInputActive
                 // Make the panel focusable if we're doing the screen off animation, since the light
                 // reveal scrim is drawing in the panel and should consume touch events so that they
                 // don't go to the app behind.
@@ -376,7 +383,7 @@
         } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
             mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
             // Make sure to remove FLAG_ALT_FOCUSABLE_IM when keyguard needs input.
-            if (state.mKeyguardNeedsInput && state.isKeyguardShowingAndNotOccluded()) {
+            if (state.keyguardNeedsInput && state.isKeyguardShowingAndNotOccluded()) {
                 mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
             } else {
                 mLpChanged.flags |= LayoutParams.FLAG_ALT_FOCUSABLE_IM;
@@ -387,19 +394,19 @@
         }
     }
 
-    private void applyForceShowNavigationFlag(State state) {
-        if (state.mPanelExpanded || state.mBouncerShowing
-                || ENABLE_REMOTE_INPUT && state.mRemoteInputActive) {
+    private void applyForceShowNavigationFlag(NotificationShadeWindowState state) {
+        if (state.panelExpanded || state.bouncerShowing
+                || ENABLE_REMOTE_INPUT && state.remoteInputActive) {
             mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
         } else {
             mLpChanged.privateFlags &= ~LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
         }
     }
 
-    private void applyVisibility(State state) {
+    private void applyVisibility(NotificationShadeWindowState state) {
         boolean visible = isExpanded(state);
         mLogger.logApplyVisibility(visible);
-        if (state.mForcePluginOpen) {
+        if (state.forcePluginOpen) {
             if (mListener != null) {
                 mListener.setWouldOtherwiseCollapse(visible);
             }
@@ -415,16 +422,16 @@
         }
     }
 
-    private boolean isExpanded(State state) {
-        return !state.mForceCollapsed && (state.isKeyguardShowingAndNotOccluded()
-                || state.mPanelVisible || state.mKeyguardFadingAway || state.mBouncerShowing
-                || state.mHeadsUpShowing
-                || state.mScrimsVisibility != ScrimController.TRANSPARENT)
-                || state.mBackgroundBlurRadius > 0
-                || state.mLaunchingActivity;
+    private boolean isExpanded(NotificationShadeWindowState state) {
+        return !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded()
+                || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
+                || state.headsUpNotificationShowing
+                || state.scrimsVisibility != ScrimController.TRANSPARENT)
+                || state.backgroundBlurRadius > 0
+                || state.launchingActivityFromNotification;
     }
 
-    private void applyFitsSystemWindows(State state) {
+    private void applyFitsSystemWindows(NotificationShadeWindowState state) {
         boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
         if (mNotificationShadeView != null
                 && mNotificationShadeView.getFitsSystemWindows() != fitsSystemWindows) {
@@ -433,21 +440,21 @@
         }
     }
 
-    private void applyUserActivityTimeout(State state) {
+    private void applyUserActivityTimeout(NotificationShadeWindowState state) {
         if (state.isKeyguardShowingAndNotOccluded()
-                && state.mStatusBarState == StatusBarState.KEYGUARD
-                && !state.mQsExpanded) {
-            mLpChanged.userActivityTimeout = state.mBouncerShowing
+                && state.statusBarState == StatusBarState.KEYGUARD
+                && !state.qsExpanded) {
+            mLpChanged.userActivityTimeout = state.bouncerShowing
                     ? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout;
         } else {
             mLpChanged.userActivityTimeout = -1;
         }
     }
 
-    private void applyInputFeatures(State state) {
+    private void applyInputFeatures(NotificationShadeWindowState state) {
         if (state.isKeyguardShowingAndNotOccluded()
-                && state.mStatusBarState == StatusBarState.KEYGUARD
-                && !state.mQsExpanded && !state.mForceUserActivity) {
+                && state.statusBarState == StatusBarState.KEYGUARD
+                && !state.qsExpanded && !state.forceUserActivity) {
             mLpChanged.inputFeatures |=
                     LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
         } else {
@@ -456,7 +463,7 @@
         }
     }
 
-    private void applyStatusBarColorSpaceAgnosticFlag(State state) {
+    private void applyStatusBarColorSpaceAgnosticFlag(NotificationShadeWindowState state) {
         if (!isExpanded(state)) {
             mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
         } else {
@@ -482,8 +489,8 @@
         applyWindowLayoutParams();
     }
 
-    private void apply(State state) {
-        mLogger.logNewState(state);
+    private void apply(NotificationShadeWindowState state) {
+        logState(state);
         applyKeyguardFlags(state);
         applyFocusableFlag(state);
         applyForceShowNavigationFlag(state);
@@ -512,6 +519,38 @@
         notifyStateChangedCallbacks();
     }
 
+    private void logState(NotificationShadeWindowState state) {
+        mStateBuffer.insert(
+                state.keyguardShowing,
+                state.keyguardOccluded,
+                state.keyguardNeedsInput,
+                state.panelVisible,
+                state.panelExpanded,
+                state.notificationShadeFocusable,
+                state.bouncerShowing,
+                state.keyguardFadingAway,
+                state.keyguardGoingAway,
+                state.qsExpanded,
+                state.headsUpNotificationShowing,
+                state.lightRevealScrimOpaque,
+                state.forceWindowCollapsed,
+                state.forceDozeBrightness,
+                state.forceUserActivity,
+                state.launchingActivityFromNotification,
+                state.mediaBackdropShowing,
+                state.wallpaperSupportsAmbientMode,
+                state.windowNotTouchable,
+                state.componentsForcingTopUi,
+                state.forceOpenTokens,
+                state.statusBarState,
+                state.remoteInputActive,
+                state.forcePluginOpen,
+                state.dozing,
+                state.scrimsVisibility,
+                state.backgroundBlurRadius
+        );
+    }
+
     @Override
     public void notifyStateChangedCallbacks() {
         // Copy callbacks to separate ArrayList to avoid concurrent modification
@@ -520,36 +559,37 @@
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
         for (StatusBarWindowCallback cb : activeCallbacks) {
-            cb.onStateChanged(mCurrentState.mKeyguardShowing,
-                    mCurrentState.mKeyguardOccluded,
-                    mCurrentState.mBouncerShowing,
-                    mCurrentState.mDozing,
-                    mCurrentState.mPanelExpanded);
+            cb.onStateChanged(mCurrentState.keyguardShowing,
+                    mCurrentState.keyguardOccluded,
+                    mCurrentState.bouncerShowing,
+                    mCurrentState.dozing,
+                    mCurrentState.panelExpanded,
+                    mCurrentState.dreaming);
         }
     }
 
-    private void applyModalFlag(State state) {
-        if (state.mHeadsUpShowing) {
+    private void applyModalFlag(NotificationShadeWindowState state) {
+        if (state.headsUpNotificationShowing) {
             mLpChanged.flags |= LayoutParams.FLAG_NOT_TOUCH_MODAL;
         } else {
             mLpChanged.flags &= ~LayoutParams.FLAG_NOT_TOUCH_MODAL;
         }
     }
 
-    private void applyBrightness(State state) {
-        if (state.mForceDozeBrightness) {
+    private void applyBrightness(NotificationShadeWindowState state) {
+        if (state.forceDozeBrightness) {
             mLpChanged.screenBrightness = mScreenBrightnessDoze;
         } else {
             mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
         }
     }
 
-    private void applyHasTopUi(State state) {
-        mHasTopUiChanged = !state.mComponentsForcingTopUi.isEmpty() || isExpanded(state);
+    private void applyHasTopUi(NotificationShadeWindowState state) {
+        mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state);
     }
 
-    private void applyNotTouchable(State state) {
-        if (state.mNotTouchable) {
+    private void applyNotTouchable(NotificationShadeWindowState state) {
+        if (state.windowNotTouchable) {
             mLpChanged.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
         } else {
             mLpChanged.flags &= ~LayoutParams.FLAG_NOT_TOUCHABLE;
@@ -571,88 +611,88 @@
 
     @Override
     public void setKeyguardShowing(boolean showing) {
-        mCurrentState.mKeyguardShowing = showing;
+        mCurrentState.keyguardShowing = showing;
         apply(mCurrentState);
     }
 
     @Override
     public void setKeyguardOccluded(boolean occluded) {
-        mCurrentState.mKeyguardOccluded = occluded;
+        mCurrentState.keyguardOccluded = occluded;
         apply(mCurrentState);
     }
 
     @Override
     public void setKeyguardNeedsInput(boolean needsInput) {
-        mCurrentState.mKeyguardNeedsInput = needsInput;
+        mCurrentState.keyguardNeedsInput = needsInput;
         apply(mCurrentState);
     }
 
     @Override
     public void setPanelVisible(boolean visible) {
-        if (mCurrentState.mPanelVisible == visible
-                && mCurrentState.mNotificationShadeFocusable == visible) {
+        if (mCurrentState.panelVisible == visible
+                && mCurrentState.notificationShadeFocusable == visible) {
             return;
         }
         mLogger.logShadeVisibleAndFocusable(visible);
-        mCurrentState.mPanelVisible = visible;
-        mCurrentState.mNotificationShadeFocusable = visible;
+        mCurrentState.panelVisible = visible;
+        mCurrentState.notificationShadeFocusable = visible;
         apply(mCurrentState);
     }
 
     @Override
     public void setNotificationShadeFocusable(boolean focusable) {
         mLogger.logShadeFocusable(focusable);
-        mCurrentState.mNotificationShadeFocusable = focusable;
+        mCurrentState.notificationShadeFocusable = focusable;
         apply(mCurrentState);
     }
 
     @Override
     public void setBouncerShowing(boolean showing) {
-        mCurrentState.mBouncerShowing = showing;
+        mCurrentState.bouncerShowing = showing;
         apply(mCurrentState);
     }
 
     @Override
     public void setBackdropShowing(boolean showing) {
-        mCurrentState.mBackdropShowing = showing;
+        mCurrentState.mediaBackdropShowing = showing;
         apply(mCurrentState);
     }
 
     @Override
     public void setKeyguardFadingAway(boolean keyguardFadingAway) {
-        mCurrentState.mKeyguardFadingAway = keyguardFadingAway;
+        mCurrentState.keyguardFadingAway = keyguardFadingAway;
         apply(mCurrentState);
     }
 
     private void onQsExpansionChanged(Boolean expanded) {
-        mCurrentState.mQsExpanded = expanded;
+        mCurrentState.qsExpanded = expanded;
         apply(mCurrentState);
     }
 
     @Override
     public void setForceUserActivity(boolean forceUserActivity) {
-        mCurrentState.mForceUserActivity = forceUserActivity;
+        mCurrentState.forceUserActivity = forceUserActivity;
         apply(mCurrentState);
     }
 
     @Override
     public void setLaunchingActivity(boolean launching) {
-        mCurrentState.mLaunchingActivity = launching;
+        mCurrentState.launchingActivityFromNotification = launching;
         apply(mCurrentState);
     }
 
     @Override
     public boolean isLaunchingActivity() {
-        return mCurrentState.mLaunchingActivity;
+        return mCurrentState.launchingActivityFromNotification;
     }
 
     @Override
     public void setScrimsVisibility(int scrimsVisibility) {
-        if (scrimsVisibility == mCurrentState.mScrimsVisibility) {
+        if (scrimsVisibility == mCurrentState.scrimsVisibility) {
             return;
         }
         boolean wasExpanded = isExpanded(mCurrentState);
-        mCurrentState.mScrimsVisibility = scrimsVisibility;
+        mCurrentState.scrimsVisibility = scrimsVisibility;
         if (wasExpanded != isExpanded(mCurrentState)) {
             apply(mCurrentState);
         }
@@ -666,31 +706,31 @@
      */
     @Override
     public void setBackgroundBlurRadius(int backgroundBlurRadius) {
-        if (mCurrentState.mBackgroundBlurRadius == backgroundBlurRadius) {
+        if (mCurrentState.backgroundBlurRadius == backgroundBlurRadius) {
             return;
         }
-        mCurrentState.mBackgroundBlurRadius = backgroundBlurRadius;
+        mCurrentState.backgroundBlurRadius = backgroundBlurRadius;
         apply(mCurrentState);
     }
 
     @Override
     public void setHeadsUpShowing(boolean showing) {
-        mCurrentState.mHeadsUpShowing = showing;
+        mCurrentState.headsUpNotificationShowing = showing;
         apply(mCurrentState);
     }
 
     @Override
     public void setLightRevealScrimOpaque(boolean opaque) {
-        if (mCurrentState.mLightRevealScrimOpaque == opaque) {
+        if (mCurrentState.lightRevealScrimOpaque == opaque) {
             return;
         }
-        mCurrentState.mLightRevealScrimOpaque = opaque;
+        mCurrentState.lightRevealScrimOpaque = opaque;
         apply(mCurrentState);
     }
 
     @Override
     public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
-        mCurrentState.mWallpaperSupportsAmbientMode = supportsAmbientMode;
+        mCurrentState.wallpaperSupportsAmbientMode = supportsAmbientMode;
         apply(mCurrentState);
     }
 
@@ -698,7 +738,7 @@
      * @param state The {@link StatusBarStateController} of the status bar.
      */
     private void setStatusBarState(int state) {
-        mCurrentState.mStatusBarState = state;
+        mCurrentState.statusBarState = state;
         apply(mCurrentState);
     }
 
@@ -709,13 +749,13 @@
      */
     @Override
     public void setForceWindowCollapsed(boolean force) {
-        mCurrentState.mForceCollapsed = force;
+        mCurrentState.forceWindowCollapsed = force;
         apply(mCurrentState);
     }
 
     @Override
     public void onRemoteInputActive(boolean remoteInputActive) {
-        mCurrentState.mRemoteInputActive = remoteInputActive;
+        mCurrentState.remoteInputActive = remoteInputActive;
         apply(mCurrentState);
     }
 
@@ -725,32 +765,38 @@
      */
     @Override
     public void setForceDozeBrightness(boolean forceDozeBrightness) {
-        if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) {
+        if (mCurrentState.forceDozeBrightness == forceDozeBrightness) {
             return;
         }
-        mCurrentState.mForceDozeBrightness = forceDozeBrightness;
+        mCurrentState.forceDozeBrightness = forceDozeBrightness;
         apply(mCurrentState);
     }
 
     @Override
     public void setDozing(boolean dozing) {
-        mCurrentState.mDozing = dozing;
+        mCurrentState.dozing = dozing;
+        apply(mCurrentState);
+    }
+
+    @Override
+    public void setDreaming(boolean dreaming) {
+        mCurrentState.dreaming = dreaming;
         apply(mCurrentState);
     }
 
     @Override
     public void setForcePluginOpen(boolean forceOpen, Object token) {
         if (forceOpen) {
-            mCurrentState.mForceOpenTokens.add(token);
+            mCurrentState.forceOpenTokens.add(token);
         } else {
-            mCurrentState.mForceOpenTokens.remove(token);
+            mCurrentState.forceOpenTokens.remove(token);
         }
-        final boolean previousForceOpenState = mCurrentState.mForcePluginOpen;
-        mCurrentState.mForcePluginOpen = !mCurrentState.mForceOpenTokens.isEmpty();
-        if (previousForceOpenState != mCurrentState.mForcePluginOpen) {
+        final boolean previousForceOpenState = mCurrentState.forcePluginOpen;
+        mCurrentState.forcePluginOpen = !mCurrentState.forceOpenTokens.isEmpty();
+        if (previousForceOpenState != mCurrentState.forcePluginOpen) {
             apply(mCurrentState);
             if (mForcePluginOpenListener != null) {
-                mForcePluginOpenListener.onChange(mCurrentState.mForcePluginOpen);
+                mForcePluginOpenListener.onChange(mCurrentState.forcePluginOpen);
             }
         }
     }
@@ -760,12 +806,12 @@
      */
     @Override
     public boolean getForcePluginOpen() {
-        return mCurrentState.mForcePluginOpen;
+        return mCurrentState.forcePluginOpen;
     }
 
     @Override
     public void setNotTouchable(boolean notTouchable) {
-        mCurrentState.mNotTouchable = notTouchable;
+        mCurrentState.windowNotTouchable = notTouchable;
         apply(mCurrentState);
     }
 
@@ -774,7 +820,7 @@
      */
     @Override
     public boolean getPanelExpanded() {
-        return mCurrentState.mPanelExpanded;
+        return mCurrentState.panelExpanded;
     }
 
     @Override
@@ -797,11 +843,16 @@
         if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) {
             mNotificationShadeView.getViewRootImpl().dump("  ", pw);
         }
+        new DumpsysTableLogger(
+                TAG,
+                NotificationShadeWindowState.TABLE_HEADERS,
+                mStateBuffer.toList()
+        ).printTableData(pw);
     }
 
     @Override
     public boolean isShowingWallpaper() {
-        return !mCurrentState.mBackdropShowing;
+        return !mCurrentState.mediaBackdropShowing;
     }
 
     @Override
@@ -831,7 +882,7 @@
      */
     @Override
     public void setKeyguardGoingAway(boolean goingAway) {
-        mCurrentState.mKeyguardGoingAway = goingAway;
+        mCurrentState.keyguardGoingAway = goingAway;
         apply(mCurrentState);
     }
 
@@ -843,87 +894,13 @@
     @Override
     public void setRequestTopUi(boolean requestTopUi, String componentTag) {
         if (requestTopUi) {
-            mCurrentState.mComponentsForcingTopUi.add(componentTag);
+            mCurrentState.componentsForcingTopUi.add(componentTag);
         } else {
-            mCurrentState.mComponentsForcingTopUi.remove(componentTag);
+            mCurrentState.componentsForcingTopUi.remove(componentTag);
         }
         apply(mCurrentState);
     }
 
-    private static class State {
-        boolean mKeyguardShowing;
-        boolean mKeyguardOccluded;
-        boolean mKeyguardNeedsInput;
-        boolean mPanelVisible;
-        boolean mPanelExpanded;
-        boolean mNotificationShadeFocusable;
-        boolean mBouncerShowing;
-        boolean mKeyguardFadingAway;
-        boolean mKeyguardGoingAway;
-        boolean mQsExpanded;
-        boolean mHeadsUpShowing;
-        boolean mLightRevealScrimOpaque;
-        boolean mForceCollapsed;
-        boolean mForceDozeBrightness;
-        boolean mForceUserActivity;
-        boolean mLaunchingActivity;
-        boolean mBackdropShowing;
-        boolean mWallpaperSupportsAmbientMode;
-        boolean mNotTouchable;
-        Set<String> mComponentsForcingTopUi = new HashSet<>();
-        Set<Object> mForceOpenTokens = new HashSet<>();
-
-        /**
-         * The status bar state from {@link CentralSurfaces}.
-         */
-        int mStatusBarState;
-
-        boolean mRemoteInputActive;
-        boolean mForcePluginOpen;
-        boolean mDozing;
-        int mScrimsVisibility;
-        int mBackgroundBlurRadius;
-
-        private boolean isKeyguardShowingAndNotOccluded() {
-            return mKeyguardShowing && !mKeyguardOccluded;
-        }
-
-        @Override
-        public String toString() {
-            return new StringBuilder()
-                    .append("State{")
-                    .append("  mKeyguardShowing=").append(mKeyguardShowing)
-                    .append(", mKeyguardOccluded=").append(mKeyguardOccluded)
-                    .append(", mKeyguardNeedsInput=").append(mKeyguardNeedsInput)
-                    .append(", mPanelVisible=").append(mPanelVisible)
-                    .append(", mPanelExpanded=").append(mPanelExpanded)
-                    .append(", mNotificationShadeFocusable=").append(mNotificationShadeFocusable)
-                    .append(", mBouncerShowing=").append(mBouncerShowing)
-                    .append(", mKeyguardFadingAway=").append(mKeyguardFadingAway)
-                    .append(", mKeyguardGoingAway=").append(mKeyguardGoingAway)
-                    .append(", mQsExpanded=").append(mQsExpanded)
-                    .append(", mHeadsUpShowing=").append(mHeadsUpShowing)
-                    .append(", mLightRevealScrimOpaque=").append(mLightRevealScrimOpaque)
-                    .append(", mForceCollapsed=").append(mForceCollapsed)
-                    .append(", mForceDozeBrightness=").append(mForceDozeBrightness)
-                    .append(", mForceUserActivity=").append(mForceUserActivity)
-                    .append(", mLaunchingActivity=").append(mLaunchingActivity)
-                    .append(", mBackdropShowing=").append(mBackdropShowing)
-                    .append(", mWallpaperSupportsAmbientMode=")
-                    .append(mWallpaperSupportsAmbientMode)
-                    .append(", mNotTouchable=").append(mNotTouchable)
-                    .append(", mComponentsForcingTopUi=").append(mComponentsForcingTopUi)
-                    .append(", mForceOpenTokens=").append(mForceOpenTokens)
-                    .append(", mStatusBarState=").append(mStatusBarState)
-                    .append(", mRemoteInputActive=").append(mRemoteInputActive)
-                    .append(", mForcePluginOpen=").append(mForcePluginOpen)
-                    .append(", mDozing=").append(mDozing)
-                    .append(", mScrimsVisibility=").append(mScrimsVisibility)
-                    .append(", mBackgroundBlurRadius=").append(mBackgroundBlurRadius)
-                    .append('}').toString();
-        }
-    }
-
     private final StateListener mStateListener = new StateListener() {
         @Override
         public void onStateChanged(int newState) {
@@ -934,5 +911,10 @@
         public void onDozingChanged(boolean isDozing) {
             setDozing(isDozing);
         }
+
+        @Override
+        public void onDreamingChanged(boolean isDreaming) {
+            setDreaming(isDreaming);
+        }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
new file mode 100644
index 0000000..fed9b84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.systemui.dump.DumpsysTableLogger
+import com.android.systemui.dump.Row
+import com.android.systemui.plugins.util.RingBuffer
+import com.android.systemui.shade.NotificationShadeWindowState.Buffer
+import com.android.systemui.statusbar.StatusBarState
+
+/**
+ * Represents state of shade window, used by [NotificationShadeWindowControllerImpl]. Contains
+ * nested class [Buffer] for pretty table logging in bug reports.
+ */
+class NotificationShadeWindowState(
+    @JvmField var keyguardShowing: Boolean = false,
+    @JvmField var keyguardOccluded: Boolean = false,
+    @JvmField var keyguardNeedsInput: Boolean = false,
+    @JvmField var panelVisible: Boolean = false,
+    /** shade panel is expanded (expansion fraction > 0) */
+    @JvmField var panelExpanded: Boolean = false,
+    @JvmField var notificationShadeFocusable: Boolean = false,
+    @JvmField var bouncerShowing: Boolean = false,
+    @JvmField var keyguardFadingAway: Boolean = false,
+    @JvmField var keyguardGoingAway: Boolean = false,
+    @JvmField var qsExpanded: Boolean = false,
+    @JvmField var headsUpNotificationShowing: Boolean = false,
+    @JvmField var lightRevealScrimOpaque: Boolean = false,
+    @JvmField var forceWindowCollapsed: Boolean = false,
+    @JvmField var forceDozeBrightness: Boolean = false,
+    // TODO: forceUserActivity seems to be unused, delete?
+    @JvmField var forceUserActivity: Boolean = false,
+    @JvmField var launchingActivityFromNotification: Boolean = false,
+    @JvmField var mediaBackdropShowing: Boolean = false,
+    @JvmField var wallpaperSupportsAmbientMode: Boolean = false,
+    @JvmField var windowNotTouchable: Boolean = false,
+    @JvmField var componentsForcingTopUi: MutableSet<String> = mutableSetOf(),
+    @JvmField var forceOpenTokens: MutableSet<Any> = mutableSetOf(),
+    /** one of [StatusBarState] */
+    @JvmField var statusBarState: Int = 0,
+    @JvmField var remoteInputActive: Boolean = false,
+    @JvmField var forcePluginOpen: Boolean = false,
+    @JvmField var dozing: Boolean = false,
+    @JvmField var dreaming: Boolean = false,
+    @JvmField var scrimsVisibility: Int = 0,
+    @JvmField var backgroundBlurRadius: Int = 0,
+) {
+
+    fun isKeyguardShowingAndNotOccluded(): Boolean {
+        return keyguardShowing && !keyguardOccluded
+    }
+
+    /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */
+    val asStringList: List<String> by lazy {
+        listOf(
+            keyguardShowing.toString(),
+            keyguardOccluded.toString(),
+            keyguardNeedsInput.toString(),
+            panelVisible.toString(),
+            panelExpanded.toString(),
+            notificationShadeFocusable.toString(),
+            bouncerShowing.toString(),
+            keyguardFadingAway.toString(),
+            keyguardGoingAway.toString(),
+            qsExpanded.toString(),
+            headsUpNotificationShowing.toString(),
+            lightRevealScrimOpaque.toString(),
+            forceWindowCollapsed.toString(),
+            forceDozeBrightness.toString(),
+            forceUserActivity.toString(),
+            launchingActivityFromNotification.toString(),
+            mediaBackdropShowing.toString(),
+            wallpaperSupportsAmbientMode.toString(),
+            windowNotTouchable.toString(),
+            componentsForcingTopUi.toString(),
+            forceOpenTokens.toString(),
+            StatusBarState.toString(statusBarState),
+            remoteInputActive.toString(),
+            forcePluginOpen.toString(),
+            dozing.toString(),
+            scrimsVisibility.toString(),
+            backgroundBlurRadius.toString()
+        )
+    }
+
+    /**
+     * [RingBuffer] to store [NotificationShadeWindowState]. After the buffer is full, it will
+     * recycle old events.
+     */
+    class Buffer(capacity: Int) {
+
+        private val buffer = RingBuffer(capacity) { NotificationShadeWindowState() }
+
+        /** Insert a new element in the buffer. */
+        fun insert(
+            keyguardShowing: Boolean,
+            keyguardOccluded: Boolean,
+            keyguardNeedsInput: Boolean,
+            panelVisible: Boolean,
+            panelExpanded: Boolean,
+            notificationShadeFocusable: Boolean,
+            bouncerShowing: Boolean,
+            keyguardFadingAway: Boolean,
+            keyguardGoingAway: Boolean,
+            qsExpanded: Boolean,
+            headsUpShowing: Boolean,
+            lightRevealScrimOpaque: Boolean,
+            forceCollapsed: Boolean,
+            forceDozeBrightness: Boolean,
+            forceUserActivity: Boolean,
+            launchingActivity: Boolean,
+            backdropShowing: Boolean,
+            wallpaperSupportsAmbientMode: Boolean,
+            notTouchable: Boolean,
+            componentsForcingTopUi: MutableSet<String>,
+            forceOpenTokens: MutableSet<Any>,
+            statusBarState: Int,
+            remoteInputActive: Boolean,
+            forcePluginOpen: Boolean,
+            dozing: Boolean,
+            scrimsVisibility: Int,
+            backgroundBlurRadius: Int,
+        ) {
+            buffer.advance().apply {
+                this.keyguardShowing = keyguardShowing
+                this.keyguardOccluded = keyguardOccluded
+                this.keyguardNeedsInput = keyguardNeedsInput
+                this.panelVisible = panelVisible
+                this.panelExpanded = panelExpanded
+                this.notificationShadeFocusable = notificationShadeFocusable
+                this.bouncerShowing = bouncerShowing
+                this.keyguardFadingAway = keyguardFadingAway
+                this.keyguardGoingAway = keyguardGoingAway
+                this.qsExpanded = qsExpanded
+                this.headsUpNotificationShowing = headsUpShowing
+                this.lightRevealScrimOpaque = lightRevealScrimOpaque
+                this.forceWindowCollapsed = forceCollapsed
+                this.forceDozeBrightness = forceDozeBrightness
+                this.forceUserActivity = forceUserActivity
+                this.launchingActivityFromNotification = launchingActivity
+                this.mediaBackdropShowing = backdropShowing
+                this.wallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode
+                this.windowNotTouchable = notTouchable
+                this.componentsForcingTopUi.clear()
+                this.componentsForcingTopUi.addAll(componentsForcingTopUi)
+                this.forceOpenTokens.clear()
+                this.forceOpenTokens.addAll(forceOpenTokens)
+                this.statusBarState = statusBarState
+                this.remoteInputActive = remoteInputActive
+                this.forcePluginOpen = forcePluginOpen
+                this.dozing = dozing
+                this.scrimsVisibility = scrimsVisibility
+                this.backgroundBlurRadius = backgroundBlurRadius
+            }
+        }
+
+        /**
+         * Returns the content of the buffer (sorted from latest to newest).
+         *
+         * @see [NotificationShadeWindowState.asStringList]
+         */
+        fun toList(): List<Row> {
+            return buffer.asSequence().map { it.asStringList }.toList()
+        }
+    }
+
+    companion object {
+        /** Headers for dumping a table using [DumpsysTableLogger]. */
+        @JvmField
+        val TABLE_HEADERS =
+            listOf(
+                "keyguardShowing",
+                "keyguardOccluded",
+                "keyguardNeedsInput",
+                "panelVisible",
+                "panelExpanded",
+                "notificationShadeFocusable",
+                "bouncerShowing",
+                "keyguardFadingAway",
+                "keyguardGoingAway",
+                "qsExpanded",
+                "headsUpShowing",
+                "lightRevealScrimOpaque",
+                "forceCollapsed",
+                "forceDozeBrightness",
+                "forceUserActivity",
+                "launchingActivity",
+                "backdropShowing",
+                "wallpaperSupportsAmbientMode",
+                "notTouchable",
+                "componentsForcingTopUi",
+                "forceOpenTokens",
+                "statusBarState",
+                "remoteInputActive",
+                "forcePluginOpen",
+                "dozing",
+                "scrimsVisibility",
+                "backgroundBlurRadius"
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index 6acf417..74a61a3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.shade;
 
-import static android.os.Trace.TRACE_TAG_ALWAYS;
+import static android.os.Trace.TRACE_TAG_APP;
 import static android.view.WindowInsets.Type.systemBars;
 
 import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG;
@@ -58,6 +58,7 @@
 import com.android.internal.view.FloatingActionMode;
 import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
 import com.android.systemui.R;
+import com.android.systemui.compose.ComposeFacade;
 
 /**
  * Combined keyguard and notification panel view. Also holding backdrop and scrims.
@@ -149,6 +150,18 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         setWillNotDraw(!DEBUG);
+
+        if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+            ComposeFacade.INSTANCE.composeInitializer().onAttachedToWindow(this);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+            ComposeFacade.INSTANCE.composeInitializer().onDetachedFromWindow(this);
+        }
     }
 
     @Override
@@ -315,7 +328,7 @@
 
     @Override
     public void requestLayout() {
-        Trace.instant(TRACE_TAG_ALWAYS, "NotificationShadeWindowView#requestLayout");
+        Trace.instant(TRACE_TAG_APP, "NotificationShadeWindowView#requestLayout");
         super.requestLayout();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 5c1ddd6..c130b39 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.shade;
 
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+
 import android.app.StatusBarManager;
 import android.media.AudioManager;
 import android.media.session.MediaSessionLegacyHelper;
@@ -36,11 +38,14 @@
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dock.DockManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationInsetsController;
@@ -57,6 +62,7 @@
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 
 import java.io.PrintWriter;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
@@ -79,6 +85,7 @@
     private final AmbientState mAmbientState;
     private final PulsingGestureListener mPulsingGestureListener;
     private final NotificationInsetsController mNotificationInsetsController;
+    private final AlternateBouncerInteractor mAlternateBouncerInteractor;
 
     private GestureDetector mPulsingWakeupGestureHandler;
     private View mBrightnessMirror;
@@ -96,6 +103,13 @@
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private boolean mIsTrackingBarGesture = false;
+    private boolean mIsOcclusionTransitionRunning = false;
+
+    private final Consumer<TransitionStep> mLockscreenToDreamingTransition =
+            (TransitionStep step) -> {
+                mIsOcclusionTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
 
     @Inject
     public NotificationShadeWindowViewController(
@@ -117,9 +131,11 @@
             NotificationInsetsController notificationInsetsController,
             AmbientState ambientState,
             PulsingGestureListener pulsingGestureListener,
-            FeatureFlags featureFlags,
             KeyguardBouncerViewModel keyguardBouncerViewModel,
-            KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory
+            KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
+            AlternateBouncerInteractor alternateBouncerInteractor,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
+            PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel
     ) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
@@ -139,15 +155,18 @@
         mAmbientState = ambientState;
         mPulsingGestureListener = pulsingGestureListener;
         mNotificationInsetsController = notificationInsetsController;
+        mAlternateBouncerInteractor = alternateBouncerInteractor;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
-        if (featureFlags.isEnabled(Flags.MODERN_BOUNCER)) {
-            KeyguardBouncerViewBinder.bind(
-                    mView.findViewById(R.id.keyguard_bouncer_container),
-                    keyguardBouncerViewModel,
-                    keyguardBouncerComponentFactory);
-        }
+        KeyguardBouncerViewBinder.bind(
+                mView.findViewById(R.id.keyguard_bouncer_container),
+                keyguardBouncerViewModel,
+                primaryBouncerToGoneTransitionViewModel,
+                keyguardBouncerComponentFactory);
+
+        collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(),
+                mLockscreenToDreamingTransition);
     }
 
     /**
@@ -215,6 +234,10 @@
                     return true;
                 }
 
+                if (mIsOcclusionTransitionRunning) {
+                    return false;
+                }
+
                 mFalsingCollector.onTouchEvent(ev);
                 mPulsingWakeupGestureHandler.onTouchEvent(ev);
                 mStatusBarKeyguardViewManager.onTouch(ev);
@@ -292,7 +315,7 @@
                     return true;
                 }
 
-                if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+                if (mAlternateBouncerInteractor.isVisibleState()) {
                     // capture all touches if the alt auth bouncer is showing
                     return true;
                 }
@@ -330,7 +353,7 @@
                     handled = !mService.isPulsing();
                 }
 
-                if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+                if (mAlternateBouncerInteractor.isVisibleState()) {
                     // eat the touch
                     handled = true;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 85b259e..fb7c5c2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -29,8 +29,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.R
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.fragments.FragmentService
 import com.android.systemui.navigationbar.NavigationModeController
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.plugins.qs.QSContainerController
@@ -48,13 +47,13 @@
 internal const val INSET_DEBOUNCE_MILLIS = 500L
 
 class NotificationsQSContainerController @Inject constructor(
-    view: NotificationsQuickSettingsContainer,
-    private val navigationModeController: NavigationModeController,
-    private val overviewProxyService: OverviewProxyService,
-    private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
-    private val shadeExpansionStateManager: ShadeExpansionStateManager,
-    private val featureFlags: FeatureFlags,
-    @Main private val delayableExecutor: DelayableExecutor
+        view: NotificationsQuickSettingsContainer,
+        private val navigationModeController: NavigationModeController,
+        private val overviewProxyService: OverviewProxyService,
+        private val shadeHeaderController: ShadeHeaderController,
+        private val shadeExpansionStateManager: ShadeExpansionStateManager,
+        private val fragmentService: FragmentService,
+        @Main private val delayableExecutor: DelayableExecutor
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
     private var qsExpanded = false
@@ -73,8 +72,6 @@
     private var panelMarginHorizontal = 0
     private var topMargin = 0
 
-    private val useCombinedQSHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)
-
     private var isGestureNavigation = true
     private var taskbarVisible = false
     private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
@@ -128,6 +125,7 @@
         mView.setInsetsChangedListener(delayedInsetSetter)
         mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
         mView.setConfigurationChangedListener { updateResources() }
+        fragmentService.getFragmentHostManager(mView).addTagListener(QS.TAG, mView)
     }
 
     override fun onViewDetached() {
@@ -136,6 +134,7 @@
         mView.removeOnInsetsChangedListener()
         mView.removeQSFragmentAttachedListener()
         mView.setConfigurationChangedListener(null)
+        fragmentService.getFragmentHostManager(mView).removeTagListener(QS.TAG, mView)
     }
 
     fun updateResources() {
@@ -180,7 +179,7 @@
     override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
         if (showing != isQSCustomizing) {
             isQSCustomizing = showing
-            largeScreenShadeHeaderController.startCustomizingAnimation(showing, animationDuration)
+            shadeHeaderController.startCustomizingAnimation(showing, animationDuration)
             updateBottomSpacing()
         }
     }
@@ -246,9 +245,7 @@
         if (largeScreenShadeHeaderActive) {
             constraintSet.constrainHeight(R.id.split_shade_status_bar, largeScreenShadeHeaderHeight)
         } else {
-            if (useCombinedQSHeaders) {
-                constraintSet.constrainHeight(R.id.split_shade_status_bar, WRAP_CONTENT)
-            }
+            constraintSet.constrainHeight(R.id.split_shade_status_bar, WRAP_CONTENT)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
index 02316b7..f73dde6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
@@ -29,7 +29,6 @@
 import androidx.constraintlayout.widget.ConstraintSet;
 
 import com.android.systemui.R;
-import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
@@ -133,18 +132,6 @@
     }
 
     @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        FragmentHostManager.get(this).addTagListener(QS.TAG, this);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        FragmentHostManager.get(this).removeTagListener(QS.TAG, this);
-    }
-
-    @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mInsetsChangedListener.accept(insets);
         return insets;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index d71fbf6..c8b6a2e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -7,6 +7,7 @@
 per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com
 per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com
 per-file *ShadeHeader* = kozynski@google.com, asc@google.com
+per-file *Shade* = justinweir@google.com
 
 per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
 per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index db70065..b42bdaa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -19,7 +19,6 @@
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.PowerManager
 import android.os.SystemClock
-import android.os.UserHandle
 import android.provider.Settings
 import android.view.GestureDetector
 import android.view.MotionEvent
@@ -29,6 +28,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
 import com.android.systemui.tuner.TunerService
@@ -54,6 +54,7 @@
         private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
         private val statusBarStateController: StatusBarStateController,
         private val shadeLogger: ShadeLogger,
+        userTracker: UserTracker,
         tunerService: TunerService,
         dumpManager: DumpManager
 ) : GestureDetector.SimpleOnGestureListener(), Dumpable {
@@ -65,10 +66,10 @@
             when (key) {
                 Settings.Secure.DOZE_DOUBLE_TAP_GESTURE ->
                     doubleTapEnabled = ambientDisplayConfiguration.doubleTapGestureEnabled(
-                            UserHandle.USER_CURRENT)
+                            userTracker.userId)
                 Settings.Secure.DOZE_TAP_SCREEN_GESTURE ->
                     singleTapEnabled = ambientDisplayConfiguration.tapGestureEnabled(
-                            UserHandle.USER_CURRENT)
+                            userTracker.userId)
             }
         }
         tunerService.addTunable(tunable,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt
new file mode 100644
index 0000000..3eec7fa0e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt
@@ -0,0 +1,70 @@
+package com.android.systemui.shade
+
+import android.content.Context
+import android.view.DisplayCutout
+import com.android.systemui.R
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import javax.inject.Inject
+
+/**
+ * Controls [BatteryMeterView.BatteryPercentMode]. It takes into account cutout and qs-qqs
+ * transition fraction when determining the mode.
+ */
+class QsBatteryModeController
+@Inject
+constructor(
+    private val context: Context,
+    private val insetsProvider: StatusBarContentInsetsProvider,
+) {
+
+    private companion object {
+        // MotionLayout frames are in [0, 100]. Where 0 and 100 are reserved for start and end
+        // frames.
+        const val MOTION_LAYOUT_MAX_FRAME = 100
+        // We add a single buffer frame to ensure that battery view faded out completely when we are
+        // about to change it's state
+        const val BUFFER_FRAME_COUNT = 1
+    }
+
+    private var fadeInStartFraction: Float = 0f
+    private var fadeOutCompleteFraction: Float = 0f
+
+    init {
+        updateResources()
+    }
+
+    /**
+     * Returns an appropriate [BatteryMeterView.BatteryPercentMode] for the [qsExpandedFraction] and
+     * [cutout]. We don't show battery estimation in qqs header on the devices with center cutout.
+     * The result might be null when the battery icon is invisible during the qs-qqs transition
+     * animation.
+     */
+    @BatteryMeterView.BatteryPercentMode
+    fun getBatteryMode(cutout: DisplayCutout?, qsExpandedFraction: Float): Int? =
+        when {
+            qsExpandedFraction > fadeInStartFraction -> BatteryMeterView.MODE_ESTIMATE
+            qsExpandedFraction < fadeOutCompleteFraction ->
+                if (hasCenterCutout(cutout)) {
+                    BatteryMeterView.MODE_ON
+                } else {
+                    BatteryMeterView.MODE_ESTIMATE
+                }
+            else -> null
+        }
+
+    fun updateResources() {
+        fadeInStartFraction =
+            (context.resources.getInteger(R.integer.fade_in_start_frame) - BUFFER_FRAME_COUNT) /
+                MOTION_LAYOUT_MAX_FRAME.toFloat()
+        fadeOutCompleteFraction =
+            (context.resources.getInteger(R.integer.fade_out_complete_frame) + BUFFER_FRAME_COUNT) /
+                MOTION_LAYOUT_MAX_FRAME.toFloat()
+    }
+
+    private fun hasCenterCutout(cutout: DisplayCutout?): Boolean =
+        cutout?.let {
+            !insetsProvider.currentRotationHasCornerCutout() && !it.boundingRectTop.isEmpty
+        }
+            ?: false
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
new file mode 100644
index 0000000..099ad94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -0,0 +1,2087 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.systemui.shade;
+
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
+import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
+import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS;
+import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE;
+import static com.android.systemui.shade.NotificationPanelViewController.FLING_EXPAND;
+import static com.android.systemui.shade.NotificationPanelViewController.FLING_HIDE;
+import static com.android.systemui.shade.NotificationPanelViewController.QS_PARALLAX_AMOUNT;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.Fragment;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.FrameLayout;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.policy.SystemBarUtils;
+import com.android.keyguard.FaceAuthApiRequestReason;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.R;
+import com.android.systemui.animation.Interpolators;
+import com.android.systemui.classifier.Classifier;
+import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.shade.transition.ShadeTransitionController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.QsFrameTranslateController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.stack.AmbientState;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.LargeScreenUtils;
+
+import javax.inject.Inject;
+
+import dagger.Lazy;
+
+/** Handles QuickSettings touch handling, expansion and animation state
+ * TODO (b/264460656) make this dumpable
+ */
+@CentralSurfacesComponent.CentralSurfacesScope
+public class QuickSettingsController {
+    public static final String TAG = "QuickSettingsController";
+
+    private QS mQs;
+    private final Lazy<NotificationPanelViewController> mPanelViewControllerLazy;
+
+    private final NotificationPanelView mPanelView;
+    private final KeyguardStatusBarView mKeyguardStatusBar;
+    private final FrameLayout mQsFrame;
+
+    private final QsFrameTranslateController mQsFrameTranslateController;
+    private final ShadeTransitionController mShadeTransitionController;
+    private final PulseExpansionHandler mPulseExpansionHandler;
+    private final ShadeExpansionStateManager mShadeExpansionStateManager;
+    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
+    private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private final NotificationShadeDepthController mDepthController;
+    private final ShadeHeaderController mShadeHeaderController;
+    private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    private final KeyguardStateController mKeyguardStateController;
+    private final KeyguardBypassController mKeyguardBypassController;
+    private final NotificationRemoteInputManager mRemoteInputManager;
+    private VelocityTracker mQsVelocityTracker;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final ScrimController mScrimController;
+    private final MediaDataManager mMediaDataManager;
+    private final MediaHierarchyManager mMediaHierarchyManager;
+    private final AmbientState mAmbientState;
+    private final RecordingController mRecordingController;
+    private final FalsingCollector mFalsingCollector;
+    private final LockscreenGestureLogger mLockscreenGestureLogger;
+    private final ShadeLogger mShadeLog;
+    private final FeatureFlags mFeatureFlags;
+    private final InteractionJankMonitor mInteractionJankMonitor;
+    private final FalsingManager mFalsingManager;
+    private final AccessibilityManager mAccessibilityManager;
+    private final MetricsLogger mMetricsLogger;
+    private final Resources mResources;
+
+    /** Whether the notifications are displayed full width (no margins on the side). */
+    private boolean mIsFullWidth;
+    private int mTouchSlop;
+    private float mSlopMultiplier;
+    /** the current {@link StatusBarState} */
+    private int mBarState;
+    private int mStatusBarMinHeight;
+    private boolean mScrimEnabled = true;
+    private int mScrimCornerRadius;
+    private int mScreenCornerRadius;
+    private boolean mUseLargeScreenShadeHeader;
+    private int mLargeScreenShadeHeaderHeight;
+    private int mDisplayRightInset = 0; // in pixels
+    private int mDisplayLeftInset = 0; // in pixels
+    private boolean mSplitShadeEnabled;
+    /**
+     * The padding between the start of notifications and the qs boundary on the lockscreen.
+     * On lockscreen, notifications aren't inset this extra amount, but we still want the
+     * qs boundary to be padded.
+     */
+    private int mLockscreenNotificationPadding;
+    private int mSplitShadeNotificationsScrimMarginBottom;
+    private boolean mDozing;
+    private boolean mEnableClipping;
+    private int mFalsingThreshold;
+    /**
+     * Position of the qs bottom during the full shade transition. This is needed as the toppadding
+     * can change during state changes, which makes it much harder to do animations
+     */
+    private int mTransitionToFullShadePosition;
+    private boolean mCollapsedOnDown;
+    private float mShadeExpandedHeight = 0;
+    private boolean mLastShadeFlingWasExpanding;
+
+    private float mInitialHeightOnTouch;
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    /** whether current touch Y delta is above falsing threshold */
+    private boolean mTouchAboveFalsingThreshold;
+    /** whether we are tracking a touch on QS container */
+    private boolean mTracking;
+    /** pointerId of the pointer we're currently tracking */
+    private int mTrackingPointer;
+
+    /**
+     * Indicates that QS is in expanded state which can happen by:
+     * - single pane shade: expanding shade and then expanding QS
+     * - split shade: just expanding shade (QS are expanded automatically)
+     */
+    private boolean mExpanded;
+    /** Indicates QS is at its max height */
+    private boolean mFullyExpanded;
+    /**
+     * Determines if QS should be already expanded when expanding shade.
+     * Used for split shade, two finger gesture as well as accessibility shortcut to QS.
+     * It needs to be set when movement starts as it resets at the end of expansion/collapse.
+     */
+    private boolean mExpandImmediate;
+    private boolean mExpandedWhenExpandingStarted;
+    private boolean mAnimatingHiddenFromCollapsed;
+    private boolean mVisible;
+    private float mExpansionHeight;
+    /**
+     * QS height when QS expansion fraction is 0 so when QS is collapsed. That state doesn't really
+     * exist for split shade so currently this value is always 0 then.
+     */
+    private int mMinExpansionHeight;
+    /** QS height when QS expansion fraction is 1 so qs is fully expanded */
+    private int mMaxExpansionHeight;
+    /** Expansion fraction of the notification shade */
+    private float mShadeExpandedFraction;
+    private int mPeekHeight;
+    private float mLastOverscroll;
+    private boolean mExpansionFromOverscroll;
+    private boolean mExpansionEnabledPolicy = true;
+    private boolean mExpansionEnabledAmbient = true;
+    private float mQuickQsHeaderHeight;
+    /**
+     * Determines if QS should be already expanded when expanding shade.
+     * Used for split shade, two finger gesture as well as accessibility shortcut to QS.
+     * It needs to be set when movement starts as it resets at the end of expansion/collapse.
+     */
+    private boolean mTwoFingerExpandPossible;
+    /** Whether the ongoing gesture might both trigger the expansion in both the view and QS. */
+    private boolean mConflictingExpansionGesture;
+    /**
+     * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
+     * need to take this into account in our panel height calculation.
+     */
+    private boolean mAnimatorExpand;
+
+    /**
+     * The amount of progress we are currently in if we're transitioning to the full shade.
+     * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
+     * shade. This value can also go beyond 1.1 when we're overshooting!
+     */
+    private float mTransitioningToFullShadeProgress;
+    /** Distance a full shade transition takes in order for qs to fully transition to the shade. */
+    private int mDistanceForFullShadeTransition;
+    private boolean mStackScrollerOverscrolling;
+    /** Indicates QS is animating - set by flingQs */
+    private boolean mAnimating;
+    /** Whether the current animator is resetting the qs translation. */
+    private boolean mIsTranslationResettingAnimator;
+    /** Whether the current animator is resetting the pulse expansion after a drag down. */
+    private boolean mIsPulseExpansionResettingAnimator;
+    /** The translation amount for QS for the full shade transition. */
+    private float mTranslationForFullShadeTransition;
+    /** Should we animate the next bounds update. */
+    private boolean mAnimateNextNotificationBounds;
+    /** The delay for the next bounds animation. */
+    private long mNotificationBoundsAnimationDelay;
+    /** The duration of the notification bounds animation. */
+    private long mNotificationBoundsAnimationDuration;
+
+    private final Region mInterceptRegion = new Region();
+    /** The end bounds of a clipping animation. */
+    private final Rect mClippingAnimationEndBounds = new Rect();
+    private final Rect mLastClipBounds = new Rect();
+
+    /** The animator for the qs clipping bounds. */
+    private ValueAnimator mClippingAnimator = null;
+    /** The main animator for QS expansion */
+    private ValueAnimator mExpansionAnimator;
+    /** The animator for QS size change */
+    private ValueAnimator mSizeChangeAnimator;
+
+    private ExpansionHeightListener mExpansionHeightListener;
+    private QsStateUpdateListener mQsStateUpdateListener;
+    private ApplyClippingImmediatelyListener mApplyClippingImmediatelyListener;
+    private FlingQsWithoutClickListener mFlingQsWithoutClickListener;
+    private ExpansionHeightSetToMaxListener mExpansionHeightSetToMaxListener;
+    private final QS.HeightListener mQsHeightListener = this::onHeightChanged;
+    private final Runnable mQsCollapseExpandAction = this::collapseOrExpandQs;
+    private final QS.ScrollListener mQsScrollListener = this::onScroll;
+
+    @Inject
+    public QuickSettingsController(
+            Lazy<NotificationPanelViewController> panelViewControllerLazy,
+            NotificationPanelView panelView,
+            QsFrameTranslateController qsFrameTranslateController,
+            ShadeTransitionController shadeTransitionController,
+            PulseExpansionHandler pulseExpansionHandler,
+            NotificationRemoteInputManager remoteInputManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            NotificationStackScrollLayoutController notificationStackScrollLayoutController,
+            LockscreenShadeTransitionController lockscreenShadeTransitionController,
+            NotificationShadeDepthController notificationShadeDepthController,
+            ShadeHeaderController shadeHeaderController,
+            StatusBarTouchableRegionManager statusBarTouchableRegionManager,
+            KeyguardStateController keyguardStateController,
+            KeyguardBypassController keyguardBypassController,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            ScrimController scrimController,
+            MediaDataManager mediaDataManager,
+            MediaHierarchyManager mediaHierarchyManager,
+            AmbientState ambientState,
+            RecordingController recordingController,
+            FalsingManager falsingManager,
+            FalsingCollector falsingCollector,
+            AccessibilityManager accessibilityManager,
+            LockscreenGestureLogger lockscreenGestureLogger,
+            MetricsLogger metricsLogger,
+            FeatureFlags featureFlags,
+            InteractionJankMonitor interactionJankMonitor,
+            ShadeLogger shadeLog
+    ) {
+        mPanelViewControllerLazy = panelViewControllerLazy;
+        mPanelView = panelView;
+        mQsFrame = mPanelView.findViewById(R.id.qs_frame);
+        mKeyguardStatusBar = mPanelView.findViewById(R.id.keyguard_header);
+        mResources = mPanelView.getResources();
+        mSplitShadeEnabled = LargeScreenUtils.shouldUseSplitNotificationShade(mResources);
+        mQsFrameTranslateController = qsFrameTranslateController;
+        mShadeTransitionController = shadeTransitionController;
+        mPulseExpansionHandler = pulseExpansionHandler;
+        pulseExpansionHandler.setPulseExpandAbortListener(() -> {
+            if (mQs != null) {
+                mQs.animateHeaderSlidingOut();
+            }
+        });
+        mRemoteInputManager = remoteInputManager;
+        mShadeExpansionStateManager = shadeExpansionStateManager;
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
+        mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
+        mDepthController = notificationShadeDepthController;
+        mShadeHeaderController = shadeHeaderController;
+        mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
+        mKeyguardStateController = keyguardStateController;
+        mKeyguardBypassController = keyguardBypassController;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mScrimController = scrimController;
+        mMediaDataManager = mediaDataManager;
+        mMediaHierarchyManager = mediaHierarchyManager;
+        mAmbientState = ambientState;
+        mRecordingController = recordingController;
+        mFalsingManager = falsingManager;
+        mFalsingCollector = falsingCollector;
+        mAccessibilityManager = accessibilityManager;
+
+        mLockscreenGestureLogger = lockscreenGestureLogger;
+        mMetricsLogger = metricsLogger;
+        mShadeLog = shadeLog;
+        mFeatureFlags = featureFlags;
+        mInteractionJankMonitor = interactionJankMonitor;
+
+        mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
+        mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback());
+    }
+
+    @VisibleForTesting
+    void setQs(QS qs) {
+        mQs = qs;
+    }
+
+    public void setExpansionHeightListener(ExpansionHeightListener listener) {
+        mExpansionHeightListener = listener;
+    }
+
+    public void setQsStateUpdateListener(QsStateUpdateListener listener) {
+        mQsStateUpdateListener = listener;
+    }
+
+    public void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) {
+        mApplyClippingImmediatelyListener = listener;
+    }
+
+    public void setFlingQsWithoutClickListener(FlingQsWithoutClickListener listener) {
+        mFlingQsWithoutClickListener = listener;
+    }
+
+    public void setExpansionHeightSetToMaxListener(ExpansionHeightSetToMaxListener callback) {
+        mExpansionHeightSetToMaxListener = callback;
+    }
+
+    void loadDimens() {
+        final ViewConfiguration configuration = ViewConfiguration.get(this.mPanelView.getContext());
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
+        mPeekHeight = mResources.getDimensionPixelSize(R.dimen.qs_peek_height);
+        mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mPanelView.getContext());
+        mScrimCornerRadius = mResources.getDimensionPixelSize(
+                R.dimen.notification_scrim_corner_radius);
+        mScreenCornerRadius = (int) ScreenDecorationsUtils.getWindowCornerRadius(
+                mPanelView.getContext());
+        mFalsingThreshold = mResources.getDimensionPixelSize(R.dimen.qs_falsing_threshold);
+        mLockscreenNotificationPadding = mResources.getDimensionPixelSize(
+                R.dimen.notification_side_paddings);
+        mDistanceForFullShadeTransition = mResources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_qs_transition_distance);
+    }
+
+    void updateResources() {
+        mSplitShadeEnabled = LargeScreenUtils.shouldUseSplitNotificationShade(mResources);
+        if (mQs != null) {
+            mQs.setInSplitShade(mSplitShadeEnabled);
+        }
+        mSplitShadeNotificationsScrimMarginBottom =
+                mResources.getDimensionPixelSize(
+                        R.dimen.split_shade_notifications_scrim_margin_bottom);
+
+        mUseLargeScreenShadeHeader =
+                LargeScreenUtils.shouldUseLargeScreenShadeHeader(mPanelView.getResources());
+        mLargeScreenShadeHeaderHeight =
+                mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
+        int topMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight :
+                mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top);
+        mShadeHeaderController.setLargeScreenActive(mUseLargeScreenShadeHeader);
+        mAmbientState.setStackTopMargin(topMargin);
+
+        mQuickQsHeaderHeight = mLargeScreenShadeHeaderHeight;
+
+        mEnableClipping = mResources.getBoolean(R.bool.qs_enable_clipping);
+    }
+
+    // TODO (b/265054088): move this and others to a CoreStartable
+    void initNotificationStackScrollLayoutController() {
+        mNotificationStackScrollLayoutController.setOverscrollTopChangedListener(
+                new NsslOverscrollTopChangedListener());
+        mNotificationStackScrollLayoutController.setOnStackYChanged(this::onStackYChanged);
+        mNotificationStackScrollLayoutController.setOnScrollListener(this::onNotificationScrolled);
+    }
+
+    private void onStackYChanged(boolean shouldAnimate) {
+        if (isQsFragmentCreated()) {
+            if (shouldAnimate) {
+                setAnimateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_STANDARD,
+                        0 /* delay */);
+            }
+            setClippingBounds();
+        }
+    }
+
+    private void onNotificationScrolled(int newScrollPosition) {
+        updateExpansionEnabledAmbient();
+    }
+
+    @VisibleForTesting
+    void setStatusBarMinHeight(int height) {
+        mStatusBarMinHeight = height;
+    }
+
+    int getHeaderHeight() {
+        return mQs.getHeader().getHeight();
+    }
+
+    /** Returns the padding of the stackscroller when unlocked */
+    int getUnlockedStackScrollerPadding() {
+        return (mQs != null ? mQs.getHeader().getHeight() : 0) + mPeekHeight;
+    }
+
+    public boolean isExpansionEnabled() {
+        return mExpansionEnabledPolicy && mExpansionEnabledAmbient
+                && !mRemoteInputManager.isRemoteInputActive();
+    }
+
+    public float getTransitioningToFullShadeProgress() {
+        return mTransitioningToFullShadeProgress;
+    }
+
+    /** */
+    @VisibleForTesting
+    boolean isExpandImmediate() {
+        return mExpandImmediate;
+    }
+
+    float getInitialTouchY() {
+        return mInitialTouchY;
+    }
+
+    /** Returns whether split shade is enabled and an x coordinate is outside of the QS frame. */
+    private boolean isSplitShadeAndTouchXOutsideQs(float touchX) {
+        return mSplitShadeEnabled && touchX < mQsFrame.getX()
+                || touchX > mQsFrame.getX() + mQsFrame.getWidth();
+    }
+
+    /** Returns whether touch is within QS area */
+    private boolean isTouchInQsArea(float x, float y) {
+        if (isSplitShadeAndTouchXOutsideQs(x)) {
+            return false;
+        }
+        // TODO (b/265193930): remove dependency on NPVC
+        // Let's reject anything at the very bottom around the home handle in gesture nav
+        if (mPanelViewControllerLazy.get().isInGestureNavHomeHandleArea(x, y)) {
+            return false;
+        }
+        return y <= mNotificationStackScrollLayoutController.getBottomMostNotificationBottom()
+                || y <= mQs.getView().getY() + mQs.getView().getHeight();
+    }
+
+    /** Returns whether or not event should open QS */
+    @VisibleForTesting
+    boolean isOpenQsEvent(MotionEvent event) {
+        final int pointerCount = event.getPointerCount();
+        final int action = event.getActionMasked();
+
+        final boolean
+                twoFingerDrag =
+                action == MotionEvent.ACTION_POINTER_DOWN && pointerCount == 2;
+
+        final boolean
+                stylusButtonClickDrag =
+                action == MotionEvent.ACTION_DOWN && (event.isButtonPressed(
+                        MotionEvent.BUTTON_STYLUS_PRIMARY) || event.isButtonPressed(
+                        MotionEvent.BUTTON_STYLUS_SECONDARY));
+
+        final boolean
+                mouseButtonClickDrag =
+                action == MotionEvent.ACTION_DOWN && (event.isButtonPressed(
+                        MotionEvent.BUTTON_SECONDARY) || event.isButtonPressed(
+                        MotionEvent.BUTTON_TERTIARY));
+
+        return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag;
+    }
+
+
+    public boolean getExpanded() {
+        return mExpanded;
+    }
+
+    @VisibleForTesting
+    boolean isTracking() {
+        return mTracking;
+    }
+
+    public boolean getFullyExpanded() {
+        return mFullyExpanded;
+    }
+
+    boolean isGoingBetweenClosedShadeAndExpandedQs() {
+        // Below is true when QS are expanded and we swipe up from the same bottom of panel to
+        // close the whole shade with one motion. Also this will be always true when closing
+        // split shade as there QS are always expanded so every collapsing motion is motion from
+        // expanded QS to closed panel
+        return mExpandImmediate || (mExpanded
+                && !mTracking && !isExpansionAnimating()
+                && !mExpansionFromOverscroll);
+    }
+
+    private boolean isQsFragmentCreated() {
+        return mQs != null;
+    }
+
+    public boolean isCustomizing() {
+        return isQsFragmentCreated() && mQs.isCustomizing();
+    }
+
+    public float getExpansionHeight() {
+        return mExpansionHeight;
+    }
+
+    public boolean getExpandedWhenExpandingStarted() {
+        return mExpandedWhenExpandingStarted;
+    }
+
+    public int getMinExpansionHeight() {
+        return mMinExpansionHeight;
+    }
+
+    public boolean isFullyExpandedAndTouchesDisallowed() {
+        return isQsFragmentCreated() && getFullyExpanded() && disallowTouches();
+    }
+
+    public int getMaxExpansionHeight() {
+        return mMaxExpansionHeight;
+    }
+
+    private boolean isQsFalseTouch() {
+        if (mFalsingManager.isClassifierEnabled()) {
+            return mFalsingManager.isFalseTouch(Classifier.QUICK_SETTINGS);
+        }
+        return !mTouchAboveFalsingThreshold;
+    }
+
+    public int getFalsingThreshold() {
+        return mFalsingThreshold;
+    }
+
+    /**
+     * Returns Whether we should intercept a gesture to open Quick Settings.
+     */
+    public boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
+        boolean keyguardShowing = mBarState == KEYGUARD;
+        if (!isExpansionEnabled() || mCollapsedOnDown || (keyguardShowing
+                && mKeyguardBypassController.getBypassEnabled()) || mSplitShadeEnabled) {
+            return false;
+        }
+        View header = keyguardShowing || mQs == null ? mKeyguardStatusBar : mQs.getHeader();
+        int frameTop = keyguardShowing
+                || mQs == null ? 0 : mQsFrame.getTop();
+        mInterceptRegion.set(
+                /* left= */ (int) mQsFrame.getX(),
+                /* top= */ header.getTop() + frameTop,
+                /* right= */ (int) mQsFrame.getX() + mQsFrame.getWidth(),
+                /* bottom= */ header.getBottom() + frameTop);
+        // Also allow QS to intercept if the touch is near the notch.
+        mStatusBarTouchableRegionManager.updateRegionForNotch(mInterceptRegion);
+        final boolean onHeader = mInterceptRegion.contains((int) x, (int) y);
+
+        if (getExpanded()) {
+            return onHeader || (yDiff < 0 && isTouchInQsArea(x, y));
+        } else {
+            return onHeader;
+        }
+    }
+
+    /** Returns amount header should be translated */
+    private float getHeaderTranslation() {
+        if (mSplitShadeEnabled) {
+            // in split shade QS don't translate, just (un)squish and overshoot
+            return 0;
+        }
+        if (mBarState == KEYGUARD && !mKeyguardBypassController.getBypassEnabled()) {
+            return -mQs.getQsMinExpansionHeight();
+        }
+        float appearAmount = mNotificationStackScrollLayoutController
+                .calculateAppearFraction(mShadeExpandedHeight);
+        float startHeight = -getExpansionHeight();
+        if (mBarState == StatusBarState.SHADE) {
+            // Small parallax as we pull down and clip QS
+            startHeight = -getExpansionHeight() * QS_PARALLAX_AMOUNT;
+        }
+        if (mKeyguardBypassController.getBypassEnabled() && mBarState == KEYGUARD) {
+            appearAmount = mNotificationStackScrollLayoutController.calculateAppearFractionBypass();
+            startHeight = -mQs.getQsMinExpansionHeight();
+        }
+        float translation = MathUtils.lerp(startHeight, 0, Math.min(1.0f, appearAmount));
+        return Math.min(0, translation);
+    }
+
+    /**
+     * Can the panel collapse in this motion because it was started on QQS?
+     *
+     * @param downX the x location where the touch started
+     * @param downY the y location where the touch started
+     * Returns true if the panel could be collapsed because it stared on QQS
+     */
+    public boolean canPanelCollapseOnQQS(float downX, float downY) {
+        if (mCollapsedOnDown || mBarState == KEYGUARD || getExpanded()) {
+            return false;
+        }
+        View header = mQs == null ? mKeyguardStatusBar : mQs.getHeader();
+        return downX >= mQsFrame.getX() && downX <= mQsFrame.getX() + mQsFrame.getWidth()
+                && downY <= header.getBottom();
+    }
+
+    /** Closes the Qs customizer. */
+    public void closeQsCustomizer() {
+        mQs.closeCustomizer();
+    }
+
+    /** Returns whether touches from the notification panel should be disallowed */
+    public boolean disallowTouches() {
+        return mQs.disallowPanelTouches();
+    }
+
+    void setListening(boolean listening) {
+        if (mQs != null) {
+            mQs.setListening(listening);
+        }
+    }
+
+    void hideQsImmediately() {
+        if (mQs != null) {
+            mQs.hideImmediately();
+        }
+    }
+
+    public void setDozing(boolean dozing) {
+        mDozing = dozing;
+    }
+
+    /**
+     * This method closes QS but in split shade it should be used only in special cases: to make
+     * sure QS closes when shade is closed as well. Otherwise it will result in QS disappearing
+     * from split shade
+     */
+    public void closeQs() {
+        if (mSplitShadeEnabled) {
+            mShadeLog.d("Closing QS while in split shade");
+        }
+        cancelExpansionAnimation();
+        setExpansionHeight(getMinExpansionHeight());
+        // qsExpandImmediate is a safety latch in case we're calling closeQS while we're in the
+        // middle of animation - we need to make sure that value is always false when shade if
+        // fully collapsed or expanded
+        setExpandImmediate(false);
+    }
+
+    @VisibleForTesting
+    void setExpanded(boolean expanded) {
+        boolean changed = mExpanded != expanded;
+        if (changed) {
+            mExpanded = expanded;
+            updateQsState();
+            mShadeExpansionStateManager.onQsExpansionChanged(expanded);
+            mShadeLog.logQsExpansionChanged("QS Expansion Changed.", expanded,
+                    getMinExpansionHeight(), getMaxExpansionHeight(),
+                    mStackScrollerOverscrolling, mAnimatorExpand, mAnimating);
+        }
+    }
+
+    void setLastShadeFlingWasExpanding(boolean expanding) {
+        mLastShadeFlingWasExpanding = expanding;
+        mShadeLog.logLastFlingWasExpanding(expanding);
+    }
+
+    /** update Qs height state */
+    public void setExpansionHeight(float height) {
+        checkCorrectSplitShadeState(height);
+        int maxHeight = getMaxExpansionHeight();
+        height = Math.min(Math.max(
+                height, getMinExpansionHeight()), maxHeight);
+        mFullyExpanded = height == maxHeight && maxHeight != 0;
+        boolean qsAnimatingAway = !mAnimatorExpand && mAnimating;
+        if (height > getMinExpansionHeight() && !getExpanded()
+                && !mStackScrollerOverscrolling
+                && !mDozing && !qsAnimatingAway) {
+            setExpanded(true);
+        } else if (height <= getMinExpansionHeight()
+                && getExpanded()) {
+            setExpanded(false);
+        }
+        mExpansionHeight = height;
+        updateExpansion();
+
+        if (mExpansionHeightListener != null) {
+            mExpansionHeightListener.onQsSetExpansionHeightCalled(getFullyExpanded());
+        }
+    }
+
+    /** TODO(b/269742565) Remove this logging */
+    private void checkCorrectSplitShadeState(float height) {
+        if (mSplitShadeEnabled && height == 0
+                && mPanelViewControllerLazy.get().isShadeFullyOpen()) {
+            Log.wtfStack(TAG, "qsExpansion set to 0 while split shade is expanding or open");
+        }
+    }
+
+    /** */
+    public void setHeightOverrideToDesiredHeight() {
+        if (isSizeChangeAnimationRunning() && isQsFragmentCreated()) {
+            mQs.setHeightOverride(mQs.getDesiredHeight());
+        }
+    }
+
+    /** Updates quick setting heights and returns old max height. */
+    int updateHeightsOnShadeLayoutChange() {
+        int oldMaxHeight = getMaxExpansionHeight();
+        if (isQsFragmentCreated()) {
+            updateMinHeight();
+            mMaxExpansionHeight = mQs.getDesiredHeight();
+            mNotificationStackScrollLayoutController.setMaxTopPadding(
+                    getMaxExpansionHeight());
+        }
+        return oldMaxHeight;
+    }
+
+    /** Called when Shade view layout changed. Updates QS expansion or
+     * starts size change animation if height has changed. */
+    void handleShadeLayoutChanged(int oldMaxHeight) {
+        if (mExpanded && mFullyExpanded) {
+            mExpansionHeight = mMaxExpansionHeight;
+            if (mExpansionHeightSetToMaxListener != null) {
+                mExpansionHeightSetToMaxListener.onExpansionHeightSetToMax(true);
+            }
+
+            // Size has changed, start an animation.
+            if (getMaxExpansionHeight() != oldMaxHeight) {
+                startSizeChangeAnimation(oldMaxHeight,
+                        getMaxExpansionHeight());
+            }
+        } else if (!getExpanded()
+                && !isExpansionAnimating()) {
+            setExpansionHeight(getMinExpansionHeight() + mLastOverscroll);
+        } else {
+            mShadeLog.v("onLayoutChange: qs expansion not set");
+        }
+    }
+
+    private boolean isSizeChangeAnimationRunning() {
+        return mSizeChangeAnimator != null;
+    }
+
+    private void startSizeChangeAnimation(int oldHeight, final int newHeight) {
+        if (mSizeChangeAnimator != null) {
+            oldHeight = (int) mSizeChangeAnimator.getAnimatedValue();
+            mSizeChangeAnimator.cancel();
+        }
+        mSizeChangeAnimator = ValueAnimator.ofInt(oldHeight, newHeight);
+        mSizeChangeAnimator.setDuration(300);
+        mSizeChangeAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mSizeChangeAnimator.addUpdateListener(animation -> {
+            if (mExpansionHeightSetToMaxListener != null) {
+                mExpansionHeightSetToMaxListener.onExpansionHeightSetToMax(true);
+            }
+
+            int height = (int) mSizeChangeAnimator.getAnimatedValue();
+            mQs.setHeightOverride(height);
+        });
+        mSizeChangeAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mSizeChangeAnimator = null;
+            }
+        });
+        mSizeChangeAnimator.start();
+    }
+
+    void setNotificationPanelFullWidth(boolean isFullWidth) {
+        mIsFullWidth = isFullWidth;
+        if (mQs != null) {
+            mQs.setIsNotificationPanelFullWidth(isFullWidth);
+        }
+    }
+
+    void setBarState(int barState) {
+        mBarState = barState;
+    }
+
+    /** */
+    public void setExpansionEnabledPolicy(boolean expansionEnabledPolicy) {
+        mExpansionEnabledPolicy = expansionEnabledPolicy;
+        if (mQs != null) {
+            mQs.setHeaderClickable(isExpansionEnabled());
+        }
+    }
+
+    void setOverScrollAmount(int overExpansion) {
+        mQs.setOverScrollAmount(overExpansion);
+    }
+
+    private void setOverScrolling(boolean overscrolling) {
+        mStackScrollerOverscrolling = overscrolling;
+        if (mQs != null) {
+            mQs.setOverscrolling(overscrolling);
+        }
+    }
+
+    /** Sets Qs ScrimEnabled and updates QS state. */
+    public void setScrimEnabled(boolean scrimEnabled) {
+        boolean changed = mScrimEnabled != scrimEnabled;
+        mScrimEnabled = scrimEnabled;
+        if (changed) {
+            updateQsState();
+        }
+    }
+
+    void setCollapsedOnDown(boolean collapsedOnDown) {
+        mCollapsedOnDown = collapsedOnDown;
+    }
+
+    void setShadeExpandedHeight(float shadeExpandedHeight) {
+        mShadeExpandedHeight = shadeExpandedHeight;
+    }
+
+    @VisibleForTesting
+    float getShadeExpandedHeight() {
+        return mShadeExpandedHeight;
+    }
+
+    @VisibleForTesting
+    void setExpandImmediate(boolean expandImmediate) {
+        if (expandImmediate != mExpandImmediate) {
+            mShadeLog.logQsExpandImmediateChanged(expandImmediate);
+            mExpandImmediate = expandImmediate;
+            mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate);
+        }
+    }
+
+    void setTwoFingerExpandPossible(boolean expandPossible) {
+        mTwoFingerExpandPossible = expandPossible;
+    }
+
+    @VisibleForTesting
+    boolean isTwoFingerExpandPossible() {
+        return mTwoFingerExpandPossible;
+    }
+
+    /** Called when Qs starts expanding */
+    private void onExpansionStarted() {
+        cancelExpansionAnimation();
+        // TODO (b/265193930): remove dependency on NPVC
+        mPanelViewControllerLazy.get().cancelHeightAnimator();
+        // end
+
+        // Reset scroll position and apply that position to the expanded height.
+        float height = mExpansionHeight;
+        setExpansionHeight(height);
+        mNotificationStackScrollLayoutController.checkSnoozeLeavebehind();
+
+        // When expanding QS, let's authenticate the user if possible,
+        // this will speed up notification actions.
+        if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) {
+            mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.QS_EXPANDED);
+        }
+    }
+
+    void updateQsState() {
+        boolean qsFullScreen = mExpanded && !mSplitShadeEnabled;
+        mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
+        mNotificationStackScrollLayoutController.setScrollingEnabled(
+                mBarState != KEYGUARD && (!qsFullScreen || mExpansionFromOverscroll));
+
+        if (mQsStateUpdateListener != null) {
+            mQsStateUpdateListener.onQsStateUpdated(mExpanded, mStackScrollerOverscrolling);
+        }
+
+        if (mQs == null) return;
+        mQs.setExpanded(mExpanded);
+    }
+
+    /** update expanded state of QS */
+    public void updateExpansion() {
+        if (mQs == null) return;
+        final float squishiness;
+        if ((mExpandImmediate || mExpanded) && !mSplitShadeEnabled) {
+            squishiness = 1;
+        } else if (mTransitioningToFullShadeProgress > 0.0f) {
+            squishiness = mLockscreenShadeTransitionController.getQsSquishTransitionFraction();
+        } else {
+            squishiness = mNotificationStackScrollLayoutController
+                    .getNotificationSquishinessFraction();
+        }
+        final float qsExpansionFraction = computeExpansionFraction();
+        final float adjustedExpansionFraction = mSplitShadeEnabled
+                ? 1f : computeExpansionFraction();
+        mQs.setQsExpansion(
+                adjustedExpansionFraction,
+                mShadeExpandedFraction,
+                getHeaderTranslation(),
+                squishiness
+        );
+        mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
+        int qsPanelBottomY = calculateBottomPosition(qsExpansionFraction);
+        mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
+        setClippingBounds();
+
+        if (mSplitShadeEnabled) {
+            // In split shade we want to pretend that QS are always collapsed so their behaviour and
+            // interactions don't influence notifications as they do in portrait. But we want to set
+            // 0 explicitly in case we're rotating from non-split shade with QS expansion of 1.
+            mNotificationStackScrollLayoutController.setQsExpansionFraction(0);
+        } else {
+            mNotificationStackScrollLayoutController.setQsExpansionFraction(qsExpansionFraction);
+        }
+
+        mDepthController.setQsPanelExpansion(qsExpansionFraction);
+        mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction);
+
+        // TODO (b/265193930): remove dependency on NPVC
+        float shadeExpandedFraction = mBarState == KEYGUARD
+                ? mPanelViewControllerLazy.get().getLockscreenShadeDragProgress()
+                : mShadeExpandedFraction;
+        mShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction);
+        mShadeHeaderController.setQsExpandedFraction(qsExpansionFraction);
+        mShadeHeaderController.setQsVisible(mVisible);
+    }
+
+    /** */
+    public void updateExpansionEnabledAmbient() {
+        final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsHeaderHeight;
+        mExpansionEnabledAmbient = mSplitShadeEnabled
+                || (mAmbientState.getScrollY() <= scrollRangeToTop);
+        if (mQs != null) {
+            mQs.setHeaderClickable(isExpansionEnabled());
+        }
+    }
+
+    /** Calculate y value of bottom of QS */
+    private int calculateBottomPosition(float qsExpansionFraction) {
+        if (mTransitioningToFullShadeProgress > 0.0f) {
+            return mTransitionToFullShadePosition;
+        } else {
+            int qsBottomYFrom = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
+            int expandedTopMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : 0;
+            int qsBottomYTo = mQs.getDesiredHeight() + expandedTopMargin;
+            return (int) MathUtils.lerp(qsBottomYFrom, qsBottomYTo, qsExpansionFraction);
+        }
+    }
+
+    /** Calculate fraction of current QS expansion state */
+    public float computeExpansionFraction() {
+        if (mAnimatingHiddenFromCollapsed) {
+            // When hiding QS from collapsed state, the expansion can sometimes temporarily
+            // be larger than 0 because of the timing, leading to flickers.
+            return 0.0f;
+        }
+        return Math.min(
+                1f, (mExpansionHeight - mMinExpansionHeight) / (mMaxExpansionHeight
+                        - mMinExpansionHeight));
+    }
+
+    void updateMinHeight() {
+        float previousMin = mMinExpansionHeight;
+        if (mBarState == KEYGUARD || mSplitShadeEnabled) {
+            mMinExpansionHeight = 0;
+        } else {
+            mMinExpansionHeight = mQs.getQsMinExpansionHeight();
+        }
+        if (mExpansionHeight == previousMin) {
+            mExpansionHeight = mMinExpansionHeight;
+        }
+    }
+
+    void updateQsFrameTranslation() {
+        // TODO (b/265193930): remove dependency on NPVC
+        mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs,
+                mPanelViewControllerLazy.get().getNavigationBarBottomHeight()
+                        + mAmbientState.getStackTopMargin());
+    }
+
+    /** Called when shade starts expanding. */
+    public void onExpandingStarted(boolean qsFullyExpanded) {
+        mNotificationStackScrollLayoutController.onExpansionStarted();
+        mExpandedWhenExpandingStarted = qsFullyExpanded;
+        mMediaHierarchyManager.setCollapsingShadeFromQS(mExpandedWhenExpandingStarted
+                /* We also start expanding when flinging closed Qs. Let's exclude that */
+                && !mAnimating);
+        if (mExpanded) {
+            onExpansionStarted();
+        }
+        // Since there are QS tiles in the header now, we need to make sure we start listening
+        // immediately so they can be up to date.
+        if (mQs == null) return;
+        mQs.setHeaderListening(true);
+    }
+
+    /** Set animate next notification bounds. */
+    private void setAnimateNextNotificationBounds(long duration, long delay) {
+        mAnimateNextNotificationBounds = true;
+        mNotificationBoundsAnimationDuration = duration;
+        mNotificationBoundsAnimationDelay = delay;
+    }
+
+    /**
+     * Updates scrim bounds, QS clipping, notifications clipping and keyguard status view clipping
+     * as well based on the bounds of the shade and QS state.
+     */
+    private void setClippingBounds() {
+        float qsExpansionFraction = computeExpansionFraction();
+        final int qsPanelBottomY = calculateBottomPosition(qsExpansionFraction);
+        final boolean qsVisible = (qsExpansionFraction > 0 || qsPanelBottomY > 0);
+        checkCorrectScrimVisibility(qsExpansionFraction);
+
+        int top = calculateTopClippingBound(qsPanelBottomY);
+        int bottom = calculateBottomClippingBound(top);
+        int left = calculateLeftClippingBound();
+        int right = calculateRightClippingBound();
+        // top should never be lower than bottom, otherwise it will be invisible.
+        top = Math.min(top, bottom);
+        applyClippingBounds(left, top, right, bottom, qsVisible);
+    }
+
+    /**
+     * Applies clipping to quick settings, notifications layout and
+     * updates bounds of the notifications background (notifications scrim).
+     *
+     * The parameters are bounds of the notifications area rectangle, this function
+     * calculates bounds for the QS clipping based on the notifications bounds.
+     */
+    private void applyClippingBounds(int left, int top, int right, int bottom,
+            boolean qsVisible) {
+        if (!mAnimateNextNotificationBounds || mLastClipBounds.isEmpty()) {
+            if (mClippingAnimator != null) {
+                // update the end position of the animator
+                mClippingAnimationEndBounds.set(left, top, right, bottom);
+            } else {
+                applyClippingImmediately(left, top, right, bottom, qsVisible);
+            }
+        } else {
+            mClippingAnimationEndBounds.set(left, top, right, bottom);
+            final int startLeft = mLastClipBounds.left;
+            final int startTop = mLastClipBounds.top;
+            final int startRight = mLastClipBounds.right;
+            final int startBottom = mLastClipBounds.bottom;
+            if (mClippingAnimator != null) {
+                mClippingAnimator.cancel();
+            }
+            mClippingAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+            mClippingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+            mClippingAnimator.setDuration(mNotificationBoundsAnimationDuration);
+            mClippingAnimator.setStartDelay(mNotificationBoundsAnimationDelay);
+            mClippingAnimator.addUpdateListener(animation -> {
+                float fraction = animation.getAnimatedFraction();
+                int animLeft = (int) MathUtils.lerp(startLeft,
+                        mClippingAnimationEndBounds.left, fraction);
+                int animTop = (int) MathUtils.lerp(startTop,
+                        mClippingAnimationEndBounds.top, fraction);
+                int animRight = (int) MathUtils.lerp(startRight,
+                        mClippingAnimationEndBounds.right, fraction);
+                int animBottom = (int) MathUtils.lerp(startBottom,
+                        mClippingAnimationEndBounds.bottom, fraction);
+                applyClippingImmediately(animLeft, animTop, animRight, animBottom,
+                        qsVisible /* qsVisible */);
+            });
+            mClippingAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mClippingAnimator = null;
+                    mIsTranslationResettingAnimator = false;
+                    mIsPulseExpansionResettingAnimator = false;
+                }
+            });
+            mClippingAnimator.start();
+        }
+        mAnimateNextNotificationBounds = false;
+        mNotificationBoundsAnimationDelay = 0;
+    }
+
+    private void applyClippingImmediately(int left, int top, int right, int bottom,
+            boolean qsVisible) {
+        int radius = mScrimCornerRadius;
+        boolean clipStatusView = false;
+        mLastClipBounds.set(left, top, right, bottom);
+        if (mIsFullWidth) {
+            clipStatusView = qsVisible;
+            float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius;
+            radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius,
+                    Math.min(top / (float) mScrimCornerRadius, 1f));
+        }
+        if (isQsFragmentCreated()) {
+            float qsTranslation = 0;
+            boolean pulseExpanding = mPulseExpansionHandler.isExpanding();
+            if (mTransitioningToFullShadeProgress > 0.0f
+                    || pulseExpanding || (mClippingAnimator != null
+                    && (mIsTranslationResettingAnimator || mIsPulseExpansionResettingAnimator))) {
+                if (pulseExpanding || mIsPulseExpansionResettingAnimator) {
+                    // qsTranslation should only be positive during pulse expansion because it's
+                    // already translating in from the top
+                    qsTranslation = Math.max(0, (top - getHeaderHeight()) / 2.0f);
+                } else if (!mSplitShadeEnabled) {
+                    qsTranslation = (top - getHeaderHeight()) * QS_PARALLAX_AMOUNT;
+                }
+            }
+            mTranslationForFullShadeTransition = qsTranslation;
+            updateQsFrameTranslation();
+            float currentTranslation = mQsFrame.getTranslationY();
+            int clipTop = mEnableClipping
+                    ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
+            int clipBottom = mEnableClipping
+                    ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
+            mVisible = qsVisible;
+            mQs.setQsVisible(qsVisible);
+            mQs.setFancyClipping(
+                    clipTop,
+                    clipBottom,
+                    radius,
+                    qsVisible && !mSplitShadeEnabled);
+
+        }
+
+        // Increase the height of the notifications scrim when not in split shade
+        // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
+        // in this case they are rendered off-screen
+        final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
+        mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
+
+        if (mApplyClippingImmediatelyListener != null) {
+            mApplyClippingImmediatelyListener.onQsClippingImmediatelyApplied(clipStatusView,
+                    mLastClipBounds, top, isQsFragmentCreated(), mVisible);
+        }
+
+        mScrimController.setScrimCornerRadius(radius);
+
+        // Convert global clipping coordinates to local ones,
+        // relative to NotificationStackScrollLayout
+        int nsslLeft = calculateNsslLeft(left);
+        int nsslRight = calculateNsslRight(right);
+        int nsslTop = getNotificationsClippingTopBounds(top);
+        int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
+        int bottomRadius = mSplitShadeEnabled ? radius : 0;
+        // TODO (b/265193930): remove dependency on NPVC
+        int topRadius = mSplitShadeEnabled
+                && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
+        mNotificationStackScrollLayoutController.setRoundedClippingBounds(
+                nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+    }
+
+    void setDisplayInsets(int leftInset, int rightInset) {
+        mDisplayLeftInset = leftInset;
+        mDisplayRightInset = rightInset;
+    }
+
+    private int calculateNsslLeft(int nsslLeftAbsolute) {
+        int left = nsslLeftAbsolute - mNotificationStackScrollLayoutController.getLeft();
+        if (mIsFullWidth) {
+            return left;
+        }
+        return left - mDisplayLeftInset;
+    }
+
+    private int calculateNsslRight(int nsslRightAbsolute) {
+        int right = nsslRightAbsolute - mNotificationStackScrollLayoutController.getLeft();
+        if (mIsFullWidth) {
+            return right;
+        }
+        return right - mDisplayLeftInset;
+    }
+
+    private int getNotificationsClippingTopBounds(int qsTop) {
+        // TODO (b/265193930): remove dependency on NPVC
+        if (mSplitShadeEnabled && mPanelViewControllerLazy.get().isExpandingFromHeadsUp()) {
+            // in split shade nssl has extra top margin so clipping at top 0 is not enough, we need
+            // to set top clipping bound to negative value to allow HUN to go up to the top edge of
+            // the screen without clipping.
+            return -mAmbientState.getStackTopMargin();
+        } else {
+            return qsTop - mNotificationStackScrollLayoutController.getTop();
+        }
+    }
+
+    private void checkCorrectScrimVisibility(float expansionFraction) {
+        // issues with scrims visible on keyguard occur only in split shade
+        if (mSplitShadeEnabled) {
+            // TODO (b/265193930): remove dependency on NPVC
+            boolean keyguardViewsVisible = mBarState == KEYGUARD
+                            && mPanelViewControllerLazy.get().getKeyguardOnlyContentAlpha() == 1;
+            // expansionFraction == 1 means scrims are fully visible as their size/visibility depend
+            // on QS expansion
+            if (expansionFraction == 1 && keyguardViewsVisible) {
+                Log.wtf(TAG,
+                        "Incorrect state, scrim is visible at the same time when clock is visible");
+            }
+        }
+    }
+
+    /** Calculate top padding for notifications */
+    public float calculateNotificationsTopPadding(boolean isShadeExpanding,
+            int keyguardNotificationStaticPadding, float expandedFraction) {
+        boolean keyguardShowing = mBarState == KEYGUARD;
+        if (mSplitShadeEnabled) {
+            return keyguardShowing
+                    ? keyguardNotificationStaticPadding : 0;
+        }
+        if (keyguardShowing && (isExpandImmediate()
+                || isShadeExpanding && getExpandedWhenExpandingStarted())) {
+
+            // Either QS pushes the notifications down when fully expanded, or QS is fully above the
+            // notifications (mostly on tablets). maxNotificationPadding denotes the normal top
+            // padding on Keyguard, maxQsPadding denotes the top padding from the quick settings
+            // panel. We need to take the maximum and linearly interpolate with the panel expansion
+            // for a nice motion.
+            int maxQsPadding = getMaxExpansionHeight();
+            int max = keyguardShowing ? Math.max(
+                    keyguardNotificationStaticPadding, maxQsPadding) : maxQsPadding;
+            return (int) MathUtils.lerp((float) getMinExpansionHeight(),
+                    (float) max, expandedFraction);
+        } else if (isSizeChangeAnimationRunning()) {
+            return Math.max((int) mSizeChangeAnimator.getAnimatedValue(),
+                    keyguardNotificationStaticPadding);
+        } else if (keyguardShowing) {
+            // We can only do the smoother transition on Keyguard when we also are not collapsing
+            // from a scrolled quick settings.
+            return MathUtils.lerp((float) keyguardNotificationStaticPadding,
+                    (float) (getMaxExpansionHeight()), computeExpansionFraction());
+        } else {
+            return mQsFrameTranslateController.getNotificationsTopPadding(
+                    mExpansionHeight, mNotificationStackScrollLayoutController);
+        }
+    }
+
+    /** Calculate height of QS panel */
+    public int calculatePanelHeightExpanded(int stackScrollerPadding) {
+        float
+                notificationHeight =
+                mNotificationStackScrollLayoutController.getHeight()
+                        - mNotificationStackScrollLayoutController.getEmptyBottomMargin()
+                        - mNotificationStackScrollLayoutController.getTopPadding();
+
+        // When only empty shade view is visible in QS collapsed state, simulate that we would have
+        // it in expanded QS state as well so we don't run into troubles when fading the view in/out
+        // and expanding/collapsing the whole panel from/to quick settings.
+        if (mNotificationStackScrollLayoutController.getNotGoneChildCount() == 0
+                && mNotificationStackScrollLayoutController.isShowingEmptyShadeView()) {
+            notificationHeight = mNotificationStackScrollLayoutController.getEmptyShadeViewHeight();
+        }
+        int maxQsHeight = mMaxExpansionHeight;
+
+        // If an animation is changing the size of the QS panel, take the animated value.
+        if (mSizeChangeAnimator != null) {
+            maxQsHeight = (int) mSizeChangeAnimator.getAnimatedValue();
+        }
+        float totalHeight = Math.max(maxQsHeight, mBarState == KEYGUARD ? stackScrollerPadding : 0)
+                + notificationHeight
+                + mNotificationStackScrollLayoutController.getTopPaddingOverflow();
+        if (totalHeight > mNotificationStackScrollLayoutController.getHeight()) {
+            float
+                    fullyCollapsedHeight =
+                    maxQsHeight + mNotificationStackScrollLayoutController.getLayoutMinHeight();
+            totalHeight = Math.max(fullyCollapsedHeight,
+                    mNotificationStackScrollLayoutController.getHeight());
+        }
+        return (int) totalHeight;
+    }
+
+    private float getEdgePosition() {
+        // TODO: replace StackY with unified calculation
+        return Math.max(mQuickQsHeaderHeight * mAmbientState.getExpansionFraction(),
+                mAmbientState.getStackY()
+                        // need to adjust for extra margin introduced by large screen shade header
+                        + mAmbientState.getStackTopMargin() * mAmbientState.getExpansionFraction()
+                        - mAmbientState.getScrollY());
+    }
+
+    private int calculateTopClippingBound(int qsPanelBottomY) {
+        int top;
+        if (mSplitShadeEnabled) {
+            top = Math.min(qsPanelBottomY, mLargeScreenShadeHeaderHeight);
+        } else {
+            if (mTransitioningToFullShadeProgress > 0.0f) {
+                // If we're transitioning, let's use the actual value. The else case
+                // can be wrong during transitions when waiting for the keyguard to unlock
+                top = mTransitionToFullShadePosition;
+            } else {
+                final float notificationTop = getEdgePosition();
+                if (mBarState == KEYGUARD) {
+                    if (mKeyguardBypassController.getBypassEnabled()) {
+                        // When bypassing on the keyguard, let's use the panel bottom.
+                        // this should go away once we unify the stackY position and don't have
+                        // to do this min anymore below.
+                        top = qsPanelBottomY;
+                    } else {
+                        top = (int) Math.min(qsPanelBottomY, notificationTop);
+                    }
+                } else {
+                    top = (int) notificationTop;
+                }
+            }
+            // TODO (b/265193930): remove dependency on NPVC
+            top += mPanelViewControllerLazy.get().getOverStretchAmount();
+            // Correction for instant expansion caused by HUN pull down/
+            float minFraction = mPanelViewControllerLazy.get().getMinFraction();
+            if (minFraction > 0f && minFraction < 1f) {
+                float realFraction = (mShadeExpandedFraction
+                        - minFraction) / (1f - minFraction);
+                top *= MathUtils.saturate(realFraction / minFraction);
+            }
+        }
+        return top;
+    }
+
+    private int calculateBottomClippingBound(int top) {
+        if (mSplitShadeEnabled) {
+            return top + mNotificationStackScrollLayoutController.getHeight()
+                    + mSplitShadeNotificationsScrimMarginBottom;
+        } else {
+            return mPanelView.getBottom();
+        }
+    }
+
+    private int calculateLeftClippingBound() {
+        if (mIsFullWidth) {
+            // left bounds can ignore insets, it should always reach the edge of the screen
+            return 0;
+        } else {
+            return mNotificationStackScrollLayoutController.getLeft()
+                    + mDisplayLeftInset;
+        }
+    }
+
+    private int calculateRightClippingBound() {
+        if (mIsFullWidth) {
+            return mPanelView.getRight()
+                    + mDisplayRightInset;
+        } else {
+            return mNotificationStackScrollLayoutController.getRight()
+                    + mDisplayLeftInset;
+        }
+    }
+
+    private void trackMovement(MotionEvent event) {
+        if (mQsVelocityTracker != null) mQsVelocityTracker.addMovement(event);
+    }
+
+    private void initVelocityTracker() {
+        if (mQsVelocityTracker != null) {
+            mQsVelocityTracker.recycle();
+        }
+        mQsVelocityTracker = VelocityTracker.obtain();
+    }
+
+    private float getCurrentVelocity() {
+        if (mQsVelocityTracker == null) {
+            return 0;
+        }
+        mQsVelocityTracker.computeCurrentVelocity(1000);
+        return mQsVelocityTracker.getYVelocity();
+    }
+
+    boolean updateAndGetTouchAboveFalsingThreshold() {
+        mTouchAboveFalsingThreshold = mFullyExpanded;
+        return mTouchAboveFalsingThreshold;
+    }
+
+    @VisibleForTesting
+    void onHeightChanged() {
+        mMaxExpansionHeight = isQsFragmentCreated() ? mQs.getDesiredHeight() : 0;
+        if (mExpanded && mFullyExpanded) {
+            mExpansionHeight = mMaxExpansionHeight;
+            if (mExpansionHeightSetToMaxListener != null) {
+                mExpansionHeightSetToMaxListener.onExpansionHeightSetToMax(true);
+            }
+        }
+        if (mAccessibilityManager.isEnabled()) {
+            // TODO (b/265193930): remove dependency on NPVC
+            mPanelView.setAccessibilityPaneTitle(
+                    mPanelViewControllerLazy.get().determineAccessibilityPaneTitle());
+        }
+        mNotificationStackScrollLayoutController.setMaxTopPadding(mMaxExpansionHeight);
+    }
+
+    private void collapseOrExpandQs() {
+        if (mSplitShadeEnabled) {
+            return; // QS is always expanded in split shade
+        }
+        onExpansionStarted();
+        if (getExpanded()) {
+            flingQs(0, FLING_COLLAPSE, null, true);
+        } else if (isExpansionEnabled()) {
+            mLockscreenGestureLogger.write(MetricsProto.MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0);
+            flingQs(0, FLING_EXPAND, null, true);
+        }
+    }
+
+    private void onScroll(int scrollY) {
+        mShadeHeaderController.setQsScrollY(scrollY);
+        if (scrollY > 0 && !mFullyExpanded) {
+            // TODO (b/265193930): remove dependency on NPVC
+            // If we are scrolling QS, we should be fully expanded.
+            mPanelViewControllerLazy.get().expandWithQs();
+        }
+    }
+
+    @VisibleForTesting
+    boolean isTrackingBlocked() {
+        return mConflictingExpansionGesture && getExpanded();
+    }
+
+    boolean isExpansionAnimating() {
+        return mExpansionAnimator != null;
+    }
+
+    @VisibleForTesting
+    boolean isConflictingExpansionGesture() {
+        return mConflictingExpansionGesture;
+    }
+
+    /** handles touches in Qs panel area */
+    public boolean handleTouch(MotionEvent event, boolean isFullyCollapsed,
+            boolean isShadeOrQsHeightAnimationRunning) {
+        if (isSplitShadeAndTouchXOutsideQs(event.getX())) {
+            return false;
+        }
+        final int action = event.getActionMasked();
+        boolean collapsedQs = !getExpanded() && !mSplitShadeEnabled;
+        boolean expandedShadeCollapsedQs = mShadeExpandedFraction == 1f
+                && mBarState != KEYGUARD && collapsedQs && isExpansionEnabled();
+        if (action == MotionEvent.ACTION_DOWN && expandedShadeCollapsedQs) {
+            // Down in the empty area while fully expanded - go to QS.
+            mShadeLog.logMotionEvent(event, "handleQsTouch: down action, QS tracking enabled");
+            mTracking = true;
+            traceQsJank(true, false);
+            mConflictingExpansionGesture = true;
+            onExpansionStarted();
+            mInitialHeightOnTouch = mExpansionHeight;
+            mInitialTouchY = event.getY();
+            mInitialTouchX = event.getX();
+        }
+        if (!isFullyCollapsed && !isShadeOrQsHeightAnimationRunning) {
+            handleDown(event);
+        }
+        // defer touches on QQS to shade while shade is collapsing. Added margin for error
+        // as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
+        if (!mSplitShadeEnabled && !mLastShadeFlingWasExpanding
+                && computeExpansionFraction() <= 0.01 && mShadeExpandedFraction < 1.0) {
+            mShadeLog.logMotionEvent(event,
+                    "handleQsTouch: shade touched while shade collapsing, QS tracking disabled");
+            mTracking = false;
+        }
+        if (!isExpandImmediate() && mTracking) {
+            onTouch(event);
+            if (!mConflictingExpansionGesture && !mSplitShadeEnabled) {
+                mShadeLog.logMotionEvent(event,
+                        "handleQsTouch: not immediate expand or conflicting gesture");
+                return true;
+            }
+        }
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+            mConflictingExpansionGesture = false;
+        }
+        if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed && isExpansionEnabled()) {
+            mTwoFingerExpandPossible = true;
+        }
+        if (mTwoFingerExpandPossible && isOpenQsEvent(event)
+                && event.getY(event.getActionIndex())
+                < mStatusBarMinHeight) {
+            mMetricsLogger.count(COUNTER_PANEL_OPEN_QS, 1);
+            setExpandImmediate(true);
+            mNotificationStackScrollLayoutController.setShouldShowShelfOnly(!mSplitShadeEnabled);
+            if (mExpansionHeightSetToMaxListener != null) {
+                mExpansionHeightSetToMaxListener.onExpansionHeightSetToMax(false);
+            }
+
+            // Normally, we start listening when the panel is expanded, but here we need to start
+            // earlier so the state is already up to date when dragging down.
+            setListening(true);
+        }
+        return false;
+    }
+
+    private void handleDown(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN
+                && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) {
+            mFalsingCollector.onQsDown();
+            mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled");
+            mTracking = true;
+            onExpansionStarted();
+            mInitialHeightOnTouch = mExpansionHeight;
+            mInitialTouchY = event.getY();
+            mInitialTouchX = event.getX();
+            // TODO (b/265193930): remove dependency on NPVC
+            // If we interrupt an expansion gesture here, make sure to update the state correctly.
+            mPanelViewControllerLazy.get().notifyExpandingFinished();
+        }
+    }
+
+    private void onTouch(MotionEvent event) {
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float y = event.getY(pointerIndex);
+        final float x = event.getX(pointerIndex);
+        final float h = y - mInitialTouchY;
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mShadeLog.logMotionEvent(event, "onQsTouch: down action, QS tracking enabled");
+                mTracking = true;
+                traceQsJank(true, false);
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                onExpansionStarted();
+                mInitialHeightOnTouch = mExpansionHeight;
+                initVelocityTracker();
+                trackMovement(event);
+                break;
+
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    final float newY = event.getY(newIndex);
+                    final float newX = event.getX(newIndex);
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialHeightOnTouch = mExpansionHeight;
+                    mInitialTouchY = newY;
+                    mInitialTouchX = newX;
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion");
+                setExpansionHeight(h + mInitialHeightOnTouch);
+                // TODO (b/265193930): remove dependency on NPVC
+                if (h >= mPanelViewControllerLazy.get().getFalsingThreshold()) {
+                    mTouchAboveFalsingThreshold = true;
+                }
+                trackMovement(event);
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mShadeLog.logMotionEvent(event,
+                        "onQsTouch: up/cancel action, QS tracking disabled");
+                mTracking = false;
+                mTrackingPointer = -1;
+                trackMovement(event);
+                float fraction = computeExpansionFraction();
+                if (fraction != 0f || y >= mInitialTouchY) {
+                    flingQsWithCurrentVelocity(y,
+                            event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+                } else {
+                    traceQsJank(false,
+                            event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+                }
+                if (mQsVelocityTracker != null) {
+                    mQsVelocityTracker.recycle();
+                    mQsVelocityTracker = null;
+                }
+                break;
+        }
+    }
+
+    /** intercepts touches on Qs panel area. */
+    public boolean onIntercept(MotionEvent event) {
+        int pointerIndex = event.findPointerIndex(mTrackingPointer);
+        if (pointerIndex < 0) {
+            pointerIndex = 0;
+            mTrackingPointer = event.getPointerId(pointerIndex);
+        }
+        final float x = event.getX(pointerIndex);
+        final float y = event.getY(pointerIndex);
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mInitialTouchY = y;
+                mInitialTouchX = x;
+                initVelocityTracker();
+                trackMovement(event);
+                float qsExpansionFraction = computeExpansionFraction();
+                // Intercept the touch if QS is between fully collapsed and fully expanded state
+                if (!mSplitShadeEnabled
+                        && qsExpansionFraction > 0.0 && qsExpansionFraction < 1.0) {
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: down action, QS partially expanded/collapsed");
+                    return true;
+                }
+                // TODO (b/265193930): remove dependency on NPVC
+                if (mPanelViewControllerLazy.get().getKeyguardShowing()
+                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
+                    // Dragging down on the lockscreen statusbar should prohibit other interactions
+                    // immediately, otherwise we'll wait on the touchslop. This is to allow
+                    // dragging down to expanded quick settings directly on the lockscreen.
+                    mPanelView.getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                if (mExpansionAnimator != null) {
+                    mInitialHeightOnTouch = mExpansionHeight;
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: down action, QS tracking enabled");
+                    mTracking = true;
+                    traceQsJank(true, false);
+                    mNotificationStackScrollLayoutController.cancelLongPress();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                final int upPointer = event.getPointerId(event.getActionIndex());
+                if (mTrackingPointer == upPointer) {
+                    // gesture is ongoing, find a new pointer to track
+                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                    mTrackingPointer = event.getPointerId(newIndex);
+                    mInitialTouchX = event.getX(newIndex);
+                    mInitialTouchY = event.getY(newIndex);
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final float h = y - mInitialTouchY;
+                trackMovement(event);
+                if (mTracking) {
+
+                    // Already tracking because onOverscrolled was called. We need to update here
+                    // so we don't stop for a frame until the next touch event gets handled in
+                    // onTouchEvent.
+                    setExpansionHeight(h + mInitialHeightOnTouch);
+                    trackMovement(event);
+                    return true;
+                } else {
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: move ignored because qs tracking disabled");
+                }
+                // TODO (b/265193930): remove dependency on NPVC
+                float touchSlop = event.getClassification()
+                        == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
+                        ? mTouchSlop * mSlopMultiplier
+                        : mTouchSlop;
+                if ((h > touchSlop || (h < -touchSlop && getExpanded()))
+                        && Math.abs(h) > Math.abs(x - mInitialTouchX)
+                        && shouldQuickSettingsIntercept(
+                        mInitialTouchX, mInitialTouchY, h)) {
+                    mPanelView.getParent().requestDisallowInterceptTouchEvent(true);
+                    mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
+                    mTracking = true;
+                    traceQsJank(true, false);
+                    onExpansionStarted();
+                    mPanelViewControllerLazy.get().notifyExpandingFinished();
+                    mInitialHeightOnTouch = mExpansionHeight;
+                    mInitialTouchY = y;
+                    mInitialTouchX = x;
+                    mNotificationStackScrollLayoutController.cancelLongPress();
+                    return true;
+                } else {
+                    mShadeLog.logQsTrackingNotStarted(mInitialTouchY, y, h, touchSlop,
+                            getExpanded(), mPanelViewControllerLazy.get().getKeyguardShowing(),
+                            isExpansionEnabled());
+                }
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                trackMovement(event);
+                mShadeLog.logMotionEvent(event, "onQsIntercept: up action, QS tracking disabled");
+                mTracking = false;
+                break;
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
+        mShadeExpandedFraction = event.getFraction();
+    }
+
+    /**
+     * Animate QS closing by flinging it.
+     * If QS is expanded, it will collapse into QQS and stop.
+     * If in split shade, it will collapse the whole shade.
+     *
+     * @param animateAway Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
+     */
+    public void animateCloseQs(boolean animateAway) {
+        if (mExpansionAnimator != null) {
+            if (!mAnimatorExpand) {
+                return;
+            }
+            float height = mExpansionHeight;
+            mExpansionAnimator.cancel();
+            setExpansionHeight(height);
+        }
+        flingQs(0 /* vel */, animateAway ? FLING_HIDE : FLING_COLLAPSE);
+    }
+
+    private void cancelExpansionAnimation() {
+        if (mExpansionAnimator != null) {
+            mExpansionAnimator.cancel();
+        }
+    }
+
+    /** @see #flingQs(float, int, Runnable, boolean) */
+    public void flingQs(float vel, int type) {
+        flingQs(vel, type, null /* onFinishRunnable */, false /* isClick */);
+    }
+
+    /**
+     * Animates QS or QQS as if the user had swiped up or down.
+     *
+     * @param vel              Finger velocity or 0 when not initiated by touch events.
+     * @param type             Either FLING_EXPAND, FLING_COLLAPSE or FLING_HIDE.
+     * @param onFinishRunnable Runnable to be executed at the end of animation.
+     * @param isClick          If originated by click (different interpolator and duration.)
+     */
+    private void flingQs(float vel, int type, final Runnable onFinishRunnable,
+            boolean isClick) {
+        mShadeLog.flingQs(type, isClick);
+        float target;
+        switch (type) {
+            case FLING_EXPAND:
+                target = getMaxExpansionHeight();
+                break;
+            case FLING_COLLAPSE:
+                if (mSplitShadeEnabled) { // TODO:(b/269742565) remove below log
+                    Log.wtfStack(TAG, "FLING_COLLAPSE called in split shade");
+                }
+                target = getMinExpansionHeight();
+                break;
+            case FLING_HIDE:
+            default:
+                if (isQsFragmentCreated()) {
+                    mQs.closeDetail();
+                }
+                target = 0;
+        }
+        if (target == mExpansionHeight) {
+            if (onFinishRunnable != null) {
+                onFinishRunnable.run();
+            }
+            traceQsJank(false, type != FLING_EXPAND);
+            return;
+        }
+
+        // If we move in the opposite direction, reset velocity and use a different duration.
+        boolean oppositeDirection = false;
+        boolean expanding = type == FLING_EXPAND;
+        if (vel > 0 && !expanding || vel < 0 && expanding) {
+            vel = 0;
+            oppositeDirection = true;
+        }
+        ValueAnimator animator = ValueAnimator.ofFloat(
+                mExpansionHeight, target);
+        if (isClick) {
+            animator.setInterpolator(Interpolators.TOUCH_RESPONSE);
+            animator.setDuration(368);
+        } else {
+            if (mFlingQsWithoutClickListener != null) {
+                mFlingQsWithoutClickListener.onFlingQsWithoutClick(animator, mExpansionHeight,
+                        target, vel);
+            }
+        }
+        if (oppositeDirection) {
+            animator.setDuration(350);
+        }
+        animator.addUpdateListener(
+                animation -> setExpansionHeight((Float) animation.getAnimatedValue()));
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mIsCanceled;
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mPanelViewControllerLazy.get().notifyExpandingStarted();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mIsCanceled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimatingHiddenFromCollapsed = false;
+                mAnimating = false;
+                mPanelViewControllerLazy.get().notifyExpandingFinished();
+                mNotificationStackScrollLayoutController.resetCheckSnoozeLeavebehind();
+                mExpansionAnimator = null;
+                if (onFinishRunnable != null) {
+                    onFinishRunnable.run();
+                }
+                traceQsJank(false, mIsCanceled);
+            }
+        });
+        // Let's note that we're animating QS. Moving the animator here will cancel it immediately,
+        // so we need a separate flag.
+        mAnimating = true;
+        animator.start();
+        mExpansionAnimator = animator;
+        mAnimatorExpand = expanding;
+        mAnimatingHiddenFromCollapsed =
+                computeExpansionFraction() == 0.0f && target == 0;
+    }
+
+    private void flingQsWithCurrentVelocity(float y, boolean isCancelMotionEvent) {
+        float vel = getCurrentVelocity();
+        // TODO (b/265193930): remove dependency on NPVC
+        boolean expandsQs = mPanelViewControllerLazy.get().flingExpandsQs(vel);
+        if (expandsQs) {
+            if (mFalsingManager.isUnlockingDisabled() || isQsFalseTouch()) {
+                expandsQs = false;
+            } else {
+                logQsSwipeDown(y);
+            }
+        } else if (vel < 0) {
+            mFalsingManager.isFalseTouch(QS_COLLAPSE);
+        }
+
+        int flingType;
+        if (expandsQs && !isCancelMotionEvent) {
+            flingType = FLING_EXPAND;
+        } else if (mSplitShadeEnabled) {
+            flingType = FLING_HIDE;
+        } else {
+            flingType = FLING_COLLAPSE;
+        }
+        flingQs(vel, flingType);
+    }
+
+    private void logQsSwipeDown(float y) {
+        float vel = getCurrentVelocity();
+        final int gesture = mBarState == KEYGUARD ? MetricsProto.MetricsEvent.ACTION_LS_QS
+                : MetricsProto.MetricsEvent.ACTION_SHADE_QS_PULL;
+        // TODO (b/265193930): remove dependency on NPVC
+        float displayDensity = mPanelViewControllerLazy.get().getDisplayDensity();
+        mLockscreenGestureLogger.write(gesture,
+                (int) ((y - getInitialTouchY()) / displayDensity), (int) (vel / displayDensity));
+    }
+
+    /** */
+    public FragmentHostManager.FragmentListener getQsFragmentListener() {
+        return new QsFragmentListener();
+    }
+
+    /** */
+    public final class QsFragmentListener implements FragmentHostManager.FragmentListener {
+        /** */
+        @Override
+        public void onFragmentViewCreated(String tag, Fragment fragment) {
+            mQs = (QS) fragment;
+            mQs.setPanelView(mQsHeightListener);
+            mQs.setCollapseExpandAction(mQsCollapseExpandAction);
+            mQs.setHeaderClickable(isExpansionEnabled());
+            mQs.setOverscrolling(mStackScrollerOverscrolling);
+            mQs.setInSplitShade(mSplitShadeEnabled);
+            mQs.setIsNotificationPanelFullWidth(mIsFullWidth);
+
+            // recompute internal state when qspanel height changes
+            mQs.getView().addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                        final int height = bottom - top;
+                        final int oldHeight = oldBottom - oldTop;
+                        if (height != oldHeight) {
+                            onHeightChanged();
+                        }
+                    });
+            mQs.setCollapsedMediaVisibilityChangedListener((visible) -> {
+                if (mQs.getHeader().isShown()) {
+                    setAnimateNextNotificationBounds(
+                            StackStateAnimator.ANIMATION_DURATION_STANDARD, 0);
+                    mNotificationStackScrollLayoutController.animateNextTopPaddingChange();
+                }
+            });
+            mLockscreenShadeTransitionController.setQS(mQs);
+            mShadeTransitionController.setQs(mQs);
+            mNotificationStackScrollLayoutController.setQsHeader((ViewGroup) mQs.getHeader());
+            mQs.setScrollListener(mQsScrollListener);
+            updateExpansion();
+        }
+
+        /** */
+        @Override
+        public void onFragmentViewDestroyed(String tag, Fragment fragment) {
+            // Manual handling of fragment lifecycle is only required because this bridges
+            // non-fragment and fragment code. Once we are using a fragment for the notification
+            // panel, mQs will not need to be null cause it will be tied to the same lifecycle.
+            if (fragment == mQs) {
+                mQs = null;
+            }
+        }
+    }
+
+    private final class LockscreenShadeTransitionCallback
+            implements LockscreenShadeTransitionController.Callback {
+        /** Called when pulse expansion has finished and this is going to the full shade. */
+        @Override
+        public void onPulseExpansionFinished() {
+            setAnimateNextNotificationBounds(
+                    StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 0);
+            mIsPulseExpansionResettingAnimator = true;
+        }
+
+        @Override
+        public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) {
+            if (animate && mIsFullWidth) {
+                setAnimateNextNotificationBounds(
+                        StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, delay);
+                mIsTranslationResettingAnimator = mTranslationForFullShadeTransition > 0.0f;
+            }
+            float endPosition = 0;
+            if (pxAmount > 0.0f) {
+                if (mSplitShadeEnabled) {
+                    float qsHeight = MathUtils.lerp(getMinExpansionHeight(),
+                            getMaxExpansionHeight(),
+                            mLockscreenShadeTransitionController.getQSDragProgress());
+                    setExpansionHeight(qsHeight);
+                }
+                if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0
+                        && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
+                    // No notifications are visible, let's animate to the height of qs instead
+                    if (isQsFragmentCreated()) {
+                        // Let's interpolate to the header height instead of the top padding,
+                        // because the toppadding is way too low because of the large clock.
+                        // we still want to take into account the edgePosition though as that nicely
+                        // overshoots in the stackscroller
+                        endPosition = getEdgePosition()
+                                - mNotificationStackScrollLayoutController.getTopPadding()
+                                + getHeaderHeight();
+                    }
+                } else {
+                    // Interpolating to the new bottom edge position!
+                    endPosition = getEdgePosition() + mNotificationStackScrollLayoutController
+                            .getFullShadeTransitionInset();
+                    if (mBarState == KEYGUARD) {
+                        endPosition -= mLockscreenNotificationPadding;
+                    }
+                }
+            }
+
+            // Calculate the overshoot amount such that we're reaching the target after our desired
+            // distance, but only reach it fully once we drag a full shade length.
+            mTransitioningToFullShadeProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+                    MathUtils.saturate(pxAmount / mDistanceForFullShadeTransition));
+
+            int position = (int) MathUtils.lerp((float) 0, endPosition,
+                    mTransitioningToFullShadeProgress);
+            if (mTransitioningToFullShadeProgress > 0.0f) {
+                // we want at least 1 pixel otherwise the panel won't be clipped
+                position = Math.max(1, position);
+            }
+            mTransitionToFullShadePosition = position;
+            updateExpansion();
+        }
+    }
+
+    private final class NsslOverscrollTopChangedListener implements
+            NotificationStackScrollLayout.OnOverscrollTopChangedListener {
+        @Override
+        public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
+            // When in split shade, overscroll shouldn't carry through to QS
+            if (mSplitShadeEnabled) {
+                return;
+            }
+            cancelExpansionAnimation();
+            if (!isExpansionEnabled()) {
+                amount = 0f;
+            }
+            float rounded = amount >= 1f ? amount : 0f;
+            setOverScrolling(rounded != 0f && isRubberbanded);
+            mExpansionFromOverscroll = rounded != 0f;
+            mLastOverscroll = rounded;
+            updateQsState();
+            setExpansionHeight(getMinExpansionHeight() + rounded);
+        }
+
+        @Override
+        public void flingTopOverscroll(float velocity, boolean open) {
+            // in split shade mode we want to expand/collapse QS only when touch happens within QS
+            if (isSplitShadeAndTouchXOutsideQs(mInitialTouchX)) {
+                return;
+            }
+            mLastOverscroll = 0f;
+            mExpansionFromOverscroll = false;
+            if (open) {
+                // During overscrolling, qsExpansion doesn't actually change that the qs is
+                // becoming expanded. Any layout could therefore reset the position again. Let's
+                // make sure we can expand
+                setOverScrolling(false);
+            }
+            setExpansionHeight(getExpansionHeight());
+            boolean canExpand = isExpansionEnabled();
+            flingQs(!canExpand && open ? 0f : velocity,
+                    open && canExpand ? FLING_EXPAND : FLING_COLLAPSE, () -> {
+                        setOverScrolling(false);
+                        updateQsState();
+                    }, false);
+        }
+    }
+
+    void beginJankMonitoring(boolean isFullyCollapsed) {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        // TODO (b/265193930): remove dependency on NPVC
+        InteractionJankMonitor.Configuration.Builder builder =
+                InteractionJankMonitor.Configuration.Builder.withView(
+                        InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+                        mPanelView).setTag(isFullyCollapsed ? "Expand" : "Collapse");
+        mInteractionJankMonitor.begin(builder);
+    }
+
+    void endJankMonitoring() {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        InteractionJankMonitor.getInstance().end(
+                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+    }
+
+    void cancelJankMonitoring() {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        InteractionJankMonitor.getInstance().cancel(
+                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+    }
+
+    void traceQsJank(boolean startTracing, boolean wasCancelled) {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        if (startTracing) {
+            mInteractionJankMonitor.begin(mPanelView, CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE);
+        } else {
+            if (wasCancelled) {
+                mInteractionJankMonitor.cancel(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE);
+            } else {
+                mInteractionJankMonitor.end(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE);
+            }
+        }
+    }
+
+    interface ExpansionHeightSetToMaxListener {
+        void onExpansionHeightSetToMax(boolean requestPaddingUpdate);
+    }
+
+    interface ExpansionHeightListener {
+        void onQsSetExpansionHeightCalled(boolean qsFullyExpanded);
+    }
+
+    interface QsStateUpdateListener {
+        void onQsStateUpdated(boolean qsExpanded, boolean isStackScrollerOverscrolling);
+    }
+
+    interface ApplyClippingImmediatelyListener {
+        void onQsClippingImmediatelyApplied(boolean clipStatusView, Rect lastQsClipBounds,
+                int top, boolean qsFragmentCreated, boolean qsVisible);
+    }
+
+    interface FlingQsWithoutClickListener {
+        void onFlingQsWithoutClick(ValueAnimator animator, float qsExpansionHeight,
+                float target, float vel);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index 026673a..c136993 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -270,8 +270,6 @@
         // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
         mNotificationPanelViewController.collapsePanel(false, false, 1.0f);
 
-        mNotificationPanelViewController.closeQs();
-
         mExpandedVisible = false;
         notifyVisibilityChanged(false);
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index e406be1..37773e9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.shade
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
 import android.annotation.IdRes
 import android.app.StatusBarManager
 import android.content.res.Configuration
@@ -23,6 +25,7 @@
 import android.os.Trace
 import android.os.Trace.TRACE_TAG_APP
 import android.util.Pair
+import android.view.DisplayCutout
 import android.view.View
 import android.view.WindowInsets
 import android.widget.TextView
@@ -38,23 +41,21 @@
 import com.android.systemui.demomode.DemoMode
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.qs.ChipVisibilityListener
 import com.android.systemui.qs.HeaderPrivacyIconsController
 import com.android.systemui.qs.carrier.QSCarrierGroup
 import com.android.systemui.qs.carrier.QSCarrierGroupController
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
+import com.android.systemui.shade.ShadeHeaderController.Companion.HEADER_TRANSITION_ID
+import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
+import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
+import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
+import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
-import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
-import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_SHADE_HEADER
+import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.SHADE_HEADER
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.VariableDateView
@@ -65,60 +66,52 @@
 import javax.inject.Named
 
 /**
- * Controller for QS header on Large Screen width (large screen + landscape).
+ * Controller for QS header.
  *
- * Additionally, this serves as the staging ground for the combined QS headers. A single
- * [MotionLayout] that changes constraints depending on the configuration and can animate the
- * expansion of the headers in small screen portrait.
- *
- * [header] will be a [MotionLayout] if [Flags.COMBINED_QS_HEADERS] is enabled. In this case, the
- * [MotionLayout] has one transitions:
+ * [header] is a [MotionLayout] that has two transitions:
  * * [HEADER_TRANSITION_ID]: [QQS_HEADER_CONSTRAINT] <-> [QS_HEADER_CONSTRAINT] for portrait
- *   handheld device configuration.
+ * handheld device configuration.
+ * * [LARGE_SCREEN_HEADER_TRANSITION_ID]: [LARGE_SCREEN_HEADER_CONSTRAINT] for all other
+ * configurations
  */
 @CentralSurfacesScope
-class LargeScreenShadeHeaderController @Inject constructor(
-    @Named(LARGE_SCREEN_SHADE_HEADER) private val header: View,
+class ShadeHeaderController
+@Inject
+constructor(
+    @Named(SHADE_HEADER) private val header: MotionLayout,
     private val statusBarIconController: StatusBarIconController,
     private val tintedIconManagerFactory: StatusBarIconController.TintedIconManager.Factory,
     private val privacyIconsController: HeaderPrivacyIconsController,
     private val insetsProvider: StatusBarContentInsetsProvider,
     private val configurationController: ConfigurationController,
     private val variableDateViewControllerFactory: VariableDateViewController.Factory,
-    @Named(LARGE_SCREEN_BATTERY_CONTROLLER)
-    private val batteryMeterViewController: BatteryMeterViewController,
+    @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController,
     private val dumpManager: DumpManager,
-    private val featureFlags: FeatureFlags,
     private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder,
     private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager,
-    private val demoModeController: DemoModeController
+    private val demoModeController: DemoModeController,
+    private val qsBatteryModeController: QsBatteryModeController,
 ) : ViewController<View>(header), Dumpable {
 
     companion object {
-        /** IDs for transitions and constraints for the [MotionLayout]. These are only used when
-         * [Flags.COMBINED_QS_HEADERS] is enabled.
-         */
-        @VisibleForTesting
-        internal val HEADER_TRANSITION_ID = R.id.header_transition
+        /** IDs for transitions and constraints for the [MotionLayout]. */
+        @VisibleForTesting internal val HEADER_TRANSITION_ID = R.id.header_transition
         @VisibleForTesting
         internal val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition
-        @VisibleForTesting
-        internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
-        @VisibleForTesting
-        internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
+        @VisibleForTesting internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
+        @VisibleForTesting internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
         @VisibleForTesting
         internal val LARGE_SCREEN_HEADER_CONSTRAINT = R.id.large_screen_header_constraint
 
-        private fun Int.stateToString() = when (this) {
-            QQS_HEADER_CONSTRAINT -> "QQS Header"
-            QS_HEADER_CONSTRAINT -> "QS Header"
-            LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header"
-            else -> "Unknown state"
-        }
+        private fun Int.stateToString() =
+            when (this) {
+                QQS_HEADER_CONSTRAINT -> "QQS Header"
+                QS_HEADER_CONSTRAINT -> "QS Header"
+                LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header"
+                else -> "Unknown state $this"
+            }
     }
 
-    private val combinedHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)
-
     private lateinit var iconManager: StatusBarIconController.TintedIconManager
     private lateinit var carrierIconSlots: List<String>
     private lateinit var qsCarrierGroupController: QSCarrierGroupController
@@ -129,9 +122,8 @@
     private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons)
     private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group)
 
-    private var cutoutLeft = 0
-    private var cutoutRight = 0
     private var roundedCorners = 0
+    private var cutout: DisplayCutout? = null
     private var lastInsets: WindowInsets? = null
 
     private var qsDisabled = false
@@ -144,6 +136,14 @@
             updateListeners()
         }
 
+    private var customizing = false
+        set(value) {
+            if (field != value) {
+                field = value
+                updateVisibility()
+            }
+        }
+
     /**
      * Whether the QQS/QS part of the shade is visible. This is particularly important in
      * Lockscreen, as the shade is visible but QS is not.
@@ -170,21 +170,16 @@
             onHeaderStateChanged()
         }
 
-    /**
-     * Expansion fraction of the QQS/QS shade. This is not the expansion between QQS <-> QS.
-     */
+    /** Expansion fraction of the QQS/QS shade. This is not the expansion between QQS <-> QS. */
     var shadeExpandedFraction = -1f
         set(value) {
-            if (field != value) {
+            if (qsVisible && field != value) {
                 header.alpha = ShadeInterpolation.getContentAlpha(value)
                 field = value
-                updateVisibility()
             }
         }
 
-    /**
-     * Expansion fraction of the QQS <-> QS animation.
-     */
+    /** Expansion fraction of the QQS <-> QS animation. */
     var qsExpandedFraction = -1f
         set(value) {
             if (visible && field != value) {
@@ -193,9 +188,7 @@
             }
         }
 
-    /**
-     * Current scroll of QS.
-     */
+    /** Current scroll of QS. */
     var qsScrollY = 0
         set(value) {
             if (field != value) {
@@ -204,40 +197,41 @@
             }
         }
 
-    private val insetListener = View.OnApplyWindowInsetsListener { view, insets ->
-        updateConstraintsForInsets(view as MotionLayout, insets)
-        lastInsets = WindowInsets(insets)
+    private val insetListener =
+        View.OnApplyWindowInsetsListener { view, insets ->
+            updateConstraintsForInsets(view as MotionLayout, insets)
+            lastInsets = WindowInsets(insets)
 
-        view.onApplyWindowInsets(insets)
-    }
+            view.onApplyWindowInsets(insets)
+        }
 
-    private val demoModeReceiver = object : DemoMode {
-        override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK)
-        override fun dispatchDemoCommand(command: String, args: Bundle) =
-            clock.dispatchDemoCommand(command, args)
-        override fun onDemoModeStarted() = clock.onDemoModeStarted()
-        override fun onDemoModeFinished() = clock.onDemoModeFinished()
-    }
+    private val demoModeReceiver =
+        object : DemoMode {
+            override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK)
+            override fun dispatchDemoCommand(command: String, args: Bundle) =
+                clock.dispatchDemoCommand(command, args)
+            override fun onDemoModeStarted() = clock.onDemoModeStarted()
+            override fun onDemoModeFinished() = clock.onDemoModeFinished()
+        }
 
-    private val chipVisibilityListener: ChipVisibilityListener = object : ChipVisibilityListener {
-        override fun onChipVisibilityRefreshed(visible: Boolean) {
-            if (header is MotionLayout) {
+    private val chipVisibilityListener: ChipVisibilityListener =
+        object : ChipVisibilityListener {
+            override fun onChipVisibilityRefreshed(visible: Boolean) {
                 // If the privacy chip is visible, we hide the status icons and battery remaining
                 // icon, only in QQS.
-                val update = combinedShadeHeadersConstraintManager
-                    .privacyChipVisibilityConstraints(visible)
+                val update =
+                    combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(visible)
                 header.updateAllConstraints(update)
             }
         }
-    }
 
     private val configurationControllerListener =
         object : ConfigurationController.ConfigurationListener {
-        override fun onConfigChanged(newConfig: Configuration?) {
-            if (header !is MotionLayout) {
-                val left = header.resources.getDimensionPixelSize(
-                    R.dimen.large_screen_shade_header_left_padding
-                )
+            override fun onConfigChanged(newConfig: Configuration?) {
+                val left =
+                    header.resources.getDimensionPixelSize(
+                        R.dimen.large_screen_shade_header_left_padding
+                    )
                 header.setPadding(
                     left,
                     header.paddingTop,
@@ -245,31 +239,25 @@
                     header.paddingBottom
                 )
             }
-        }
 
-        override fun onDensityOrFontScaleChanged() {
-            clock.setTextAppearance(R.style.TextAppearance_QS_Status)
-            date.setTextAppearance(R.style.TextAppearance_QS_Status)
-            qsCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers)
-            if (header is MotionLayout) {
+            override fun onDensityOrFontScaleChanged() {
+                clock.setTextAppearance(R.style.TextAppearance_QS_Status)
+                date.setTextAppearance(R.style.TextAppearance_QS_Status)
+                qsCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers)
                 loadConstraints()
-                header.minHeight = resources
-                        .getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height)
+                header.minHeight =
+                    resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height)
                 lastInsets?.let { updateConstraintsForInsets(header, it) }
+                updateResources()
             }
-            updateResources()
         }
-    }
 
     override fun onInit() {
-        if (header is MotionLayout) {
-            variableDateViewControllerFactory.create(date as VariableDateView).init()
-        }
+        variableDateViewControllerFactory.create(date as VariableDateView).init()
         batteryMeterViewController.init()
 
         // battery settings same as in QS icons
         batteryMeterViewController.ignoreTunerUpdates()
-        batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
 
         iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
         iconManager.setTint(
@@ -278,39 +266,31 @@
 
         carrierIconSlots =
             listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile))
-        qsCarrierGroupController = qsCarrierGroupControllerBuilder
-            .setQSCarrierGroup(qsCarrierGroup)
-            .build()
+        qsCarrierGroupController =
+            qsCarrierGroupControllerBuilder.setQSCarrierGroup(qsCarrierGroup).build()
 
-        if (!combinedHeaders) {
-            // In the new header, we display alarm icon but we ignore it when not using the new
-            // headers.
-            iconContainer.addIgnoredSlot(
-                    context.getString(com.android.internal.R.string.status_bar_alarm_clock)
-            )
-        }
-        if (combinedHeaders) {
-            privacyIconsController.onParentVisible()
-        }
+        privacyIconsController.onParentVisible()
     }
 
     override fun onViewAttached() {
         privacyIconsController.chipVisibilityListener = chipVisibilityListener
-        if (header is MotionLayout) {
-            header.setOnApplyWindowInsetsListener(insetListener)
-            clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
-                val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
-                v.pivotX = newPivot
-                v.pivotY = v.height.toFloat() / 2
-            }
+        updateVisibility()
+        updateTransition()
+
+        header.setOnApplyWindowInsetsListener(insetListener)
+
+        clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
+            val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
+            v.pivotX = newPivot
+            v.pivotY = v.height.toFloat() / 2
+
+            qsCarrierGroup.setPaddingRelative((v.width * v.scaleX).toInt(), 0, 0, 0)
         }
 
         dumpManager.registerDumpable(this)
         configurationController.addCallback(configurationControllerListener)
         demoModeController.addCallback(demoModeReceiver)
-
-        updateVisibility()
-        updateTransition()
+        statusBarIconController.addIconGroup(iconManager)
     }
 
     override fun onViewDetached() {
@@ -318,6 +298,7 @@
         dumpManager.unregisterDumpable(this::class.java.simpleName)
         configurationController.removeCallback(configurationControllerListener)
         demoModeController.removeCallback(demoModeReceiver)
+        statusBarIconController.removeIconGroup(iconManager)
     }
 
     fun disable(state1: Int, state2: Int, animate: Boolean) {
@@ -328,40 +309,40 @@
     }
 
     fun startCustomizingAnimation(show: Boolean, duration: Long) {
-        header.animate()
-                .setDuration(duration)
-                .alpha(if (show) 0f else 1f)
-                .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN)
-                .setUpdateListener {
-                    updateVisibility()
-                }
-                .start()
+        header
+            .animate()
+            .setDuration(duration)
+            .alpha(if (show) 0f else 1f)
+            .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN)
+            .setListener(CustomizerAnimationListener(show))
+            .start()
     }
 
     private fun loadConstraints() {
-        if (header is MotionLayout) {
-            // Use resources.getXml instead of passing the resource id due to bug b/205018300
-            header.getConstraintSet(QQS_HEADER_CONSTRAINT)
-                    .load(context, resources.getXml(R.xml.qqs_header))
-            header.getConstraintSet(QS_HEADER_CONSTRAINT)
-                    .load(context, resources.getXml(R.xml.qs_header))
-            header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT)
-                    .load(context, resources.getXml(R.xml.large_screen_shade_header))
-        }
+        // Use resources.getXml instead of passing the resource id due to bug b/205018300
+        header
+            .getConstraintSet(QQS_HEADER_CONSTRAINT)
+            .load(context, resources.getXml(R.xml.qqs_header))
+        header
+            .getConstraintSet(QS_HEADER_CONSTRAINT)
+            .load(context, resources.getXml(R.xml.qs_header))
+        header
+            .getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT)
+            .load(context, resources.getXml(R.xml.large_screen_shade_header))
     }
 
     private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) {
-        val cutout = insets.displayCutout
+        val cutout = insets.displayCutout.also { this.cutout = it }
 
         val sbInsets: Pair<Int, Int> = insetsProvider.getStatusBarContentInsetsForCurrentRotation()
-        cutoutLeft = sbInsets.first
-        cutoutRight = sbInsets.second
+        val cutoutLeft = sbInsets.first
+        val cutoutRight = sbInsets.second
         val hasCornerCutout: Boolean = insetsProvider.currentRotationHasCornerCutout()
         updateQQSPaddings()
         // Set these guides as the left/right limits for content that lives in the top row, using
         // cutoutLeft and cutoutRight
-        var changes = combinedShadeHeadersConstraintManager
-            .edgesGuidelinesConstraints(
+        var changes =
+            combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
                 if (view.isLayoutRtl) cutoutRight else cutoutLeft,
                 header.paddingStart,
                 if (view.isLayoutRtl) cutoutLeft else cutoutRight,
@@ -373,20 +354,28 @@
             if (topCutout.isEmpty || hasCornerCutout) {
                 changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
             } else {
-                changes += combinedShadeHeadersConstraintManager.centerCutoutConstraints(
-                    view.isLayoutRtl,
-                    (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2
-                )
+                changes +=
+                    combinedShadeHeadersConstraintManager.centerCutoutConstraints(
+                        view.isLayoutRtl,
+                        (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2
+                    )
             }
         } else {
-           changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
+            changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
         }
 
         view.updateAllConstraints(changes)
+        updateBatteryMode()
+    }
+
+    private fun updateBatteryMode() {
+        qsBatteryModeController.getBatteryMode(cutout, qsExpandedFraction)?.let {
+            batteryIcon.setPercentShowMode(it)
+        }
     }
 
     private fun updateScrollY() {
-        if (!largeScreenActive && combinedHeaders) {
+        if (!largeScreenActive) {
             header.scrollY = qsScrollY
         }
     }
@@ -402,12 +391,6 @@
     }
 
     private fun onHeaderStateChanged() {
-        if (largeScreenActive || combinedHeaders) {
-            privacyIconsController.onParentVisible()
-        } else {
-            privacyIconsController.onParentInvisible()
-        }
-        updateVisibility()
         updateTransition()
     }
 
@@ -416,13 +399,14 @@
      * be visible any time the QQS/QS shade is open.
      */
     private fun updateVisibility() {
-        val visibility = if (!largeScreenActive && !combinedHeaders || qsDisabled) {
-            View.GONE
-        } else if (qsVisible && header.alpha > 0f) {
-            View.VISIBLE
-        } else {
-            View.INVISIBLE
-        }
+        val visibility =
+            if (qsDisabled) {
+                View.GONE
+            } else if (qsVisible && !customizing) {
+                View.VISIBLE
+            } else {
+                View.INVISIBLE
+            }
         if (header.visibility != visibility) {
             header.visibility = visibility
             visible = visibility == View.VISIBLE
@@ -430,36 +414,28 @@
     }
 
     private fun updateTransition() {
-        if (!combinedHeaders) {
-            return
-        }
-        header as MotionLayout
         if (largeScreenActive) {
             logInstantEvent("Large screen constraints set")
-            header.setTransition(HEADER_TRANSITION_ID)
-            header.transitionToStart()
+            header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
         } else {
             logInstantEvent("Small screen constraints set")
             header.setTransition(HEADER_TRANSITION_ID)
-            header.transitionToStart()
-            updatePosition()
-            updateScrollY()
         }
+        header.jumpToState(header.startState)
+        updatePosition()
+        updateScrollY()
     }
 
     private fun updatePosition() {
-        if (header is MotionLayout && !largeScreenActive && visible) {
+        if (!largeScreenActive && visible) {
             logInstantEvent("updatePosition: $qsExpandedFraction")
             header.progress = qsExpandedFraction
+            updateBatteryMode()
         }
     }
 
     private fun logInstantEvent(message: String) {
-        Trace.instantForTrack(
-                TRACE_TAG_APP,
-                "LargeScreenHeaderController",
-                message
-        )
+        Trace.instantForTrack(TRACE_TAG_APP, "LargeScreenHeaderController", message)
     }
 
     private fun updateListeners() {
@@ -467,10 +443,8 @@
         if (visible) {
             updateSingleCarrier(qsCarrierGroupController.isSingleCarrier)
             qsCarrierGroupController.setOnSingleCarrierChangedListener { updateSingleCarrier(it) }
-            statusBarIconController.addIconGroup(iconManager)
         } else {
             qsCarrierGroupController.setOnSingleCarrierChangedListener(null)
-            statusBarIconController.removeIconGroup(iconManager)
         }
     }
 
@@ -487,21 +461,20 @@
         val padding = resources.getDimensionPixelSize(R.dimen.qs_panel_padding)
         header.setPadding(padding, header.paddingTop, padding, header.paddingBottom)
         updateQQSPaddings()
+        qsBatteryModeController.updateResources()
     }
 
     private fun updateQQSPaddings() {
-        if (header is MotionLayout) {
-            val clockPaddingStart = resources
-                .getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding)
-            val clockPaddingEnd = resources
-                .getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding)
-            clock.setPaddingRelative(
-                clockPaddingStart,
-                clock.paddingTop,
-                clockPaddingEnd,
-                clock.paddingBottom
-            )
-        }
+        val clockPaddingStart =
+            resources.getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding)
+        val clockPaddingEnd =
+            resources.getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding)
+        clock.setPaddingRelative(
+            clockPaddingStart,
+            clock.paddingTop,
+            clockPaddingEnd,
+            clock.paddingBottom
+        )
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -511,10 +484,7 @@
         pw.println("active: $largeScreenActive")
         pw.println("qsExpandedFraction: $qsExpandedFraction")
         pw.println("qsScrollY: $qsScrollY")
-        if (combinedHeaders) {
-            header as MotionLayout
-            pw.println("currentState: ${header.currentState.stateToString()}")
-        }
+        pw.println("currentState: ${header.currentState.stateToString()}")
     }
 
     private fun MotionLayout.updateConstraints(@IdRes state: Int, update: ConstraintChange) {
@@ -540,6 +510,24 @@
         }
     }
 
-    @VisibleForTesting
-    internal fun simulateViewDetached() = this.onViewDetached()
+    @VisibleForTesting internal fun simulateViewDetached() = this.onViewDetached()
+
+    inner class CustomizerAnimationListener(
+        private val enteringCustomizing: Boolean,
+    ) : AnimatorListenerAdapter() {
+        override fun onAnimationEnd(animation: Animator?) {
+            super.onAnimationEnd(animation)
+            header.animate().setListener(null)
+            if (enteringCustomizing) {
+                customizing = true
+            }
+        }
+
+        override fun onAnimationStart(animation: Animator?) {
+            super.onAnimationStart(animation)
+            if (!enteringCustomizing) {
+                customizing = false
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 5fedbeb..d34e127 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -20,7 +20,9 @@
 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.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE
+import com.android.systemui.shade.NotificationPanelViewController.FLING_EXPAND
+import com.android.systemui.shade.NotificationPanelViewController.FLING_HIDE
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -36,16 +38,9 @@
         buffer.log(TAG, LogLevel.DEBUG, msg)
     }
 
-    private inline fun log(
-        logLevel: LogLevel,
-        initializer: LogMessage.() -> Unit,
-        noinline printer: LogMessage.() -> String
-    ) {
-        buffer.log(TAG, logLevel, initializer, printer)
-    }
-
     fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
-        log(
+        buffer.log(
+            TAG,
             LogLevel.VERBOSE,
             { double1 = h.toDouble() },
             { "onQsIntercept: move action, QS tracking enabled. h = $double1" }
@@ -58,11 +53,11 @@
         h: Float,
         touchSlop: Float,
         qsExpanded: Boolean,
-        collapsedOnDown: Boolean,
         keyguardShowing: Boolean,
         qsExpansionEnabled: Boolean
     ) {
-        log(
+        buffer.log(
+            TAG,
             LogLevel.VERBOSE,
             {
                 int1 = initialTouchY.toInt()
@@ -70,19 +65,19 @@
                 long1 = h.toLong()
                 double1 = touchSlop.toDouble()
                 bool1 = qsExpanded
-                bool2 = collapsedOnDown
-                bool3 = keyguardShowing
-                bool4 = qsExpansionEnabled
+                bool2 = keyguardShowing
+                bool3 = qsExpansionEnabled
             },
             {
                 "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" +
-                    "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
+                    "=$bool1,keyguardShowing=$bool2,qsExpansion=$bool3"
             }
         )
     }
 
     fun logMotionEvent(event: MotionEvent, message: String) {
-        log(
+        buffer.log(
+            TAG,
             LogLevel.VERBOSE,
             {
                 str1 = message
@@ -99,7 +94,8 @@
     }
 
     fun logMotionEventStatusBarState(event: MotionEvent, statusBarState: Int, message: String) {
-        log(
+        buffer.log(
+                TAG,
                 LogLevel.VERBOSE,
                 {
                     str1 = message
@@ -128,25 +124,44 @@
             tracking: Boolean,
             dragDownPxAmount: Float,
     ) {
-        log(LogLevel.VERBOSE, {
-            str1 = message
-            double1 = fraction.toDouble()
-            bool1 = expanded
-            bool2 = tracking
-            long1 = dragDownPxAmount.toLong()
-        }, {
-            "$str1 fraction=$double1,expanded=$bool1," +
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                double1 = fraction.toDouble()
+                bool1 = expanded
+                bool2 = tracking
+                long1 = dragDownPxAmount.toLong()
+            },
+            {
+                "$str1 fraction=$double1,expanded=$bool1," +
                     "tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount"
-        })
+            }
+        )
     }
 
     fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) {
-        log(LogLevel.VERBOSE, {
-            bool1 = hasVibratedOnOpen
-            double1 = fraction.toDouble()
-        }, {
-            "hasVibratedOnOpen=$bool1, expansionFraction=$double1"
-        })
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                bool1 = hasVibratedOnOpen
+                double1 = fraction.toDouble()
+            },
+            { "hasVibratedOnOpen=$bool1, expansionFraction=$double1" }
+        )
+    }
+
+    fun logQsExpandImmediateChanged(newValue: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                bool1 = newValue
+            },
+            { "qsExpandImmediate=$bool1" }
+        )
     }
 
     fun logQsExpansionChanged(
@@ -155,46 +170,58 @@
             qsMinExpansionHeight: Int,
             qsMaxExpansionHeight: Int,
             stackScrollerOverscrolling: Boolean,
-            dozing: Boolean,
             qsAnimatorExpand: Boolean,
             animatingQs: Boolean
     ) {
-        log(LogLevel.VERBOSE, {
-            str1 = message
-            bool1 = qsExpanded
-            int1 = qsMinExpansionHeight
-            int2 = qsMaxExpansionHeight
-            bool2 = stackScrollerOverscrolling
-            bool3 = dozing
-            bool4 = qsAnimatorExpand
-            // 0 = false, 1 = true
-            long1 = animatingQs.compareTo(false).toLong()
-        }, {
-            "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," +
-                    "stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," +
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                bool1 = qsExpanded
+                int1 = qsMinExpansionHeight
+                int2 = qsMaxExpansionHeight
+                bool2 = stackScrollerOverscrolling
+                bool3 = qsAnimatorExpand
+                // 0 = false, 1 = true
+                long1 = animatingQs.compareTo(false).toLong()
+            },
+            {
+                "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," +
+                    "stackScrollerOverscrolling=$bool2,qsAnimatorExpand=$bool3," +
                     "animatingQs=$long1"
-        })
+            }
+        )
     }
 
     fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) {
-        log(LogLevel.DEBUG, {
-            bool1 = isDozing
-            bool2 = singleTapEnabled
-            bool3 = isNotDocked
-        }, {
-            "PulsingGestureListener#onSingleTapUp all of this must true for single " +
-              "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3"
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = isDozing
+                bool2 = singleTapEnabled
+                bool3 = isNotDocked
+            },
+            {
+                "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+               "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3"
         })
     }
 
     fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) {
-        log(LogLevel.DEBUG, {
-            bool1 = proximityIsNotNear
-            bool2 = isNotFalseTap
-        }, {
-            "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = proximityIsNotNear
+                bool2 = isNotFalseTap
+            },
+            {
+                "PulsingGestureListener#onSingleTapUp all of this must true for single " +
                     "tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2"
-        })
+            }
+        )
     }
 
     fun logNotInterceptingTouchInstantExpanding(
@@ -202,13 +229,55 @@
             notificationsDragEnabled: Boolean,
             touchDisabled: Boolean
     ) {
-        log(LogLevel.VERBOSE, {
-            bool1 = instantExpanding
-            bool2 = notificationsDragEnabled
-            bool3 = touchDisabled
-        }, {
-            "NPVC not intercepting touch, instantExpanding: $bool1, " +
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                bool1 = instantExpanding
+                bool2 = notificationsDragEnabled
+                bool3 = touchDisabled
+            },
+            {
+                "NPVC not intercepting touch, instantExpanding: $bool1, " +
                     "!notificationsDragEnabled: $bool2, touchDisabled: $bool3"
-        })
+            }
+        )
+    }
+
+    fun logLastFlingWasExpanding(expand: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            { bool1 = expand },
+            { "NPVC mLastFlingWasExpanding set to: $bool1" }
+        )
+    }
+
+    fun flingQs(flingType: Int, isClick: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = flingTypeToString(flingType)
+                bool1 = isClick
+            },
+            { "QS fling with type $str1, originated from click: $isClick" }
+        )
+    }
+
+    private fun flingTypeToString(flingType: Int) = when (flingType) {
+        FLING_EXPAND -> "FLING_EXPAND"
+        FLING_COLLAPSE -> "FLING_COLLAPSE"
+        FLING_HIDE -> "FLING_HIDE"
+        else -> "UNKNOWN"
+    }
+
+    fun logSplitShadeChanged(splitShadeEnabled: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            { bool1 = splitShadeEnabled },
+            { "Split shade state changed: split shade ${if (bool1) "enabled" else "disabled"}" }
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
index c6a6e87..9851625 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
@@ -32,11 +32,21 @@
     ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) {
 
     fun logApplyingWindowLayoutParams(lp: WindowManager.LayoutParams) {
-        log(DEBUG, { str1 = lp.toString() }, { "Applying new window layout params: $str1" })
+        buffer.log(
+            TAG,
+            DEBUG,
+            { str1 = lp.toString() },
+            { "Applying new window layout params: $str1" }
+        )
     }
 
     fun logNewState(state: Any) {
-        log(DEBUG, { str1 = state.toString() }, { "Applying new state: $str1" })
+        buffer.log(
+            TAG,
+            DEBUG,
+            { str1 = state.toString() },
+            { "Applying new state: $str1" }
+        )
     }
 
     private inline fun log(
@@ -48,11 +58,16 @@
     }
 
     fun logApplyVisibility(visible: Boolean) {
-        log(DEBUG, { bool1 = visible }, { "Updating visibility, should be visible : $bool1" })
+        buffer.log(
+            TAG,
+            DEBUG,
+            { bool1 = visible },
+            { "Updating visibility, should be visible : $bool1" })
     }
 
     fun logShadeVisibleAndFocusable(visible: Boolean) {
-        log(
+        buffer.log(
+            TAG,
             DEBUG,
             { bool1 = visible },
             { "Updating shade, should be visible and focusable: $bool1" }
@@ -60,6 +75,11 @@
     }
 
     fun logShadeFocusable(focusable: Boolean) {
-        log(DEBUG, { bool1 = focusable }, { "Updating shade, should be focusable : $bool1" })
+        buffer.log(
+            TAG,
+            DEBUG,
+            { bool1 = focusable },
+            { "Updating shade, should be focusable : $bool1" }
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
index 07820ec..129d09e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
@@ -43,8 +43,6 @@
     shadeExpansionStateManager: ShadeExpansionStateManager,
     dumpManager: DumpManager,
     private val context: Context,
-    private val splitShadeOverScrollerFactory: SplitShadeOverScroller.Factory,
-    private val noOpOverScroller: NoOpOverScroller,
     private val scrimShadeTransitionController: ScrimShadeTransitionController,
     private val statusBarStateController: SysuiStatusBarStateController,
 ) {
@@ -57,17 +55,6 @@
     private var currentPanelState: Int? = null
     private var lastShadeExpansionChangeEvent: ShadeExpansionChangeEvent? = null
 
-    private val splitShadeOverScroller by lazy {
-        splitShadeOverScrollerFactory.create({ qs }, { notificationStackScrollLayoutController })
-    }
-    private val shadeOverScroller: ShadeOverScroller
-        get() =
-            if (inSplitShade && isScreenUnlocked() && propertiesInitialized()) {
-                splitShadeOverScroller
-            } else {
-                noOpOverScroller
-            }
-
     init {
         updateResources()
         configurationController.addCallback(
@@ -89,21 +76,14 @@
 
     private fun onPanelStateChanged(@PanelState state: Int) {
         currentPanelState = state
-        shadeOverScroller.onPanelStateChanged(state)
         scrimShadeTransitionController.onPanelStateChanged(state)
     }
 
     private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
         lastShadeExpansionChangeEvent = event
-        shadeOverScroller.onDragDownAmountChanged(event.dragDownPxAmount)
         scrimShadeTransitionController.onPanelExpansionChanged(event)
     }
 
-    private fun propertiesInitialized() =
-        this::qs.isInitialized &&
-            this::notificationPanelViewController.isInitialized &&
-            this::notificationStackScrollLayoutController.isInitialized
-
     private fun dump(pw: PrintWriter) {
         pw.println(
             """
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
deleted file mode 100644
index f95125f..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade.transition
-
-import android.animation.Animator
-import android.animation.ValueAnimator
-import android.content.Context
-import android.content.res.Configuration
-import android.util.MathUtils
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.qs.QS
-import com.android.systemui.shade.PanelState
-import com.android.systemui.shade.STATE_CLOSED
-import com.android.systemui.shade.STATE_OPENING
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import java.io.PrintWriter
-
-class SplitShadeOverScroller
-@AssistedInject
-constructor(
-    configurationController: ConfigurationController,
-    dumpManager: DumpManager,
-    private val context: Context,
-    private val scrimController: ScrimController,
-    @Assisted private val qSProvider: () -> QS,
-    @Assisted private val nsslControllerProvider: () -> NotificationStackScrollLayoutController
-) : ShadeOverScroller {
-
-    private var releaseOverScrollDuration = 0L
-    private var maxOverScrollAmount = 0
-    private var previousOverscrollAmount = 0
-    private var dragDownAmount: Float = 0f
-    @PanelState private var panelState: Int = STATE_CLOSED
-
-    private var releaseOverScrollAnimator: Animator? = null
-
-    private val qS: QS
-        get() = qSProvider()
-
-    private val nsslController: NotificationStackScrollLayoutController
-        get() = nsslControllerProvider()
-
-    init {
-        updateResources()
-        configurationController.addCallback(
-            object : ConfigurationController.ConfigurationListener {
-                override fun onConfigChanged(newConfig: Configuration?) {
-                    updateResources()
-                }
-            })
-        dumpManager.registerCriticalDumpable("SplitShadeOverScroller") { printWriter, _ ->
-            dump(printWriter)
-        }
-    }
-
-    private fun updateResources() {
-        val resources = context.resources
-        maxOverScrollAmount = resources.getDimensionPixelSize(R.dimen.shade_max_over_scroll_amount)
-        releaseOverScrollDuration =
-            resources.getInteger(R.integer.lockscreen_shade_over_scroll_release_duration).toLong()
-    }
-
-    override fun onPanelStateChanged(@PanelState newPanelState: Int) {
-        if (shouldReleaseOverscroll(previousState = panelState, newState = newPanelState)) {
-            releaseOverScroll()
-        }
-        panelState = newPanelState
-    }
-
-    override fun onDragDownAmountChanged(newDragDownAmount: Float) {
-        if (dragDownAmount == newDragDownAmount) {
-            return
-        }
-        dragDownAmount = newDragDownAmount
-        if (shouldOverscroll()) {
-            overScroll(newDragDownAmount)
-        }
-    }
-
-    private fun shouldOverscroll() = panelState == STATE_OPENING
-
-    private fun shouldReleaseOverscroll(@PanelState previousState: Int, @PanelState newState: Int) =
-        previousState == STATE_OPENING && newState != STATE_OPENING
-
-    private fun overScroll(dragDownAmount: Float) {
-        val overscrollAmount: Int = calculateOverscrollAmount(dragDownAmount)
-        applyOverscroll(overscrollAmount)
-        previousOverscrollAmount = overscrollAmount
-    }
-
-    private fun calculateOverscrollAmount(dragDownAmount: Float): Int {
-        val fullHeight: Int = nsslController.height
-        val fullHeightProgress: Float = MathUtils.saturate(dragDownAmount / fullHeight)
-        return (fullHeightProgress * maxOverScrollAmount).toInt()
-    }
-
-    private fun applyOverscroll(overscrollAmount: Int) {
-        qS.setOverScrollAmount(overscrollAmount)
-        scrimController.setNotificationsOverScrollAmount(overscrollAmount)
-        nsslController.setOverScrollAmount(overscrollAmount)
-    }
-
-    private fun releaseOverScroll() {
-        val animator = ValueAnimator.ofInt(previousOverscrollAmount, 0)
-        animator.addUpdateListener {
-            val overScrollAmount = it.animatedValue as Int
-            qS.setOverScrollAmount(overScrollAmount)
-            scrimController.setNotificationsOverScrollAmount(overScrollAmount)
-            nsslController.setOverScrollAmount(overScrollAmount)
-        }
-        animator.interpolator = Interpolators.STANDARD
-        animator.duration = releaseOverScrollDuration
-        animator.start()
-        releaseOverScrollAnimator = animator
-        previousOverscrollAmount = 0
-    }
-
-    @VisibleForTesting
-    internal fun finishAnimations() {
-        releaseOverScrollAnimator?.end()
-        releaseOverScrollAnimator = null
-    }
-
-    private fun dump(pw: PrintWriter) {
-        pw.println(
-            """
-            SplitShadeOverScroller:
-                Resources:
-                    releaseOverScrollDuration: $releaseOverScrollDuration
-                    maxOverScrollAmount: $maxOverScrollAmount
-                State:
-                    previousOverscrollAmount: $previousOverscrollAmount
-                    dragDownAmount: $dragDownAmount
-                    panelState: $panelState
-            """.trimIndent())
-    }
-
-    @AssistedFactory
-    fun interface Factory {
-        fun create(
-            qSProvider: () -> QS,
-            nsslControllerProvider: () -> NotificationStackScrollLayoutController
-        ): SplitShadeOverScroller
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
new file mode 100644
index 0000000..ab0d6e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
@@ -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.smartspace.config
+
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
+
+class BcSmartspaceConfigProvider(private val featureFlags: FeatureFlags) :
+    BcSmartspaceConfigPlugin {
+    override val isDefaultDateWeatherDisabled: Boolean
+        get() = featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index b02a45a..641131e 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -18,7 +18,6 @@
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.smartspace.SmartspacePrecondition
 import com.android.systemui.smartspace.SmartspaceTargetFilter
-import com.android.systemui.smartspace.filters.LockscreenAndDreamTargetFilter
 import com.android.systemui.smartspace.preconditions.LockscreenPrecondition
 import dagger.Binds
 import dagger.BindsOptionalOf
@@ -35,11 +34,6 @@
         const val DREAM_SMARTSPACE_DATA_PLUGIN = "dreams_smartspace_data_plugin"
 
         /**
-         * The lockscreen smartspace target filter.
-         */
-        const val LOCKSCREEN_SMARTSPACE_TARGET_FILTER = "lockscreen_smartspace_target_filter"
-
-        /**
          * The dream smartspace target filter.
          */
         const val DREAM_SMARTSPACE_TARGET_FILTER = "dream_smartspace_target_filter"
@@ -48,6 +42,16 @@
          * The precondition for dream smartspace
          */
         const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition"
+
+        /**
+         * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
+         */
+        const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin"
+
+        /**
+         * The BcSmartspaceDataPlugin for the standalone weather.
+         */
+        const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
     }
 
     @BindsOptionalOf
@@ -59,12 +63,6 @@
     abstract fun optionalDreamsBcSmartspaceDataPlugin(): BcSmartspaceDataPlugin?
 
     @Binds
-    @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER)
-    abstract fun provideLockscreenSmartspaceTargetFilter(
-        filter: LockscreenAndDreamTargetFilter?
-    ): SmartspaceTargetFilter?
-
-    @Binds
     @Named(DREAM_SMARTSPACE_PRECONDITION)
     abstract fun bindSmartspacePrecondition(
         lockscreenPrecondition: LockscreenPrecondition?
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
index 236ba1f..5736a5c 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
@@ -21,6 +21,7 @@
 import android.view.ViewGroup
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_DREAM
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.smartspace.dagger.SmartspaceViewComponent.SmartspaceViewModule.PLUGIN
 import dagger.BindsInstance
@@ -57,7 +58,7 @@
                 BcSmartspaceDataPlugin.SmartspaceView {
             val ssView = plugin.getView(parent)
             // Currently, this is only used to provide SmartspaceView on Dream surface.
-            ssView.setIsDreaming(true)
+            ssView.setUiSurface(UI_SURFACE_DREAM)
             ssView.registerDataProvider(plugin)
 
             ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt b/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt
index 1302ec9..88e8ad9 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt
@@ -15,8 +15,6 @@
  */
 package com.android.systemui.smartspace.preconditions
 
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.smartspace.SmartspacePrecondition
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.util.concurrency.Execution
@@ -24,11 +22,9 @@
 
 /**
  * {@link LockscreenPrecondition} covers the conditions that must be met before Smartspace can be
- * used over lockscreen. These conditions include the device being provisioned with a setup user
- * and the Smartspace feature flag enabled.
+ * used over lockscreen. These conditions include the device being provisioned with a setup user.
  */
 class LockscreenPrecondition @Inject constructor(
-    private val featureFlags: FeatureFlags,
     private val deviceProvisionedController: DeviceProvisionedController,
     private val execution: Execution
 ) : SmartspacePrecondition {
@@ -90,6 +86,6 @@
 
     override fun conditionsMet(): Boolean {
         execution.assertIsMainThread()
-        return featureFlags.isEnabled(Flags.SMARTSPACE) && deviceReady
+        return deviceReady
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
index 87c12c2..6577cf6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
@@ -20,10 +20,21 @@
 import android.util.AttributeSet;
 import android.widget.Button;
 
+import com.android.systemui.animation.LaunchableView;
+import com.android.systemui.animation.LaunchableViewDelegate;
+
+import kotlin.Unit;
+
 /**
  * A Button which doesn't have overlapping drawing commands
  */
-public class AlphaOptimizedButton extends Button {
+public class AlphaOptimizedButton extends Button implements LaunchableView {
+    private LaunchableViewDelegate mDelegate = new LaunchableViewDelegate(this,
+            (visibility) -> {
+                super.setVisibility(visibility);
+                return Unit.INSTANCE;
+            });
+
     public AlphaOptimizedButton(Context context) {
         super(context);
     }
@@ -45,4 +56,14 @@
     public boolean hasOverlappingRendering() {
         return false;
     }
+
+    @Override
+    public void setShouldBlockVisibilityChanges(boolean block) {
+        mDelegate.setShouldBlockVisibilityChanges(block);
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        mDelegate.setVisibility(visibility);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt
new file mode 100644
index 0000000..37140ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt
@@ -0,0 +1,73 @@
+/*
+ * 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 android.annotation.IntRange
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Configuration
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.statusbar.events.BackgroundAnimatableView
+
+class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+    FrameLayout(context, attrs), BackgroundAnimatableView {
+
+    private val roundedContainer: LinearLayout
+    private val batteryMeterView: BatteryMeterView
+    override val contentView: View
+        get() = batteryMeterView
+
+    init {
+        inflate(context, R.layout.battery_status_chip, this)
+        roundedContainer = findViewById(R.id.rounded_container)
+        batteryMeterView = findViewById(R.id.battery_meter_view)
+        updateResources()
+    }
+
+    /**
+     * When animating as a chip in the status bar, we want to animate the width for the rounded
+     * container. We have to subtract our own top and left offset because the bounds come to us as
+     * absolute on-screen bounds.
+     */
+    override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
+        roundedContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top)
+    }
+
+    fun setBatteryLevel(@IntRange(from = 0, to = 100) batteryLevel: Int) {
+        batteryMeterView.setForceShowPercent(true)
+        batteryMeterView.onBatteryLevelChanged(batteryLevel, true)
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        updateResources()
+    }
+
+    @SuppressLint("UseCompatLoadingForDrawables")
+    private fun updateResources() {
+        val primaryColor =
+            Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+        val textColorSecondary =
+            Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)
+        batteryMeterView.updateColors(primaryColor, textColorSecondary, primaryColor)
+        roundedContainer.background = mContext.getDrawable(R.drawable.statusbar_chip_bg)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 750d004..f2e729d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -20,11 +20,8 @@
 import static android.app.StatusBarManager.DISABLE_NONE;
 import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
 import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static com.android.systemui.statusbar.phone.CentralSurfacesImpl.ONLY_CORE_APPS;
-
 import android.annotation.Nullable;
 import android.app.ITransientNotificationCallback;
 import android.app.StatusBarManager;
@@ -40,17 +37,19 @@
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
-import android.hardware.display.DisplayManager;
 import android.hardware.fingerprint.IUdfpsHbmListener;
 import android.inputmethodservice.InputMethodService.BackDispositionMode;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.MediaRoute2Info;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -60,6 +59,7 @@
 import android.view.WindowInsetsController.Behavior;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.os.SomeArgs;
 import com.android.internal.statusbar.IAddTileResultCallback;
@@ -70,6 +70,7 @@
 import com.android.internal.util.GcUtils;
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.dump.DumpHandler;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.policy.CallbackController;
@@ -89,8 +90,7 @@
  * are coalesced, note that they are all idempotent.
  */
 public class CommandQueue extends IStatusBar.Stub implements
-        CallbackController<Callbacks>,
-        DisplayManager.DisplayListener {
+        CallbackController<Callbacks> {
     private static final String TAG = CommandQueue.class.getSimpleName();
 
     private static final int INDEX_MASK = 0xffff;
@@ -168,6 +168,7 @@
     private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT;
     private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT;
     private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
+    private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT;
 
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -191,6 +192,7 @@
     private ProtoTracer mProtoTracer;
     private final @Nullable CommandRegistry mRegistry;
     private final @Nullable DumpHandler mDumpHandler;
+    private final @Nullable DisplayTracker mDisplayTracker;
 
     /**
      * These methods are called back on the main thread.
@@ -334,7 +336,7 @@
         /**
          * @see IStatusBar#setBiometicContextListener(IBiometricContextListener)
          */
-        default void setBiometicContextListener(IBiometricContextListener listener) {
+        default void setBiometricContextListener(IBiometricContextListener listener) {
         }
 
         /**
@@ -350,7 +352,7 @@
         }
 
         /**
-         * @see DisplayManager.DisplayListener#onDisplayRemoved(int)
+         * @see DisplayTracker.Callback#onDisplayRemoved(int)
          */
         default void onDisplayRemoved(int displayId) {
         }
@@ -492,14 +494,21 @@
          * @see IStatusBar#enterStageSplitFromRunningApp
          */
         default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
+
+        /**
+         * @see IStatusBar#showMediaOutputSwitcher
+         */
+        default void showMediaOutputSwitcher(String packageName) {}
     }
 
-    public CommandQueue(Context context) {
-        this(context, null, null, null);
+    @VisibleForTesting
+    public CommandQueue(Context context, DisplayTracker displayTracker) {
+        this(context, displayTracker, null, null, null);
     }
 
     public CommandQueue(
             Context context,
+            DisplayTracker displayTracker,
             ProtoTracer protoTracer,
             CommandRegistry registry,
             DumpHandler dumpHandler
@@ -507,36 +516,30 @@
         mProtoTracer = protoTracer;
         mRegistry = registry;
         mDumpHandler = dumpHandler;
-        context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler);
+        mDisplayTracker = displayTracker;
+        mDisplayTracker.addDisplayChangeCallback(new DisplayTracker.Callback() {
+            @Override
+            public void onDisplayRemoved(int displayId) {
+                synchronized (mLock) {
+                    mDisplayDisabled.remove(displayId);
+                }
+                // This callback is registered with {@link #mHandler} that already posts to run
+                // on main thread, so it is safe to dispatch directly.
+                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                    mCallbacks.get(i).onDisplayRemoved(displayId);
+                }
+            }
+        }, new HandlerExecutor(mHandler));
         // We always have default display.
-        setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE);
+        setDisabled(mDisplayTracker.getDefaultDisplayId(), DISABLE_NONE, DISABLE2_NONE);
     }
 
-    @Override
-    public void onDisplayAdded(int displayId) { }
-
-    @Override
-    public void onDisplayRemoved(int displayId) {
-        synchronized (mLock) {
-            mDisplayDisabled.remove(displayId);
-        }
-        // This callback is registered with {@link #mHandler} that already posts to run on main
-        // thread, so it is safe to dispatch directly.
-        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-            mCallbacks.get(i).onDisplayRemoved(displayId);
-        }
-    }
-
-    @Override
-    public void onDisplayChanged(int displayId) { }
-
     // TODO(b/118592525): add multi-display support if needed.
     public boolean panelsEnabled() {
-        final int disabled1 = getDisabled1(DEFAULT_DISPLAY);
-        final int disabled2 = getDisabled2(DEFAULT_DISPLAY);
+        final int disabled1 = getDisabled1(mDisplayTracker.getDefaultDisplayId());
+        final int disabled2 = getDisabled2(mDisplayTracker.getDefaultDisplayId());
         return (disabled1 & StatusBarManager.DISABLE_EXPAND) == 0
-                && (disabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0
-                && !ONLY_CORE_APPS;
+                && (disabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0;
     }
 
     @Override
@@ -1262,6 +1265,19 @@
     }
 
     @Override
+    public void showMediaOutputSwitcher(String packageName) {
+        int callingUid = Binder.getCallingUid();
+        if (callingUid != 0 && callingUid != Process.SYSTEM_UID) {
+            throw new SecurityException("Call only allowed from system server.");
+        }
+        synchronized (mLock) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = packageName;
+            mHandler.obtainMessage(MSG_SHOW_MEDIA_OUTPUT_SWITCHER, args).sendToTarget();
+        }
+    }
+
+    @Override
     public void requestAddTile(
             @NonNull ComponentName componentName,
             @NonNull CharSequence appName,
@@ -1583,7 +1599,7 @@
                 }
                 case MSG_SET_BIOMETRICS_LISTENER:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).setBiometicContextListener(
+                        mCallbacks.get(i).setBiometricContextListener(
                                 (IBiometricContextListener) msg.obj);
                     }
                     break;
@@ -1777,6 +1793,13 @@
                         mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj);
                     }
                     break;
+                case MSG_SHOW_MEDIA_OUTPUT_SWITCHER:
+                    args = (SomeArgs) msg.obj;
+                    String clientPackageName = (String) args.arg1;
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName);
+                    }
+                    break;
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index 63179da..5adb58b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -77,6 +77,12 @@
      */
     public static void fadeOut(View view, float fadeOutAmount, boolean remap) {
         view.animate().cancel();
+
+        // Don't fade out if already not visible.
+        if (view.getAlpha() == 0.0f) {
+            return;
+        }
+
         if (fadeOutAmount == 1.0f && view.getVisibility() != View.GONE) {
             view.setVisibility(View.INVISIBLE);
         } else if (view.getVisibility() == View.INVISIBLE) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java
new file mode 100644
index 0000000..a797d4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.content.BroadcastReceiver;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+
+/**
+ * Module for {@link com.android.systemui.KeyboardShortcutsReceiver}.
+ */
+@Module
+public abstract class KeyboardShortcutsModule {
+
+    /**
+     *
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(KeyboardShortcutsReceiver.class)
+    public abstract BroadcastReceiver bindKeyboardShortcutsReceiver(
+            KeyboardShortcutsReceiver broadcastReceiver);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 6a658b6..f4782c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -25,7 +25,9 @@
 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;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.IMPORTANT_MSG_MIN_DURATION;
@@ -41,7 +43,9 @@
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED;
 import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
 import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
+import static com.android.systemui.plugins.log.LogLevel.ERROR;
 
+import android.app.AlarmManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -90,12 +94,15 @@
 import com.android.systemui.keyguard.KeyguardIndication;
 import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController;
 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;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.AlarmTimeout;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.wakelock.SettableWakeLock;
 import com.android.systemui.util.wakelock.WakeLock;
@@ -122,13 +129,11 @@
 @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_HIDE_TRANSIENT = 1;
-    private static final int MSG_SHOW_ACTION_TO_UNLOCK = 2;
-    private static final int MSG_HIDE_BIOMETRIC_MESSAGE = 3;
-    private static final int MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON = 4;
+    private static final int MSG_SHOW_ACTION_TO_UNLOCK = 1;
+    private static final int MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON = 2;
     private static final long TRANSIENT_BIOMETRIC_ERROR_TIMEOUT = 1300;
     public static final long DEFAULT_HIDE_DELAY_MS =
             3500 + KeyguardIndicationTextView.Y_IN_DURATION;
@@ -155,6 +160,7 @@
     private final KeyguardBypassController mKeyguardBypassController;
     private final AccessibilityManager mAccessibilityManager;
     private final Handler mHandler;
+    private final AlternateBouncerInteractor mAlternateBouncerInteractor;
 
     @VisibleForTesting
     public KeyguardIndicationRotateTextViewController mRotateTextViewController;
@@ -209,6 +215,11 @@
     };
     private boolean mFaceLockedOutThisAuthSession;
 
+    // Use AlarmTimeouts to guarantee that the events are handled even if scheduled and
+    // triggered while the device is asleep
+    private final AlarmTimeout mHideTransientMessageHandler;
+    private final AlarmTimeout mHideBiometricMessageHandler;
+
     /**
      * Creates a new KeyguardIndicationController and registers callbacks.
      */
@@ -234,7 +245,10 @@
             KeyguardBypassController keyguardBypassController,
             AccessibilityManager accessibilityManager,
             FaceHelpMessageDeferral faceHelpMessageDeferral,
-            KeyguardLogger keyguardLogger) {
+            KeyguardLogger keyguardLogger,
+            AlternateBouncerInteractor alternateBouncerInteractor,
+            AlarmManager alarmManager
+    ) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
         mDevicePolicyManager = devicePolicyManager;
@@ -256,6 +270,7 @@
         mScreenLifecycle = screenLifecycle;
         mKeyguardLogger = keyguardLogger;
         mScreenLifecycle.addObserver(mScreenObserver);
+        mAlternateBouncerInteractor = alternateBouncerInteractor;
 
         mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
         mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
@@ -268,17 +283,26 @@
         mHandler = new Handler(mainLooper) {
             @Override
             public void handleMessage(Message msg) {
-                if (msg.what == MSG_HIDE_TRANSIENT) {
-                    hideTransientIndication();
-                } else if (msg.what == MSG_SHOW_ACTION_TO_UNLOCK) {
+                if (msg.what == MSG_SHOW_ACTION_TO_UNLOCK) {
                     showActionToUnlock();
-                } else if (msg.what == MSG_HIDE_BIOMETRIC_MESSAGE) {
-                    hideBiometricMessage();
                 } else if (msg.what == MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON) {
                     mBiometricErrorMessageToShowOnScreenOn = null;
                 }
             }
         };
+
+        mHideTransientMessageHandler = new AlarmTimeout(
+                alarmManager,
+                this::hideTransientIndication,
+                TAG,
+                mHandler
+        );
+        mHideBiometricMessageHandler = new AlarmTimeout(
+                alarmManager,
+                this::hideBiometricMessage,
+                TAG,
+                mHandler
+        );
     }
 
     /** Call this after construction to finish setting up the instance. */
@@ -305,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) {
@@ -330,6 +356,8 @@
      */
     public void destroy() {
         mHandler.removeCallbacksAndMessages(null);
+        mHideBiometricMessageHandler.cancel();
+        mHideTransientMessageHandler.cancel();
         mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
     }
 
@@ -516,23 +544,23 @@
                             .build(),
                     true
             );
-            if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) {
-                mRotateTextViewController.updateIndication(
-                        INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
-                        new KeyguardIndication.Builder()
-                                .setMessage(mBiometricMessageFollowUp)
-                                .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
-                                .setTextColor(mInitialTextColorState)
-                                .build(),
-                        true
-                );
-            } else {
-                mRotateTextViewController.hideIndication(
-                        INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
-            }
         } else {
-            mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE);
-            mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
+            mRotateTextViewController.hideIndication(
+                    INDICATION_TYPE_BIOMETRIC_MESSAGE);
+        }
+        if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) {
+            mRotateTextViewController.updateIndication(
+                    INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                    new KeyguardIndication.Builder()
+                            .setMessage(mBiometricMessageFollowUp)
+                            .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
+                            .setTextColor(mInitialTextColorState)
+                            .build(),
+                    true
+            );
+        } else {
+            mRotateTextViewController.hideIndication(
+                    INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
         }
     }
 
@@ -674,7 +702,7 @@
         if (visible) {
             // If this is called after an error message was already shown, we should not clear it.
             // Otherwise the error message won't be shown
-            if (!mHandler.hasMessages(MSG_HIDE_TRANSIENT)) {
+            if (!mHideTransientMessageHandler.isScheduled()) {
                 hideTransientIndication();
             }
             updateDeviceEntryIndication(false);
@@ -722,16 +750,14 @@
      * Hides transient indication in {@param delayMs}.
      */
     public void hideTransientIndicationDelayed(long delayMs) {
-        mHandler.sendMessageDelayed(
-                mHandler.obtainMessage(MSG_HIDE_TRANSIENT), delayMs);
+        mHideTransientMessageHandler.schedule(delayMs, AlarmTimeout.MODE_RESCHEDULE_IF_SCHEDULED);
     }
 
     /**
      * Hides biometric indication in {@param delayMs}.
      */
     public void hideBiometricMessageDelayed(long delayMs) {
-        mHandler.sendMessageDelayed(
-                mHandler.obtainMessage(MSG_HIDE_BIOMETRIC_MESSAGE), delayMs);
+        mHideBiometricMessageHandler.schedule(delayMs, AlarmTimeout.MODE_RESCHEDULE_IF_SCHEDULED);
     }
 
     /**
@@ -746,7 +772,6 @@
      */
     private void showTransientIndication(CharSequence transientIndication) {
         mTransientIndication = transientIndication;
-        mHandler.removeMessages(MSG_HIDE_TRANSIENT);
         hideTransientIndicationDelayed(DEFAULT_HIDE_DELAY_MS);
 
         updateTransient();
@@ -764,7 +789,8 @@
      */
     private void showBiometricMessage(CharSequence biometricMessage,
             @Nullable CharSequence biometricMessageFollowUp) {
-        if (TextUtils.equals(biometricMessage, mBiometricMessage)) {
+        if (TextUtils.equals(biometricMessage, mBiometricMessage)
+                && TextUtils.equals(biometricMessageFollowUp, mBiometricMessageFollowUp)) {
             return;
         }
 
@@ -772,9 +798,9 @@
         mBiometricMessageFollowUp = biometricMessageFollowUp;
 
         mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
-        mHandler.removeMessages(MSG_HIDE_BIOMETRIC_MESSAGE);
         hideBiometricMessageDelayed(
-                mBiometricMessageFollowUp != null
+                !TextUtils.isEmpty(mBiometricMessage)
+                        && !TextUtils.isEmpty(mBiometricMessageFollowUp)
                         ? IMPORTANT_MSG_MIN_DURATION * 2
                         : DEFAULT_HIDE_DELAY_MS
         );
@@ -786,7 +812,7 @@
         if (mBiometricMessage != null || mBiometricMessageFollowUp != null) {
             mBiometricMessage = null;
             mBiometricMessageFollowUp = null;
-            mHandler.removeMessages(MSG_HIDE_BIOMETRIC_MESSAGE);
+            mHideBiometricMessageHandler.cancel();
             updateBiometricMessage();
         }
     }
@@ -797,7 +823,7 @@
     public void hideTransientIndication() {
         if (mTransientIndication != null) {
             mTransientIndication = null;
-            mHandler.removeMessages(MSG_HIDE_TRANSIENT);
+            mHideTransientMessageHandler.cancel();
             updateTransient();
         }
     }
@@ -808,6 +834,7 @@
      * may continuously be cycled through.
      */
     protected final void updateDeviceEntryIndication(boolean animate) {
+        mKeyguardLogger.logUpdateDeviceEntryIndication(animate, mVisible, mDozing);
         if (!mVisible) {
             return;
         }
@@ -928,7 +955,7 @@
         }
 
         if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
-            if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+            if (mAlternateBouncerInteractor.isVisibleState()) {
                 return; // udfps affordance is highlighted, no need to show action to unlock
             } else if (mKeyguardUpdateMonitor.isFaceEnrolled()
                     && !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) {
@@ -1028,7 +1055,7 @@
                 mChargingTimeRemaining = mPowerPluggedIn
                         ? mBatteryInfo.computeChargeTimeRemaining() : -1;
             } catch (RemoteException e) {
-                mKeyguardLogger.logException(e, "Error calling IBatteryStats");
+                mKeyguardLogger.log(TAG, ERROR, "Error calling IBatteryStats", e);
                 mChargingTimeRemaining = -1;
             }
             updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired);
@@ -1058,20 +1085,27 @@
                 }
             }
 
+            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
+                    && msgId == BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED; // ran matcher & failed
             final boolean isUnlockWithFingerprintPossible = canUnlockWithFingerprint();
             final boolean isCoExFaceAcquisitionMessage =
                     faceAuthSoftError && isUnlockWithFingerprintPossible;
@@ -1094,6 +1128,29 @@
                             mContext.getString(R.string.keyguard_face_failed),
                             mContext.getString(R.string.keyguard_suggest_fingerprint)
                     );
+                } else if (fpAuthFailed
+                        && mKeyguardUpdateMonitor.getUserUnlockedWithFace(getCurrentUser())) {
+                    // face had already previously unlocked the device, so instead of showing a
+                    // fingerprint error, tell them they have already unlocked with face auth
+                    // and how to enter their device
+                    showBiometricMessage(
+                            mContext.getString(R.string.keyguard_face_successful_unlock),
+                            mContext.getString(R.string.keyguard_unlock)
+                    );
+                } else if (fpAuthFailed
+                        && mKeyguardUpdateMonitor.getUserHasTrust(
+                                KeyguardUpdateMonitor.getCurrentUser())) {
+                    showBiometricMessage(
+                            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);
                 }
@@ -1377,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/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 905cc3f..f0d064b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -187,6 +187,8 @@
 
     private val qsTransitionController = qsTransitionControllerFactory.create { qS }
 
+    private val callbacks = mutableListOf<Callback>()
+
     /** See [LockscreenShadeQsTransitionController.qsTransitionFraction].*/
     @get:FloatRange(from = 0.0, to = 1.0)
     val qSDragProgress: Float
@@ -262,7 +264,6 @@
 
     fun setStackScroller(nsslController: NotificationStackScrollLayoutController) {
         this.nsslController = nsslController
-        touchHelper.host = nsslController.view
         touchHelper.expandCallback = nsslController.expandHelperCallback
     }
 
@@ -320,8 +321,8 @@
                                     true /* drag down is always an open */)
                         }
                         notificationPanelController.animateToFullShade(delay)
-                        notificationPanelController.setTransitionToFullShadeAmount(0f,
-                                true /* animated */, delay)
+                        callbacks.forEach { it.setTransitionToFullShadeAmount(0f,
+                                true /* animated */, delay) }
 
                         // Let's reset ourselves, ready for the next animation
 
@@ -425,8 +426,8 @@
 
                     qsTransitionController.dragDownAmount = value
 
-                    notificationPanelController.setTransitionToFullShadeAmount(field,
-                            false /* animate */, 0 /* delay */)
+                    callbacks.forEach { it.setTransitionToFullShadeAmount(field,
+                            false /* animate */, 0 /* delay */) }
 
                     mediaHierarchyManager.setTransitionToFullShadeAmount(field)
                     scrimTransitionController.dragDownAmount = value
@@ -509,9 +510,14 @@
      * If secure with redaction: Show bouncer, go to unlocked shade.
      * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
      *
+     * Split shade is special case and [needsQSAnimation] will be always overridden to true.
+     * That's because handheld shade will automatically follow notifications animation, but that's
+     * not the case for split shade.
+     *
      * @param expandView The view to expand after going to the shade
      * @param needsQSAnimation if this needs the quick settings to slide in from the top or if
-     *                         that's already handled separately
+     *                         that's already handled separately. This argument will be ignored on
+     *                         split shade as there QS animation can't be handled separately.
      */
     @JvmOverloads
     fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) {
@@ -519,7 +525,7 @@
         logger.logTryGoToLockedShade(isKeyguard)
         if (isKeyguard) {
             val animationHandler: ((Long) -> Unit)?
-            if (needsQSAnimation) {
+            if (needsQSAnimation || useSplitShade) {
                 // Let's use the default animation
                 animationHandler = null
             } else {
@@ -684,7 +690,7 @@
         if (cancelled) {
             setPulseHeight(0f, animate = true)
         } else {
-            notificationPanelController.onPulseExpansionFinished()
+            callbacks.forEach { it.onPulseExpansionFinished() }
             setPulseHeight(0f, animate = false)
         }
     }
@@ -716,6 +722,27 @@
                 "${animationHandlerOnKeyguardDismiss != null}")
         }
     }
+
+
+    fun addCallback(callback: Callback) {
+        if (!callbacks.contains(callback)) {
+            callbacks.add(callback)
+        }
+    }
+
+    /**
+     * Callback for authentication events.
+     */
+    interface Callback {
+        /** TODO: comment here  */
+        fun onPulseExpansionFinished() {}
+
+        /**
+         * Sets the amount of pixels we have currently dragged down if we're transitioning
+         * to the full shade. 0.0f means we're not transitioning yet.
+         */
+        fun setTransitionToFullShadeAmount(pxAmount: Float, animate: Boolean, delay: Long) {}
+    }
 }
 
 /**
@@ -731,14 +758,12 @@
 
     private var dragDownAmountOnStart = 0.0f
     lateinit var expandCallback: ExpandHelper.Callback
-    lateinit var host: View
 
     private var minDragDistance = 0
     private var initialTouchX = 0f
     private var initialTouchY = 0f
     private var touchSlop = 0f
     private var slopMultiplier = 0f
-    private val temp2 = IntArray(2)
     private var draggedFarEnough = false
     private var startingChild: ExpandableView? = null
     private var lastHeight = 0f
@@ -918,7 +943,6 @@
     }
 
     private fun findView(x: Float, y: Float): ExpandableView? {
-        host.getLocationOnScreen(temp2)
-        return expandCallback.getChildAtRawPosition(x + temp2[0], y + temp2[1])
+        return expandCallback.getChildAtRawPosition(x, y)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 56b689e..7d0ac18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -290,7 +290,8 @@
                     false,
                     null,
                     0,
-                    false
+                    false,
+                    0
             );
         }
         return ranking;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index f4cd985..51c5183 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -213,7 +213,8 @@
             DeviceProvisionedController deviceProvisionedController,
             KeyguardStateController keyguardStateController,
             SecureSettings secureSettings,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            LockPatternUtils lockPatternUtils) {
         mContext = context;
         mMainHandler = mainHandler;
         mDevicePolicyManager = devicePolicyManager;
@@ -225,7 +226,7 @@
         mClickNotifier = clickNotifier;
         mOverviewProxyServiceLazy = overviewProxyServiceLazy;
         statusBarStateController.addCallback(this);
-        mLockPatternUtils = new LockPatternUtils(context);
+        mLockPatternUtils = lockPatternUtils;
         mKeyguardManager = keyguardManager;
         mBroadcastDispatcher = broadcastDispatcher;
         mDeviceProvisionedController = deviceProvisionedController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 8f9365c..99081e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -65,8 +65,6 @@
 import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.ListenerSet;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -74,6 +72,8 @@
 import java.util.Optional;
 import java.util.function.Consumer;
 
+import dagger.Lazy;
+
 /**
  * Class for handling remote input state over a set of notifications. This class handles things
  * like keeping notifications temporarily that were cancelled as a response to a remote input
@@ -465,8 +465,7 @@
         riv.getController().setRemoteInput(input);
         riv.getController().setRemoteInputs(inputs);
         riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
-        ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
-        riv.focusAnimated(parent);
+        riv.focusAnimated();
         if (userMessageContent != null) {
             riv.setEditTextContent(userMessageContent);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 0b1807d..2ca0b00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -143,6 +143,9 @@
     /** Sets the state of whether sysui is dozing or not. */
     default void setDozing(boolean dozing) {}
 
+    /** Sets the state of whether sysui is dreaming or not. */
+    default void setDreaming(boolean dreaming) {}
+
     /** Sets the state of whether plugin open is forced or not. */
     default void setForcePluginOpen(boolean forcePluginOpen, Object token) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 56c34a0..8f1e0a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -23,6 +23,7 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
 import android.view.View;
 import android.view.ViewGroup;
@@ -52,6 +53,9 @@
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
 import com.android.systemui.statusbar.notification.stack.ViewState;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.util.DumpUtilsKt;
+
+import java.io.PrintWriter;
 
 /**
  * A notification shelf view that is placed inside the notification scroller. It manages the
@@ -86,7 +90,6 @@
     private boolean mInteractive;
     private boolean mAnimationsEnabled = true;
     private boolean mShowNotificationShelf;
-    private float mFirstElementRoundness;
     private Rect mClipRect = new Rect();
     private int mIndexOfFirstViewInShelf = -1;
     private float mCornerAnimationDistance;
@@ -263,8 +266,7 @@
         final float actualWidth = mAmbientState.isOnKeyguard()
                 ? MathUtils.lerp(shortestWidth, getWidth(), fractionToShade)
                 : getWidth();
-        ActivatableNotificationView anv = (ActivatableNotificationView) this;
-        anv.setBackgroundWidth((int) actualWidth);
+        setBackgroundWidth((int) actualWidth);
         if (mShelfIcons != null) {
             mShelfIcons.setActualLayoutWidth((int) actualWidth);
         }
@@ -365,9 +367,7 @@
         boolean expandingAnimated = mAmbientState.isExpansionChanging()
                 && !mAmbientState.isPanelTracking();
         int baseZHeight = mAmbientState.getBaseZHeight();
-        int backgroundTop = 0;
         int clipTopAmount = 0;
-        float firstElementRoundness = 0.0f;
 
         for (int i = 0; i < mHostLayoutController.getChildCount(); i++) {
             ExpandableView child = mHostLayoutController.getChildAt(i);
@@ -420,18 +420,6 @@
                 if (notGoneIndex != 0 || !aboveShelf) {
                     expandableRow.setAboveShelf(false);
                 }
-                if (notGoneIndex == 0) {
-                    StatusBarIconView icon = expandableRow.getEntry().getIcons().getShelfIcon();
-                    NotificationIconContainer.IconState iconState = getIconState(icon);
-                    // The icon state might be null in rare cases where the notification is actually
-                    // added to the layout, but not to the shelf. An example are replied messages,
-                    // since they don't show up on AOD
-                    if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
-                        // only if the first icon is fully in the shelf we want to clip to it!
-                        backgroundTop = (int) (child.getTranslationY() - getTranslationY());
-                        firstElementRoundness = expandableRow.getTopRoundness();
-                    }
-                }
 
                 previousColor = ownColorUntinted;
                 notGoneIndex++;
@@ -467,8 +455,6 @@
 
         // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa.
         setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
-        setBackgroundTop(backgroundTop);
-        setFirstElementRoundness(firstElementRoundness);
         mShelfIcons.setSpeedBumpIndex(mHostLayoutController.getSpeedBumpIndex());
         mShelfIcons.calculateIconXTranslations();
         mShelfIcons.applyIconStates();
@@ -570,12 +556,6 @@
         }
     }
 
-    private void setFirstElementRoundness(float firstElementRoundness) {
-        if (mFirstElementRoundness != firstElementRoundness) {
-            mFirstElementRoundness = firstElementRoundness;
-        }
-    }
-
     private void updateIconClipAmount(ExpandableNotificationRow row) {
         float maxTop = row.getTranslationY();
         if (getClipTopAmount() != 0) {
@@ -1011,6 +991,18 @@
         expandableView.requestRoundnessReset(LegacySourceType.OnScroll);
     }
 
+    @Override
+    public void dump(PrintWriter pwOriginal, String[] args) {
+        IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
+        super.dump(pw, args);
+        if (DUMP_VERBOSE) {
+            DumpUtilsKt.withIncreasedIndent(pw, () -> {
+                pw.println("mActualWidth: " + mActualWidth);
+                pw.println("mStatusBarHeight: " + mStatusBarHeight);
+            });
+        }
+    }
+
     public class ShelfState extends ExpandableViewState {
         private boolean hasItemsInStableShelf;
         private ExpandableView firstViewInShelf;
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/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 186e6dc..784e2d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -129,6 +129,11 @@
     private boolean mIsDozing;
 
     /**
+     * If the device is currently dreaming or not.
+     */
+    private boolean mIsDreaming;
+
+    /**
      * If the status bar is currently expanded or not.
      */
     private boolean mIsExpanded;
@@ -294,6 +299,29 @@
     }
 
     @Override
+    public boolean setIsDreaming(boolean isDreaming) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "setIsDreaming:" + isDreaming);
+        }
+        if (mIsDreaming == isDreaming) {
+            return false;
+        }
+
+        mIsDreaming = isDreaming;
+
+        synchronized (mListeners) {
+            String tag = getClass().getSimpleName() + "#setIsDreaming";
+            DejankUtils.startDetectingBlockingIpcs(tag);
+            for (RankedListener rl : new ArrayList<>(mListeners)) {
+                rl.mListener.onDreamingChanged(isDreaming);
+            }
+            DejankUtils.stopDetectingBlockingIpcs(tag);
+        }
+
+        return true;
+    }
+
+    @Override
     public void setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated) {
         if (mDarkAnimator != null && mDarkAnimator.isRunning()) {
             if (animated && mDozeAmountTarget == dozeAmount) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index e0cf812..088c568 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -99,6 +99,13 @@
     boolean setIsDozing(boolean isDozing);
 
     /**
+     * Update the dreaming state from {@link CentralSurfaces}'s perspective
+     * @param isDreaming whether we are dreaming
+     * @return {@code true} if the state changed, else {@code false}
+     */
+    boolean setIsDreaming(boolean isDreaming);
+
+    /**
      * Changes the current doze amount, also starts the
      * {@link com.android.internal.jank.InteractionJankMonitor InteractionJankMonitor} as possible.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
new file mode 100644
index 0000000..1099810
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.connectivity
+
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.AirplaneModeTile
+import com.android.systemui.qs.tiles.BluetoothTile
+import com.android.systemui.qs.tiles.CastTile
+import com.android.systemui.qs.tiles.DataSaverTile
+import com.android.systemui.qs.tiles.HotspotTile
+import com.android.systemui.qs.tiles.InternetTile
+import com.android.systemui.qs.tiles.NfcTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface ConnectivityModule {
+
+    /** Inject InternetTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(InternetTile.TILE_SPEC)
+    fun bindInternetTile(internetTile: InternetTile): QSTileImpl<*>
+
+    /** Inject BluetoothTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(BluetoothTile.TILE_SPEC)
+    fun bindBluetoothTile(bluetoothTile: BluetoothTile): QSTileImpl<*>
+
+    /** Inject CastTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(CastTile.TILE_SPEC)
+    fun bindCastTile(castTile: CastTile): QSTileImpl<*>
+
+    /** Inject HotspotTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(HotspotTile.TILE_SPEC)
+    fun bindHotspotTile(hotspotTile: HotspotTile): QSTileImpl<*>
+
+    /** Inject AirplaneModeTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(AirplaneModeTile.TILE_SPEC)
+    fun bindAirplaneModeTile(airplaneModeTile: AirplaneModeTile): QSTileImpl<*>
+
+    /** Inject DataSaverTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(DataSaverTile.TILE_SPEC)
+    fun bindDataSaverTile(dataSaverTile: DataSaverTile): QSTileImpl<*>
+
+    /** Inject NfcTile into tileMap in QSModule */
+    @Binds @IntoMap @StringKey(NfcTile.TILE_SPEC) fun bindNfcTile(nfcTile: NfcTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
index f960eb7..ed32008 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
@@ -47,8 +47,6 @@
     void addEmergencyListener(EmergencyListener listener);
     /** */
     void removeEmergencyListener(EmergencyListener listener);
-    /** */
-    boolean hasEmergencyCryptKeeperText();
 
     /** */
     boolean isRadioOn();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 5f5418f..6e74542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -84,7 +84,6 @@
 import com.android.systemui.statusbar.policy.DataSaverControllerImpl;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
-import com.android.systemui.statusbar.policy.EncryptionHelper;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.CarrierConfigTracker;
 
@@ -1486,11 +1485,6 @@
     }
 
     /** */
-    public boolean hasEmergencyCryptKeeperText() {
-        return EncryptionHelper.IS_DATA_ENCRYPTED;
-    }
-
-    /** */
     public boolean isRadioOn() {
         return !mAirplaneMode;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 14d0d7e..d7568a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -25,16 +25,21 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.AnimationFeatureFlags;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.carrier.QSCarrierGroupController;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.MediaArtworkProcessor;
@@ -61,7 +66,6 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarIconList;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags;
@@ -181,11 +185,12 @@
     @SysUISingleton
     static CommandQueue provideCommandQueue(
             Context context,
+            DisplayTracker displayTracker,
             ProtoTracer protoTracer,
             CommandRegistry registry,
             DumpHandler dumpHandler
     ) {
-        return new CommandQueue(context, protoTracer, registry, dumpHandler);
+        return new CommandQueue(context, displayTracker, protoTracer, registry, dumpHandler);
     }
 
     /**
@@ -280,8 +285,9 @@
     @SysUISingleton
     static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager,
             KeyguardStateController keyguardStateController,
-            Lazy<StatusBarKeyguardViewManager> statusBarKeyguardViewManager,
-            InteractionJankMonitor interactionJankMonitor) {
+            Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
+            InteractionJankMonitor interactionJankMonitor,
+            AnimationFeatureFlags animationFeatureFlags) {
         DialogLaunchAnimator.Callback callback = new DialogLaunchAnimator.Callback() {
             @Override
             public boolean isDreaming() {
@@ -300,9 +306,22 @@
 
             @Override
             public boolean isShowingAlternateAuthOnUnlock() {
-                return statusBarKeyguardViewManager.get().canShowAlternateBouncer();
+                return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint();
             }
         };
-        return new DialogLaunchAnimator(callback, interactionJankMonitor);
+        return new DialogLaunchAnimator(callback, interactionJankMonitor, animationFeatureFlags);
+    }
+
+    /**
+     */
+    @Provides
+    @SysUISingleton
+    static AnimationFeatureFlags provideAnimationFeatureFlags(FeatureFlags featureFlags) {
+        return new AnimationFeatureFlags() {
+            @Override
+            public boolean isPredictiveBackQsDialogAnim() {
+                return featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM);
+            }
+        };
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index 737b481..6a8d5c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.events
 
-import android.animation.Animator
+import androidx.core.animation.Animator
 import android.annotation.UiThread
 import android.graphics.Point
 import android.graphics.Rect
@@ -62,7 +62,7 @@
  */
 
 @SysUISingleton
-class PrivacyDotViewController @Inject constructor(
+open class PrivacyDotViewController @Inject constructor(
     @Main private val mainExecutor: Executor,
     private val stateController: StatusBarStateController,
     private val configurationController: ConfigurationController,
@@ -176,7 +176,7 @@
     }
 
     @UiThread
-    private fun hideDotView(dot: View, animate: Boolean) {
+    open fun hideDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.animate()
@@ -195,7 +195,7 @@
     }
 
     @UiThread
-    private fun showDotView(dot: View, animate: Boolean) {
+    open fun showDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.visibility = View.VISIBLE
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
new file mode 100644
index 0000000..3d6d489
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.events
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+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.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineScope
+
+@Module
+interface StatusBarEventsModule {
+
+    companion object {
+
+        @Provides
+        @SysUISingleton
+        fun provideSystemStatusAnimationScheduler(
+                featureFlags: FeatureFlags,
+                coordinator: SystemEventCoordinator,
+                chipAnimationController: SystemEventChipAnimationController,
+                statusBarWindowController: StatusBarWindowController,
+                dumpManager: DumpManager,
+                systemClock: SystemClock,
+                @Application coroutineScope: CoroutineScope,
+                @Main executor: DelayableExecutor
+        ): SystemStatusAnimationScheduler {
+            return if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
+                SystemStatusAnimationSchedulerImpl(
+                        coordinator,
+                        chipAnimationController,
+                        statusBarWindowController,
+                        dumpManager,
+                        systemClock,
+                        coroutineScope
+                )
+            } else {
+                SystemStatusAnimationSchedulerLegacyImpl(
+                        coordinator,
+                        chipAnimationController,
+                        statusBarWindowController,
+                        dumpManager,
+                        systemClock,
+                        executor
+                )
+            }
+        }
+    }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt
index 4e1404d..43f78c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt
@@ -16,24 +16,21 @@
 
 package com.android.systemui.statusbar.events
 
+import android.annotation.IntRange
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.view.LayoutInflater
 import android.view.View
 import android.widget.ImageView
-import com.android.settingslib.graph.ThemedBatteryDrawable
-import com.android.systemui.R
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.privacy.PrivacyItem
+import com.android.systemui.statusbar.BatteryStatusChip
 
 typealias ViewCreator = (context: Context) -> BackgroundAnimatableView
 
 interface StatusEvent {
     val priority: Int
     // Whether or not to force the status bar open and show a dot
-    val forceVisible: Boolean
+    var forceVisible: Boolean
     // Whether or not to show an animation for this event
     val showAnimation: Boolean
     val viewCreator: ViewCreator
@@ -73,17 +70,16 @@
     }
 }
 
-class BatteryEvent : StatusEvent {
+class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : StatusEvent {
     override val priority = 50
-    override val forceVisible = false
+    override var forceVisible = false
     override val showAnimation = true
     override var contentDescription: String? = ""
 
-    override val viewCreator: (context: Context) -> BGImageView = { context ->
-        val iv = BGImageView(context)
-        iv.setImageDrawable(ThemedBatteryDrawable(context, Color.WHITE))
-        iv.setBackgroundDrawable(ColorDrawable(Color.GREEN))
-        iv
+    override val viewCreator: ViewCreator = { context ->
+        BatteryStatusChip(context).apply {
+            setBatteryLevel(batteryLevel)
+        }
     }
 
     override fun toString(): String {
@@ -94,13 +90,12 @@
 class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent {
     override var contentDescription: String? = null
     override val priority = 100
-    override val forceVisible = true
+    override var forceVisible = true
     var privacyItems: List<PrivacyItem> = listOf()
     private var privacyChip: OngoingPrivacyChip? = null
 
     override val viewCreator: ViewCreator = { context ->
-        val v = LayoutInflater.from(context)
-                .inflate(R.layout.ongoing_privacy_chip, null) as OngoingPrivacyChip
+        val v = OngoingPrivacyChip(context)
         v.privacyList = privacyItems
         v.contentDescription = contentDescription
         privacyChip = v
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 8405aea..776956a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -16,10 +16,6 @@
 
 package com.android.systemui.statusbar.events
 
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Rect
 import android.view.ContextThemeWrapper
@@ -30,7 +26,13 @@
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.AnimatorSet
+import androidx.core.animation.ValueAnimator
 import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
@@ -43,7 +45,8 @@
 class SystemEventChipAnimationController @Inject constructor(
     private val context: Context,
     private val statusBarWindowController: StatusBarWindowController,
-    private val contentInsetsProvider: StatusBarContentInsetsProvider
+    private val contentInsetsProvider: StatusBarContentInsetsProvider,
+    private val featureFlags: FeatureFlags
 ) : SystemStatusAnimationCallback {
 
     private lateinit var animationWindowView: FrameLayout
@@ -53,12 +56,14 @@
 
     // Left for LTR, Right for RTL
     private var animationDirection = LEFT
-    private var chipRight = 0
-    private var chipLeft = 0
-    private var chipWidth = 0
+    private var chipBounds = Rect()
+    private val chipWidth get() = chipBounds.width()
+    private val chipRight get() = chipBounds.right
+    private val chipLeft get() = chipBounds.left
     private var chipMinWidth = context.resources.getDimensionPixelSize(
             R.dimen.ongoing_appops_chip_min_animation_width)
-    private var dotSize = context.resources.getDimensionPixelSize(
+
+    private val dotSize = context.resources.getDimensionPixelSize(
             R.dimen.ongoing_appops_dot_diameter)
     // Use during animation so that multiple animators can update the drawing rect
     private var animRect = Rect()
@@ -90,21 +95,26 @@
             it.view.measure(
                     View.MeasureSpec.makeMeasureSpec(
                             (animationWindowView.parent as View).width, AT_MOST),
-                    View.MeasureSpec.makeMeasureSpec(animationWindowView.height, AT_MOST))
-            chipWidth = it.chipWidth
-        }
+                    View.MeasureSpec.makeMeasureSpec(
+                            (animationWindowView.parent as View).height, AT_MOST))
 
-        // decide which direction we're animating from, and then set some screen coordinates
-        val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
-        when (animationDirection) {
-            LEFT -> {
-                chipRight = contentRect.right
-                chipLeft = contentRect.right - chipWidth
+            // decide which direction we're animating from, and then set some screen coordinates
+            val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
+            val chipTop = ((animationWindowView.parent as View).height - it.view.measuredHeight) / 2
+            val chipBottom = chipTop + it.view.measuredHeight
+            val chipRight: Int
+            val chipLeft: Int
+            when (animationDirection) {
+                LEFT -> {
+                    chipRight = contentRect.right
+                    chipLeft = contentRect.right - it.chipWidth
+                }
+                else /* RIGHT */ -> {
+                    chipLeft = contentRect.left
+                    chipRight = contentRect.left + it.chipWidth
+                }
             }
-            else /* RIGHT */ -> {
-                chipLeft = contentRect.left
-                chipRight = contentRect.left + chipWidth
-            }
+            chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
         }
     }
 
@@ -117,16 +127,21 @@
             interpolator = null
             addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
         }
+        currentAnimatedView?.contentView?.alpha = 0f
+        val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
+            startDelay = 10.frames
+            duration = 10.frames
+            interpolator = null
+            addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
+        }
         val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
             startDelay = 7.frames
             duration = 23.frames
             interpolator = STATUS_BAR_X_MOVE_IN
-            addUpdateListener {
-                updateAnimatedViewBoundsWidth(animatedValue as Int)
-            }
+            addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
         }
         val animSet = AnimatorSet()
-        animSet.playTogether(alphaIn, moveIn)
+        animSet.playTogether(alphaIn, contentAlphaIn, moveIn)
         return animSet
     }
 
@@ -139,7 +154,7 @@
         }
 
         finish.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) {
+            override fun onAnimationEnd(animation: Animator) {
                 animationWindowView.removeView(currentAnimatedView!!.view)
             }
         })
@@ -152,7 +167,7 @@
             duration = 9.frames
             interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
             addUpdateListener {
-                updateAnimatedViewBoundsWidth(it.animatedValue as Int)
+                updateAnimatedViewBoundsWidth(animatedValue as Int)
             }
         }
 
@@ -161,7 +176,7 @@
             duration = 20.frames
             interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
             addUpdateListener {
-                updateAnimatedViewBoundsWidth(it.animatedValue as Int)
+                updateAnimatedViewBoundsWidth(animatedValue as Int)
             }
         }
 
@@ -174,7 +189,7 @@
             duration = 6.frames
             interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
             addUpdateListener {
-                updateAnimatedViewBoundsHeight(it.animatedValue as Int, chipVerticalCenter)
+                updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
             }
         }
 
@@ -183,7 +198,7 @@
             duration = 15.frames
             interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
             addUpdateListener {
-                updateAnimatedViewBoundsHeight(it.animatedValue as Int, chipVerticalCenter)
+                updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
             }
         }
 
@@ -210,15 +225,32 @@
     }
 
     private fun createMoveOutAnimationDefault(): Animator {
+        val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
+            startDelay = 6.frames
+            duration = 6.frames
+            interpolator = null
+            addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
+        }
+
+        val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = 5.frames
+            interpolator = null
+            addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
+        }
+
         val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
             duration = 23.frames
+            interpolator = STATUS_BAR_X_MOVE_OUT
             addUpdateListener {
                 currentAnimatedView?.apply {
-                    updateAnimatedViewBoundsWidth(it.animatedValue as Int)
+                    updateAnimatedViewBoundsWidth(animatedValue as Int)
                 }
             }
         }
-        return moveOut
+
+        val animSet = AnimatorSet()
+        animSet.playTogether(alphaOut, contentAlphaOut, moveOut)
+        return animSet
     }
 
     private fun init() {
@@ -239,11 +271,15 @@
                 it.marginEnd = marginEnd
             }
 
-    private fun initializeAnimRect() = animRect.set(
-            chipLeft,
-            currentAnimatedView!!.view.top,
-            chipRight,
-            currentAnimatedView!!.view.bottom)
+    private fun initializeAnimRect() = if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
+        animRect.set(chipBounds)
+    } else {
+        animRect.set(
+                chipLeft,
+                currentAnimatedView!!.view.top,
+                chipRight,
+                currentAnimatedView!!.view.bottom)
+    }
 
     /**
      * To be called during an animation, sets the width and updates the current animated chip view
@@ -296,6 +332,8 @@
 interface BackgroundAnimatableView {
     val view: View // Since this can't extend View, add a view prop
         get() = this as View
+    val contentView: View? // This will be alpha faded during appear and disappear animation
+        get() = null
     val chipWidth: Int
         get() = view.measuredWidth
     fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
index fde5d39..26fd230 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.statusbar.events
 
+import android.annotation.IntRange
 import android.content.Context
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_PRIVACY
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.privacy.PrivacyChipBuilder
 import com.android.systemui.privacy.PrivacyItem
 import com.android.systemui.privacy.PrivacyItemController
@@ -37,21 +40,18 @@
     private val systemClock: SystemClock,
     private val batteryController: BatteryController,
     private val privacyController: PrivacyItemController,
-    private val context: Context
+    private val context: Context,
+    private val featureFlags: FeatureFlags
 ) {
     private lateinit var scheduler: SystemStatusAnimationScheduler
 
     fun startObserving() {
-        /* currently unused
         batteryController.addCallback(batteryStateListener)
-        */
         privacyController.addCallback(privacyStateListener)
     }
 
     fun stopObserving() {
-        /* currently unused
         batteryController.removeCallback(batteryStateListener)
-        */
         privacyController.removeCallback(privacyStateListener)
     }
 
@@ -59,12 +59,14 @@
         this.scheduler = s
     }
 
-    fun notifyPluggedIn() {
-        scheduler.onStatusEvent(BatteryEvent())
+    fun notifyPluggedIn(@IntRange(from = 0, to = 100) batteryLevel: Int) {
+        if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
+            scheduler.onStatusEvent(BatteryEvent(batteryLevel))
+        }
     }
 
     fun notifyPrivacyItemsEmpty() {
-        scheduler.setShouldShowPersistentPrivacyIndicator(false)
+        scheduler.removePersistentDot()
     }
 
     fun notifyPrivacyItemsChanged(showAnimation: Boolean = true) {
@@ -79,25 +81,25 @@
     }
 
     private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback {
-        var plugged = false
-        var stateKnown = false
+        private var plugged = false
+        private var stateKnown = false
         override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
             if (!stateKnown) {
                 stateKnown = true
                 plugged = pluggedIn
-                notifyListeners()
+                notifyListeners(level)
                 return
             }
 
             if (plugged != pluggedIn) {
                 plugged = pluggedIn
-                notifyListeners()
+                notifyListeners(level)
             }
         }
 
-        private fun notifyListeners() {
+        private fun notifyListeners(@IntRange(from = 0, to = 100) batteryLevel: Int) {
             // We only care about the plugged in status
-            if (plugged) notifyPluggedIn()
+            if (plugged) notifyPluggedIn(batteryLevel)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 1e7fc93..2a18f1f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -16,298 +16,21 @@
 
 package com.android.systemui.statusbar.events
 
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.AnimatorSet
 import android.annotation.IntDef
-import android.os.Process
-import android.provider.DeviceConfig
-import android.util.Log
-import android.view.animation.PathInterpolator
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorSet
+import androidx.core.animation.PathInterpolator
 import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.policy.CallbackController
-import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.systemui.util.Assert
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import java.io.PrintWriter
-import javax.inject.Inject
 
-/**
- * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD):
- *      - Avoiding log spam by only allowing 12 events per minute (1event/5s)
- *      - Waits 100ms to schedule any event for debouncing/prioritization
- *      - Simple prioritization: Privacy > Battery > connectivity (encoded in [StatusEvent])
- *      - Only schedules a single event, and throws away lowest priority events
- *
- * There are 4 basic stages of animation at play here:
- *      1. System chrome animation OUT
- *      2. Chip animation IN
- *      3. Chip animation OUT; potentially into a dot
- *      4. System chrome animation IN
- *
- * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
- * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
- * their respective views based on the progress of the animator. Interpolation differences TBD
- */
-@SysUISingleton
-class SystemStatusAnimationScheduler @Inject constructor(
-    private val coordinator: SystemEventCoordinator,
-    private val chipAnimationController: SystemEventChipAnimationController,
-    private val statusBarWindowController: StatusBarWindowController,
-    private val dumpManager: DumpManager,
-    private val systemClock: SystemClock,
-    @Main private val executor: DelayableExecutor
-) : CallbackController<SystemStatusAnimationCallback>, Dumpable {
+interface SystemStatusAnimationScheduler :
+        CallbackController<SystemStatusAnimationCallback>, Dumpable {
 
-    companion object {
-        private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
-    }
-    private fun isImmersiveIndicatorEnabled(): Boolean {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
-                PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true)
-    }
+    @SystemAnimationState fun getAnimationState(): Int
 
-    @SystemAnimationState var animationState: Int = IDLE
-        private set
+    fun onStatusEvent(event: StatusEvent)
 
-    /** True if the persistent privacy dot should be active */
-    var hasPersistentDot = false
-        private set
-
-    private var scheduledEvent: StatusEvent? = null
-    private var cancelExecutionRunnable: Runnable? = null
-    private val listeners = mutableSetOf<SystemStatusAnimationCallback>()
-
-    init {
-        coordinator.attachScheduler(this)
-        dumpManager.registerDumpable(TAG, this)
-    }
-
-    fun onStatusEvent(event: StatusEvent) {
-        // Ignore any updates until the system is up and running
-        if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
-            return
-        }
-
-        // Don't deal with threading for now (no need let's be honest)
-        Assert.isMainThread()
-        if ((event.priority > scheduledEvent?.priority ?: -1) &&
-                animationState != ANIMATING_OUT &&
-                (animationState != SHOWING_PERSISTENT_DOT && event.forceVisible)) {
-            // events can only be scheduled if a higher priority or no other event is in progress
-            if (DEBUG) {
-                Log.d(TAG, "scheduling event $event")
-            }
-
-            scheduleEvent(event)
-        } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
-            if (DEBUG) {
-                Log.d(TAG, "updating current event from: $event. animationState=$animationState")
-            }
-            scheduledEvent?.updateFromEvent(event)
-            if (event.forceVisible) {
-                hasPersistentDot = true
-                // If we missed the chance to show the persistent dot, do it now
-                if (animationState == IDLE) {
-                    notifyTransitionToPersistentDot()
-                }
-            }
-        } else {
-            if (DEBUG) {
-                Log.d(TAG, "ignoring event $event")
-            }
-        }
-    }
-
-    private fun clearDotIfVisible() {
-        notifyHidePersistentDot()
-    }
-
-    fun setShouldShowPersistentPrivacyIndicator(should: Boolean) {
-        if (hasPersistentDot == should || !isImmersiveIndicatorEnabled()) {
-            return
-        }
-
-        hasPersistentDot = should
-
-        if (!hasPersistentDot) {
-            clearDotIfVisible()
-        }
-    }
-
-    private fun isTooEarly(): Boolean {
-        return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
-    }
-
-    /**
-     * Clear the scheduled event (if any) and schedule a new one
-     */
-    private fun scheduleEvent(event: StatusEvent) {
-        scheduledEvent = event
-
-        if (event.forceVisible) {
-            hasPersistentDot = true
-        }
-
-        // If animations are turned off, we'll transition directly to the dot
-        if (!event.showAnimation && event.forceVisible) {
-            notifyTransitionToPersistentDot()
-            scheduledEvent = null
-            return
-        }
-
-        chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator)
-        animationState = ANIMATION_QUEUED
-        executor.executeDelayed({
-            runChipAnimation()
-        }, DEBOUNCE_DELAY)
-    }
-
-    /**
-     * 1. Define a total budget for the chip animation (1500ms)
-     * 2. Send out callbacks to listeners so that they can generate animations locally
-     * 3. Update the scheduler state so that clients know where we are
-     * 4. Maybe: provide scaffolding such as: dot location, margins, etc
-     * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
-     * collect all of the animators and run them together.
-     */
-    private fun runChipAnimation() {
-        statusBarWindowController.setForceStatusBarVisible(true)
-        animationState = ANIMATING_IN
-
-        val animSet = collectStartAnimations()
-        if (animSet.totalDuration > 500) {
-            throw IllegalStateException("System animation total length exceeds budget. " +
-                    "Expected: 500, actual: ${animSet.totalDuration}")
-        }
-        animSet.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) {
-                animationState = RUNNING_CHIP_ANIM
-            }
-        })
-        animSet.start()
-
-        executor.executeDelayed({
-            val animSet2 = collectFinishAnimations()
-            animationState = ANIMATING_OUT
-            animSet2.addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
-                    animationState = if (hasPersistentDot) {
-                        SHOWING_PERSISTENT_DOT
-                    } else {
-                        IDLE
-                    }
-
-                    statusBarWindowController.setForceStatusBarVisible(false)
-                }
-            })
-            animSet2.start()
-            scheduledEvent = null
-        }, DISPLAY_LENGTH)
-    }
-
-    private fun collectStartAnimations(): AnimatorSet {
-        val animators = mutableListOf<Animator>()
-        listeners.forEach { listener ->
-            listener.onSystemEventAnimationBegin()?.let { anim ->
-                animators.add(anim)
-            }
-        }
-        animators.add(chipAnimationController.onSystemEventAnimationBegin())
-        val animSet = AnimatorSet().also {
-            it.playTogether(animators)
-        }
-
-        return animSet
-    }
-
-    private fun collectFinishAnimations(): AnimatorSet {
-        val animators = mutableListOf<Animator>()
-        listeners.forEach { listener ->
-            listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
-                animators.add(anim)
-            }
-        }
-        animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
-        if (hasPersistentDot) {
-            val dotAnim = notifyTransitionToPersistentDot()
-            if (dotAnim != null) {
-                animators.add(dotAnim)
-            }
-        }
-        val animSet = AnimatorSet().also {
-            it.playTogether(animators)
-        }
-
-        return animSet
-    }
-
-    private fun notifyTransitionToPersistentDot(): Animator? {
-        val anims: List<Animator> = listeners.mapNotNull {
-            it.onSystemStatusAnimationTransitionToPersistentDot(scheduledEvent?.contentDescription)
-        }
-        if (anims.isNotEmpty()) {
-            val aSet = AnimatorSet()
-            aSet.playTogether(anims)
-            return aSet
-        }
-
-        return null
-    }
-
-    private fun notifyHidePersistentDot(): Animator? {
-        val anims: List<Animator> = listeners.mapNotNull {
-            it.onHidePersistentDot()
-        }
-
-        if (animationState == SHOWING_PERSISTENT_DOT) {
-            animationState = IDLE
-        }
-
-        if (anims.isNotEmpty()) {
-            val aSet = AnimatorSet()
-            aSet.playTogether(anims)
-            return aSet
-        }
-
-        return null
-    }
-
-    override fun addCallback(listener: SystemStatusAnimationCallback) {
-        Assert.isMainThread()
-
-        if (listeners.isEmpty()) {
-            coordinator.startObserving()
-        }
-        listeners.add(listener)
-    }
-
-    override fun removeCallback(listener: SystemStatusAnimationCallback) {
-        Assert.isMainThread()
-
-        listeners.remove(listener)
-        if (listeners.isEmpty()) {
-            coordinator.stopObserving()
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.println("Scheduled event: $scheduledEvent")
-        pw.println("Has persistent privacy dot: $hasPersistentDot")
-        pw.println("Animation state: $animationState")
-        pw.println("Listeners:")
-        if (listeners.isEmpty()) {
-            pw.println("(none)")
-        } else {
-            listeners.forEach {
-                pw.println("  $it")
-            }
-        }
-    }
+    fun removePersistentDot()
 }
 
 /**
@@ -333,6 +56,7 @@
     @JvmDefault fun onHidePersistentDot(): Animator? { return null }
 }
 
+
 /**
  * Animation state IntDef
  */
@@ -350,7 +74,7 @@
 annotation class SystemAnimationState
 
 /** No animation is in progress */
-const val IDLE = 0
+@SystemAnimationState const val IDLE = 0
 /** An animation is queued, and awaiting the debounce period */
 const val ANIMATION_QUEUED = 1
 /** System is animating out, and chip is animating in */
@@ -375,20 +99,16 @@
 val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 = PathInterpolator(0.3f, 0f, 0f, 1f)
 val STATUS_CHIP_MOVE_TO_DOT = PathInterpolator(0f, 0f, 0.05f, 1f)
 
-private const val TAG = "SystemStatusAnimationScheduler"
-private const val DEBOUNCE_DELAY = 100L
+internal const val DEBOUNCE_DELAY = 100L
 
 /**
  * The total time spent on the chip animation is 1500ms, broken up into 3 sections:
- *   - 500ms to animate the chip in (including animating system icons away)
- *   - 500ms holding the chip on screen
- *   - 500ms to animate the chip away (and system icons back)
- *
- *   So DISPLAY_LENGTH should be the sum of the first 2 phases, while the final 500ms accounts for
- *   the actual animation
+ * - 500ms to animate the chip in (including animating system icons away)
+ * - 500ms holding the chip on screen
+ * - 500ms to animate the chip away (and system icons back)
  */
-private const val DISPLAY_LENGTH = 1000L
+internal const val APPEAR_ANIMATION_DURATION = 500L
+internal const val DISPLAY_LENGTH = 3000L
+internal const val DISAPPEAR_ANIMATION_DURATION = 500L
 
-private const val MIN_UPTIME: Long = 5 * 1000
-
-private const val DEBUG = false
+internal const val MIN_UPTIME: Long = 5 * 1000
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
new file mode 100644
index 0000000..f7a4fea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
@@ -0,0 +1,425 @@
+/*
+ * 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.events
+
+import android.os.Process
+import android.provider.DeviceConfig
+import android.util.Log
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.AnimatorSet
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.Assert
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Scheduler for system status events. Obeys the following principles:
+ * ```
+ *      - Waits 100 ms to schedule any event for debouncing/prioritization
+ *      - Simple prioritization: Privacy > Battery > Connectivity (encoded in [StatusEvent])
+ *      - Only schedules a single event, and throws away lowest priority events
+ * ```
+ *
+ * There are 4 basic stages of animation at play here:
+ * ```
+ *      1. System chrome animation OUT
+ *      2. Chip animation IN
+ *      3. Chip animation OUT; potentially into a dot
+ *      4. System chrome animation IN
+ * ```
+ *
+ * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
+ * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
+ * their respective views based on the progress of the animator.
+ */
+@OptIn(FlowPreview::class)
+open class SystemStatusAnimationSchedulerImpl
+@Inject
+constructor(
+    private val coordinator: SystemEventCoordinator,
+    private val chipAnimationController: SystemEventChipAnimationController,
+    private val statusBarWindowController: StatusBarWindowController,
+    dumpManager: DumpManager,
+    private val systemClock: SystemClock,
+    @Application private val coroutineScope: CoroutineScope
+) : SystemStatusAnimationScheduler {
+
+    companion object {
+        private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
+    }
+
+    /** Contains the StatusEvent that is going to be displayed next. */
+    private var scheduledEvent = MutableStateFlow<StatusEvent?>(null)
+
+    /**
+     * The currently displayed status event. (This is null in all states except ANIMATING_IN and
+     * CHIP_ANIMATION_RUNNING)
+     */
+    private var currentlyDisplayedEvent: StatusEvent? = null
+
+    /** StateFlow holding the current [SystemAnimationState] at any time. */
+    private var animationState = MutableStateFlow(IDLE)
+
+    /** True if the persistent privacy dot should be active */
+    var hasPersistentDot = false
+        protected set
+
+    /** Set of currently registered listeners */
+    protected val listeners = mutableSetOf<SystemStatusAnimationCallback>()
+
+    /** The job that is controlling the animators of the currently displayed status event. */
+    private var currentlyRunningAnimationJob: Job? = null
+
+    /** The job that is controlling the animators when an event is cancelled. */
+    private var eventCancellationJob: Job? = null
+
+    init {
+        coordinator.attachScheduler(this)
+        dumpManager.registerCriticalDumpable(TAG, this)
+
+        coroutineScope.launch {
+            // Wait for animationState to become ANIMATION_QUEUED and scheduledEvent to be non null.
+            // Once this combination is stable for at least DEBOUNCE_DELAY, then start a chip enter
+            // animation
+            animationState
+                .combine(scheduledEvent) { animationState, scheduledEvent ->
+                    Pair(animationState, scheduledEvent)
+                }
+                .debounce(DEBOUNCE_DELAY)
+                .collect { (animationState, event) ->
+                    if (animationState == ANIMATION_QUEUED && event != null) {
+                        startAnimationLifecycle(event)
+                        scheduledEvent.value = null
+                    }
+                }
+        }
+    }
+
+    @SystemAnimationState override fun getAnimationState(): Int = animationState.value
+
+    override fun onStatusEvent(event: StatusEvent) {
+        Assert.isMainThread()
+
+        // Ignore any updates until the system is up and running
+        if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
+            return
+        }
+
+        if (
+            (event.priority > (scheduledEvent.value?.priority ?: -1)) &&
+                (event.priority > (currentlyDisplayedEvent?.priority ?: -1)) &&
+                !hasPersistentDot
+        ) {
+            // a event can only be scheduled if no other event is in progress or it has a higher
+            // priority. If a persistent dot is currently displayed, don't schedule the event.
+            if (DEBUG) {
+                Log.d(TAG, "scheduling event $event")
+            }
+
+            scheduleEvent(event)
+        } else if (currentlyDisplayedEvent?.shouldUpdateFromEvent(event) == true) {
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "updating current event from: $event. animationState=${animationState.value}"
+                )
+            }
+            currentlyDisplayedEvent?.updateFromEvent(event)
+        } else if (scheduledEvent.value?.shouldUpdateFromEvent(event) == true) {
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "updating scheduled event from: $event. animationState=${animationState.value}"
+                )
+            }
+            scheduledEvent.value?.updateFromEvent(event)
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "ignoring event $event")
+            }
+        }
+    }
+
+    override fun removePersistentDot() {
+        Assert.isMainThread()
+
+        // If there is an event scheduled currently, set its forceVisible flag to false, such that
+        // it will never transform into a persistent dot
+        scheduledEvent.value?.forceVisible = false
+
+        // Nothing else to do if hasPersistentDot is already false
+        if (!hasPersistentDot) return
+        // Set hasPersistentDot to false. If the animationState is anything before ANIMATING_OUT,
+        // the disappear animation will not animate into a dot but remove the chip entirely
+        hasPersistentDot = false
+        // if we are currently showing a persistent dot, hide it
+        if (animationState.value == SHOWING_PERSISTENT_DOT) notifyHidePersistentDot()
+        // if we are currently animating into a dot, wait for the animation to finish and then hide
+        // the dot
+        if (animationState.value == ANIMATING_OUT) {
+            coroutineScope.launch {
+                withTimeout(DISAPPEAR_ANIMATION_DURATION) {
+                    animationState.first { it == SHOWING_PERSISTENT_DOT || it == ANIMATION_QUEUED }
+                    notifyHidePersistentDot()
+                }
+            }
+        }
+    }
+
+    protected fun isTooEarly(): Boolean {
+        return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
+    }
+
+    protected fun isImmersiveIndicatorEnabled(): Boolean {
+        return DeviceConfig.getBoolean(
+            DeviceConfig.NAMESPACE_PRIVACY,
+            PROPERTY_ENABLE_IMMERSIVE_INDICATOR,
+            true
+        )
+    }
+
+    /** Clear the scheduled event (if any) and schedule a new one */
+    private fun scheduleEvent(event: StatusEvent) {
+        scheduledEvent.value = event
+        if (currentlyDisplayedEvent != null && eventCancellationJob?.isActive != true) {
+            // cancel the currently displayed event. As soon as the event is animated out, the
+            // scheduled event will be displayed.
+            cancelCurrentlyDisplayedEvent()
+            return
+        }
+        if (animationState.value == IDLE) {
+            // If we are in IDLE state, set it to ANIMATION_QUEUED now
+            animationState.value = ANIMATION_QUEUED
+        }
+    }
+
+    /**
+     * Cancels the currently displayed event by animating it out. This function should only be
+     * called if the animationState is ANIMATING_IN or RUNNING_CHIP_ANIM, or in other words whenever
+     * currentlyRunningEvent is not null
+     */
+    private fun cancelCurrentlyDisplayedEvent() {
+        eventCancellationJob =
+            coroutineScope.launch {
+                withTimeout(APPEAR_ANIMATION_DURATION) {
+                    // wait for animationState to become RUNNING_CHIP_ANIM, then cancel the running
+                    // animation job and run the disappear animation immediately
+                    animationState.first { it == RUNNING_CHIP_ANIM }
+                    currentlyRunningAnimationJob?.cancel()
+                    runChipDisappearAnimation()
+                }
+            }
+    }
+
+    /**
+     * Takes the currently scheduled Event and (using the coroutineScope) animates it in and out
+     * again after displaying it for DISPLAY_LENGTH ms. This function should only be called if there
+     * is an event scheduled (and currentlyDisplayedEvent is null)
+     */
+    private fun startAnimationLifecycle(event: StatusEvent) {
+        Assert.isMainThread()
+        hasPersistentDot = event.forceVisible
+
+        if (!event.showAnimation && event.forceVisible) {
+            // If animations are turned off, we'll transition directly to the dot
+            animationState.value = SHOWING_PERSISTENT_DOT
+            notifyTransitionToPersistentDot()
+            return
+        }
+
+        currentlyDisplayedEvent = event
+
+        chipAnimationController.prepareChipAnimation(event.viewCreator)
+        currentlyRunningAnimationJob =
+            coroutineScope.launch {
+                runChipAppearAnimation()
+                delay(APPEAR_ANIMATION_DURATION + DISPLAY_LENGTH)
+                runChipDisappearAnimation()
+            }
+    }
+
+    /**
+     * 1. Define a total budget for the chip animation (1500ms)
+     * 2. Send out callbacks to listeners so that they can generate animations locally
+     * 3. Update the scheduler state so that clients know where we are
+     * 4. Maybe: provide scaffolding such as: dot location, margins, etc
+     * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
+     *    collect all of the animators and run them together.
+     */
+    private fun runChipAppearAnimation() {
+        Assert.isMainThread()
+        if (hasPersistentDot) {
+            statusBarWindowController.setForceStatusBarVisible(true)
+        }
+        animationState.value = ANIMATING_IN
+
+        val animSet = collectStartAnimations()
+        if (animSet.totalDuration > 500) {
+            throw IllegalStateException(
+                "System animation total length exceeds budget. " +
+                    "Expected: 500, actual: ${animSet.totalDuration}"
+            )
+        }
+        animSet.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    animationState.value = RUNNING_CHIP_ANIM
+                }
+            }
+        )
+        animSet.start()
+    }
+
+    private fun runChipDisappearAnimation() {
+        Assert.isMainThread()
+        val animSet2 = collectFinishAnimations()
+        animationState.value = ANIMATING_OUT
+        animSet2.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    animationState.value =
+                        when {
+                            hasPersistentDot -> SHOWING_PERSISTENT_DOT
+                            scheduledEvent.value != null -> ANIMATION_QUEUED
+                            else -> IDLE
+                        }
+                    statusBarWindowController.setForceStatusBarVisible(false)
+                }
+            }
+        )
+        animSet2.start()
+
+        // currentlyDisplayedEvent is set to null before the animation has ended such that new
+        // events can be scheduled during the disappear animation. We don't want to miss e.g. a new
+        // privacy event being scheduled during the disappear animation, otherwise we could end up
+        // with e.g. an active microphone but no privacy dot being displayed.
+        currentlyDisplayedEvent = null
+    }
+
+    private fun collectStartAnimations(): AnimatorSet {
+        val animators = mutableListOf<Animator>()
+        listeners.forEach { listener ->
+            listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) }
+        }
+        animators.add(chipAnimationController.onSystemEventAnimationBegin())
+
+        return AnimatorSet().also { it.playTogether(animators) }
+    }
+
+    private fun collectFinishAnimations(): AnimatorSet {
+        val animators = mutableListOf<Animator>()
+        listeners.forEach { listener ->
+            listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
+                animators.add(anim)
+            }
+        }
+        animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
+        if (hasPersistentDot) {
+            val dotAnim = notifyTransitionToPersistentDot()
+            if (dotAnim != null) {
+                animators.add(dotAnim)
+            }
+        }
+
+        return AnimatorSet().also { it.playTogether(animators) }
+    }
+
+    private fun notifyTransitionToPersistentDot(): Animator? {
+        val anims: List<Animator> =
+            listeners.mapNotNull {
+                it.onSystemStatusAnimationTransitionToPersistentDot(
+                    currentlyDisplayedEvent?.contentDescription
+                )
+            }
+        if (anims.isNotEmpty()) {
+            val aSet = AnimatorSet()
+            aSet.playTogether(anims)
+            return aSet
+        }
+
+        return null
+    }
+
+    private fun notifyHidePersistentDot(): Animator? {
+        Assert.isMainThread()
+        val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() }
+
+        if (animationState.value == SHOWING_PERSISTENT_DOT) {
+            if (scheduledEvent.value != null) {
+                animationState.value = ANIMATION_QUEUED
+            } else {
+                animationState.value = IDLE
+            }
+        }
+
+        if (anims.isNotEmpty()) {
+            val aSet = AnimatorSet()
+            aSet.playTogether(anims)
+            return aSet
+        }
+
+        return null
+    }
+
+    override fun addCallback(listener: SystemStatusAnimationCallback) {
+        Assert.isMainThread()
+
+        if (listeners.isEmpty()) {
+            coordinator.startObserving()
+        }
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: SystemStatusAnimationCallback) {
+        Assert.isMainThread()
+
+        listeners.remove(listener)
+        if (listeners.isEmpty()) {
+            coordinator.stopObserving()
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("Scheduled event: ${scheduledEvent.value}")
+        pw.println("Currently displayed event: $currentlyDisplayedEvent")
+        pw.println("Has persistent privacy dot: $hasPersistentDot")
+        pw.println("Animation state: ${animationState.value}")
+        pw.println("Listeners:")
+        if (listeners.isEmpty()) {
+            pw.println("(none)")
+        } else {
+            listeners.forEach { pw.println("  $it") }
+        }
+    }
+}
+
+private const val DEBUG = false
+private const val TAG = "SystemStatusAnimationSchedulerImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLegacyImpl.kt
new file mode 100644
index 0000000..64b7ac9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLegacyImpl.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.events
+
+import android.os.Process
+import android.provider.DeviceConfig
+import android.util.Log
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.AnimatorSet
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.Assert
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD):
+ * ```
+ *      - Avoiding log spam by only allowing 12 events per minute (1event/5s)
+ *      - Waits 100ms to schedule any event for debouncing/prioritization
+ *      - Simple prioritization: Privacy > Battery > connectivity (encoded in [StatusEvent])
+ *      - Only schedules a single event, and throws away lowest priority events
+ * ```
+ * There are 4 basic stages of animation at play here:
+ * ```
+ *      1. System chrome animation OUT
+ *      2. Chip animation IN
+ *      3. Chip animation OUT; potentially into a dot
+ *      4. System chrome animation IN
+ * ```
+ * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
+ * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
+ * their respective views based on the progress of the animator. Interpolation differences TBD
+ */
+open class SystemStatusAnimationSchedulerLegacyImpl
+@Inject
+constructor(
+    private val coordinator: SystemEventCoordinator,
+    private val chipAnimationController: SystemEventChipAnimationController,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val dumpManager: DumpManager,
+    private val systemClock: SystemClock,
+    @Main private val executor: DelayableExecutor
+) : SystemStatusAnimationScheduler {
+
+    companion object {
+        private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
+    }
+
+    fun isImmersiveIndicatorEnabled(): Boolean {
+        return DeviceConfig.getBoolean(
+            DeviceConfig.NAMESPACE_PRIVACY,
+            PROPERTY_ENABLE_IMMERSIVE_INDICATOR,
+            true
+        )
+    }
+
+    @SystemAnimationState private var animationState: Int = IDLE
+
+    /** True if the persistent privacy dot should be active */
+    var hasPersistentDot = false
+        protected set
+
+    private var scheduledEvent: StatusEvent? = null
+
+    val listeners = mutableSetOf<SystemStatusAnimationCallback>()
+
+    init {
+        coordinator.attachScheduler(this)
+        dumpManager.registerDumpable(TAG, this)
+    }
+
+    @SystemAnimationState override fun getAnimationState() = animationState
+
+    override fun onStatusEvent(event: StatusEvent) {
+        // Ignore any updates until the system is up and running
+        if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
+            return
+        }
+
+        // Don't deal with threading for now (no need let's be honest)
+        Assert.isMainThread()
+        if (
+            (event.priority > (scheduledEvent?.priority ?: -1)) &&
+                animationState != ANIMATING_OUT &&
+                animationState != SHOWING_PERSISTENT_DOT
+        ) {
+            // events can only be scheduled if a higher priority or no other event is in progress
+            if (DEBUG) {
+                Log.d(TAG, "scheduling event $event")
+            }
+
+            scheduleEvent(event)
+        } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
+            if (DEBUG) {
+                Log.d(TAG, "updating current event from: $event. animationState=$animationState")
+            }
+            scheduledEvent?.updateFromEvent(event)
+            if (event.forceVisible) {
+                hasPersistentDot = true
+                // If we missed the chance to show the persistent dot, do it now
+                if (animationState == IDLE) {
+                    notifyTransitionToPersistentDot()
+                }
+            }
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "ignoring event $event")
+            }
+        }
+    }
+
+    override fun removePersistentDot() {
+        if (!hasPersistentDot || !isImmersiveIndicatorEnabled()) {
+            return
+        }
+
+        hasPersistentDot = false
+        notifyHidePersistentDot()
+        return
+    }
+
+    fun isTooEarly(): Boolean {
+        return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
+    }
+
+    /** Clear the scheduled event (if any) and schedule a new one */
+    private fun scheduleEvent(event: StatusEvent) {
+        scheduledEvent = event
+
+        if (event.forceVisible) {
+            hasPersistentDot = true
+        }
+
+        // If animations are turned off, we'll transition directly to the dot
+        if (!event.showAnimation && event.forceVisible) {
+            notifyTransitionToPersistentDot()
+            scheduledEvent = null
+            return
+        }
+
+        chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator)
+        animationState = ANIMATION_QUEUED
+        executor.executeDelayed({ runChipAnimation() }, DEBOUNCE_DELAY)
+    }
+
+    /**
+     * 1. Define a total budget for the chip animation (1500ms)
+     * 2. Send out callbacks to listeners so that they can generate animations locally
+     * 3. Update the scheduler state so that clients know where we are
+     * 4. Maybe: provide scaffolding such as: dot location, margins, etc
+     * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
+     * collect all of the animators and run them together.
+     */
+    private fun runChipAnimation() {
+        statusBarWindowController.setForceStatusBarVisible(true)
+        animationState = ANIMATING_IN
+
+        val animSet = collectStartAnimations()
+        if (animSet.totalDuration > 500) {
+            throw IllegalStateException(
+                "System animation total length exceeds budget. " +
+                    "Expected: 500, actual: ${animSet.totalDuration}"
+            )
+        }
+        animSet.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    animationState = RUNNING_CHIP_ANIM
+                }
+            }
+        )
+        animSet.start()
+
+        executor.executeDelayed(
+            {
+                val animSet2 = collectFinishAnimations()
+                animationState = ANIMATING_OUT
+                animSet2.addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            animationState =
+                                if (hasPersistentDot) {
+                                    SHOWING_PERSISTENT_DOT
+                                } else {
+                                    IDLE
+                                }
+
+                            statusBarWindowController.setForceStatusBarVisible(false)
+                        }
+                    }
+                )
+                animSet2.start()
+                scheduledEvent = null
+            },
+            DISPLAY_LENGTH
+        )
+    }
+
+    private fun collectStartAnimations(): AnimatorSet {
+        val animators = mutableListOf<Animator>()
+        listeners.forEach { listener ->
+            listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) }
+        }
+        animators.add(chipAnimationController.onSystemEventAnimationBegin())
+        val animSet = AnimatorSet().also { it.playTogether(animators) }
+
+        return animSet
+    }
+
+    private fun collectFinishAnimations(): AnimatorSet {
+        val animators = mutableListOf<Animator>()
+        listeners.forEach { listener ->
+            listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
+                animators.add(anim)
+            }
+        }
+        animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
+        if (hasPersistentDot) {
+            val dotAnim = notifyTransitionToPersistentDot()
+            if (dotAnim != null) {
+                animators.add(dotAnim)
+            }
+        }
+        val animSet = AnimatorSet().also { it.playTogether(animators) }
+
+        return animSet
+    }
+
+    private fun notifyTransitionToPersistentDot(): Animator? {
+        val anims: List<Animator> =
+            listeners.mapNotNull {
+                it.onSystemStatusAnimationTransitionToPersistentDot(
+                    scheduledEvent?.contentDescription
+                )
+            }
+        if (anims.isNotEmpty()) {
+            val aSet = AnimatorSet()
+            aSet.playTogether(anims)
+            return aSet
+        }
+
+        return null
+    }
+
+    private fun notifyHidePersistentDot(): Animator? {
+        val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() }
+
+        if (animationState == SHOWING_PERSISTENT_DOT) {
+            animationState = IDLE
+        }
+
+        if (anims.isNotEmpty()) {
+            val aSet = AnimatorSet()
+            aSet.playTogether(anims)
+            return aSet
+        }
+
+        return null
+    }
+
+    override fun addCallback(listener: SystemStatusAnimationCallback) {
+        Assert.isMainThread()
+
+        if (listeners.isEmpty()) {
+            coordinator.startObserving()
+        }
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: SystemStatusAnimationCallback) {
+        Assert.isMainThread()
+
+        listeners.remove(listener)
+        if (listeners.isEmpty()) {
+            coordinator.stopObserving()
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("Scheduled event: $scheduledEvent")
+        pw.println("Has persistent privacy dot: $hasPersistentDot")
+        pw.println("Animation state: $animationState")
+        pw.println("Listeners:")
+        if (listeners.isEmpty()) {
+            pw.println("(none)")
+        } else {
+            listeners.forEach { pw.println("  $it") }
+        }
+    }
+}
+
+private const val DEBUG = false
+private const val TAG = "SystemStatusAnimationSchedulerLegacyImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
index 3a4731a..92a8356 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
@@ -20,9 +20,9 @@
 import android.annotation.CallSuper
 import android.os.Looper
 import android.view.Choreographer
-import android.view.Display
 import android.view.InputEvent
 import android.view.MotionEvent
+import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.shared.system.InputChannelCompat
 import com.android.systemui.shared.system.InputMonitorCompat
 
@@ -38,7 +38,8 @@
  * gesture is detected, they should call [onGestureDetected] (which will notify the callbacks).
  */
 abstract class GenericGestureDetector(
-    private val tag: String
+    private val tag: String,
+    private val displayTracker: DisplayTracker
 ) {
     /**
      * Active callbacks, each associated with a tag. Gestures will only be monitored if
@@ -86,7 +87,7 @@
     internal open fun startGestureListening() {
         stopGestureListening()
 
-        inputMonitor = InputMonitorCompat(tag, Display.DEFAULT_DISPLAY).also {
+        inputMonitor = InputMonitorCompat(tag, displayTracker.defaultDisplayId).also {
             inputReceiver = it.getInputReceiver(
                 Looper.getMainLooper(),
                 Choreographer.getInstance(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
index 6115819..693ae66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
@@ -17,90 +17,27 @@
 package com.android.systemui.statusbar.gesture
 
 import android.content.Context
-import android.view.InputEvent
 import android.view.MotionEvent
-import android.view.MotionEvent.ACTION_CANCEL
-import android.view.MotionEvent.ACTION_DOWN
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_UP
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import javax.inject.Inject
 
-/**
- * A class to detect when a user swipes away the status bar. To be notified when the swipe away
- * gesture is detected, add a callback via [addOnGestureDetectedCallback].
- */
+/** A class to detect when a user swipes away the status bar. */
 @SysUISingleton
-open class SwipeStatusBarAwayGestureHandler @Inject constructor(
+class SwipeStatusBarAwayGestureHandler
+@Inject
+constructor(
     context: Context,
+    displayTracker: DisplayTracker,
+    logger: SwipeUpGestureLogger,
     private val statusBarWindowController: StatusBarWindowController,
-    private val logger: SwipeStatusBarAwayGestureLogger
-) : GenericGestureDetector(SwipeStatusBarAwayGestureHandler::class.simpleName!!) {
-
-    private var startY: Float = 0f
-    private var startTime: Long = 0L
-    private var monitoringCurrentTouch: Boolean = false
-
-    private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
-        com.android.internal.R.dimen.system_gestures_start_threshold
-    )
-
-    override fun onInputEvent(ev: InputEvent) {
-        if (ev !is MotionEvent) {
-            return
-        }
-
-        when (ev.actionMasked) {
-            ACTION_DOWN -> {
-                if (
-                    // Gesture starts just below the status bar
-                    ev.y >= statusBarWindowController.statusBarHeight
-                    && ev.y <= 3 * statusBarWindowController.statusBarHeight
-                ) {
-                    logger.logGestureDetectionStarted(ev.y.toInt())
-                    startY = ev.y
-                    startTime = ev.eventTime
-                    monitoringCurrentTouch = true
-                } else {
-                    monitoringCurrentTouch = false
-                }
-            }
-            ACTION_MOVE -> {
-                if (!monitoringCurrentTouch) {
-                    return
-                }
-                if (
-                    // Gesture is up
-                    ev.y < startY
-                    // Gesture went far enough
-                    && (startY - ev.y) >= swipeDistanceThreshold
-                    // Gesture completed quickly enough
-                    && (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
-                ) {
-                    monitoringCurrentTouch = false
-                    logger.logGestureDetected(ev.y.toInt())
-                    onGestureDetected(ev)
-                }
-            }
-            ACTION_CANCEL, ACTION_UP -> {
-                if (monitoringCurrentTouch) {
-                    logger.logGestureDetectionEndedWithoutTriggering(ev.y.toInt())
-                }
-                monitoringCurrentTouch = false
-            }
-        }
-    }
-
-    override fun startGestureListening() {
-        super.startGestureListening()
-        logger.logInputListeningStarted()
-    }
-
-    override fun stopGestureListening() {
-        super.stopGestureListening()
-        logger.logInputListeningStopped()
+) : SwipeUpGestureHandler(context, displayTracker, logger, loggerTag = LOGGER_TAG) {
+    override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
+        // Gesture starts just below the status bar
+        return ev.y >= statusBarWindowController.statusBarHeight &&
+            ev.y <= 3 * statusBarWindowController.statusBarHeight
     }
 }
 
-private const val SWIPE_TIMEOUT_MS: Long = 500
+private const val LOGGER_TAG = "SwipeStatusBarAway"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
new file mode 100644
index 0000000..452762d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
+
+/**
+ * A class to detect a generic "swipe up" gesture. To be notified when the swipe up gesture is
+ * detected, add a callback via [addOnGestureDetectedCallback].
+ */
+@SysUISingleton
+abstract class SwipeUpGestureHandler(
+    context: Context,
+    displayTracker: DisplayTracker,
+    private val logger: SwipeUpGestureLogger,
+    private val loggerTag: String
+) : GenericGestureDetector(SwipeUpGestureHandler::class.simpleName!!, displayTracker) {
+
+    private var startY: Float = 0f
+    private var startTime: Long = 0L
+    private var monitoringCurrentTouch: Boolean = false
+
+    private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
+        com.android.internal.R.dimen.system_gestures_start_threshold
+    )
+
+    override fun onInputEvent(ev: InputEvent) {
+        if (ev !is MotionEvent) {
+            return
+        }
+
+        when (ev.actionMasked) {
+            ACTION_DOWN -> {
+                if (
+                    startOfGestureIsWithinBounds(ev)
+                ) {
+                    logger.logGestureDetectionStarted(loggerTag, ev.y.toInt())
+                    startY = ev.y
+                    startTime = ev.eventTime
+                    monitoringCurrentTouch = true
+                } else {
+                    monitoringCurrentTouch = false
+                }
+            }
+            ACTION_MOVE -> {
+                if (!monitoringCurrentTouch) {
+                    return
+                }
+                if (
+                    // Gesture is up
+                    ev.y < startY &&
+                    // Gesture went far enough
+                    (startY - ev.y) >= swipeDistanceThreshold &&
+                    // Gesture completed quickly enough
+                    (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
+                ) {
+                    monitoringCurrentTouch = false
+                    logger.logGestureDetected(loggerTag, ev.y.toInt())
+                    onGestureDetected(ev)
+                }
+            }
+            ACTION_CANCEL, ACTION_UP -> {
+                if (monitoringCurrentTouch) {
+                    logger.logGestureDetectionEndedWithoutTriggering(loggerTag, ev.y.toInt())
+                }
+                monitoringCurrentTouch = false
+            }
+        }
+    }
+
+    /**
+     * Returns true if the [ACTION_DOWN] event falls within bounds for this specific swipe-up
+     * gesture.
+     *
+     * Implementations must override this method to specify what part(s) of the screen are valid
+     * locations for the swipe up gesture to start at.
+     */
+    abstract fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean
+
+    override fun startGestureListening() {
+        super.startGestureListening()
+        logger.logInputListeningStarted(loggerTag)
+    }
+
+    override fun stopGestureListening() {
+        super.stopGestureListening()
+        logger.logInputListeningStopped(loggerTag)
+    }
+}
+
+private const val SWIPE_TIMEOUT_MS: Long = 500
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
index 9bdff92..9ce6b02 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
@@ -16,49 +16,49 @@
 
 package com.android.systemui.statusbar.gesture
 
-import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.SwipeUpLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
-/** Log messages for [SwipeStatusBarAwayGestureHandler]. */
-class SwipeStatusBarAwayGestureLogger @Inject constructor(
-    @SwipeStatusBarAwayLog private val buffer: LogBuffer
+/** Log messages for [SwipeUpGestureHandler]. */
+@SysUISingleton
+class SwipeUpGestureLogger @Inject constructor(
+    @SwipeUpLog private val buffer: LogBuffer,
 ) {
-    fun logGestureDetectionStarted(y: Int) {
+    fun logGestureDetectionStarted(tag: String, y: Int) {
         buffer.log(
-            TAG,
+            tag,
             LogLevel.DEBUG,
             { int1 = y },
             { "Beginning gesture detection. y=$int1" }
         )
     }
 
-    fun logGestureDetectionEndedWithoutTriggering(y: Int) {
+    fun logGestureDetectionEndedWithoutTriggering(tag: String, y: Int) {
         buffer.log(
-            TAG,
+            tag,
             LogLevel.DEBUG,
             { int1 = y },
             { "Gesture finished; no swipe up gesture detected. Final y=$int1" }
         )
     }
 
-    fun logGestureDetected(y: Int) {
+    fun logGestureDetected(tag: String, y: Int) {
         buffer.log(
-            TAG,
+            tag,
             LogLevel.INFO,
             { int1 = y },
             { "Gesture detected; notifying callbacks. y=$int1" }
         )
     }
 
-    fun logInputListeningStarted() {
-        buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening started "})
+    fun logInputListeningStarted(tag: String) {
+        buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening started "})
     }
 
-    fun logInputListeningStopped() {
-        buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening stopped "})
+    fun logInputListeningStopped(tag: String) {
+        buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening stopped "})
     }
 }
-
-private const val TAG = "SwipeStatusBarAwayGestureHandler"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
index 7ffb07a..a901d597 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
@@ -21,6 +21,7 @@
 import android.view.InputEvent
 import android.view.MotionEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
 import javax.inject.Inject
 
 /**
@@ -29,8 +30,9 @@
  */
 @SysUISingleton
 class TapGestureDetector @Inject constructor(
-    private val context: Context
-) : GenericGestureDetector(TapGestureDetector::class.simpleName!!) {
+    private val context: Context,
+    displayTracker: DisplayTracker
+) : GenericGestureDetector(TapGestureDetector::class.simpleName!!, displayTracker) {
 
     private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
         override fun onSingleTapUp(e: MotionEvent): Boolean {
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 d6ad7d0..29510d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -31,45 +31,58 @@
 import android.os.UserHandle
 import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
 import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS
+import android.provider.Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED
 import android.util.Log
 import android.view.ContextThemeWrapper
 import android.view.View
 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
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.WeatherData
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.regionsampling.RegionSampler
 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.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 com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import java.time.Instant
 import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import javax.inject.Named
 
-/**
- * Controller for managing the smartspace view on the lockscreen
- */
+/** Controller for managing the smartspace view on the lockscreen */
 @SysUISingleton
-class LockscreenSmartspaceController @Inject constructor(
+class LockscreenSmartspaceController
+@Inject
+constructor(
         private val context: Context,
         private val featureFlags: FeatureFlags,
         private val smartspaceManager: SmartspaceManager,
         private val activityStarter: ActivityStarter,
         private val falsingManager: FalsingManager,
+        private val systemClock: SystemClock,
         private val secureSettings: SecureSettings,
         private val userTracker: UserTracker,
         private val contentResolver: ContentResolver,
@@ -77,18 +90,28 @@
         private val statusBarStateController: StatusBarStateController,
         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,
         @Main private val handler: Handler,
-        optionalPlugin: Optional<BcSmartspaceDataPlugin>
-) {
+        @Named(DATE_SMARTSPACE_DATA_PLUGIN)
+        optionalDatePlugin: Optional<BcSmartspaceDataPlugin>,
+        @Named(WEATHER_SMARTSPACE_DATA_PLUGIN)
+        optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
+        optionalPlugin: Optional<BcSmartspaceDataPlugin>,
+        optionalConfigPlugin: Optional<BcSmartspaceConfigPlugin>,
+) : Dumpable {
     companion object {
         private const val TAG = "LockscreenSmartspaceController"
     }
 
     private var session: SmartspaceSession? = null
+    private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
+    private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
     private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
+    private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null)
 
     // Smartspace can be used on multiple displays, such as when the user casts their screen
     private var smartspaceViews = mutableSetOf<SmartspaceView>()
@@ -97,7 +120,7 @@
 
     private val regionSamplingEnabled =
             featureFlags.isEnabled(Flags.REGION_SAMPLING)
-
+    private var isContentUpdatedOnce = false
     private var showNotifications = false
     private var showSensitiveContentForCurrentUser = false
     private var showSensitiveContentForManagedUser = false
@@ -111,19 +134,6 @@
         override fun onViewAttachedToWindow(v: View) {
             smartspaceViews.add(v as SmartspaceView)
 
-            if (regionSamplingEnabled) {
-                var regionSampler = RegionSampler(
-                        v,
-                        uiExecutor,
-                        bgExecutor,
-                        regionSamplingEnabled,
-                        updateFun
-                )
-                initializeTextColors(regionSampler)
-                regionSampler.startRegionSampler()
-                regionSamplers.put(v, regionSampler)
-            }
-
             connectSession()
 
             updateTextColorFromWallpaper()
@@ -133,12 +143,6 @@
         override fun onViewDetachedFromWindow(v: View) {
             smartspaceViews.remove(v as SmartspaceView)
 
-            if (regionSamplingEnabled) {
-                var regionSampler = regionSamplers.getValue(v)
-                regionSampler.stopRegionSampler()
-                regionSamplers.remove(v)
-            }
-
             if (smartspaceViews.isEmpty()) {
                 disconnect()
             }
@@ -147,8 +151,42 @@
 
     private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
         execution.assertIsMainThread()
+
+        // The weather data plugin takes unfiltered targets and performs the filtering internally.
+        weatherPlugin?.onTargetsAvailable(targets)
+        val now = Instant.ofEpochMilli(systemClock.currentTimeMillis())
+        val weatherTarget = targets.find { t ->
+            t.featureType == SmartspaceTarget.FEATURE_WEATHER &&
+                    now.isAfter(Instant.ofEpochMilli(t.creationTimeMillis)) &&
+                    now.isBefore(Instant.ofEpochMilli(t.expiryTimeMillis))
+        }
+        if (weatherTarget != null) {
+            val weatherData = WeatherData.fromBundle(weatherTarget.baseAction.extras)
+            if (weatherData != null) {
+                keyguardUpdateMonitor.sendWeatherData(weatherData)
+            }
+        }
+
         val filteredTargets = targets.filter(::filterSmartspaceTarget)
         plugin?.onTargetsAvailable(filteredTargets)
+        if (!isContentUpdatedOnce) {
+            for (v in smartspaceViews) {
+                if (regionSamplingEnabled) {
+                    var regionSampler = RegionSampler(
+                        v as View,
+                        uiExecutor,
+                        bgExecutor,
+                        regionSamplingEnabled,
+                        updateFun
+                    )
+                    initializeTextColors(regionSampler)
+                    regionSamplers[v] = regionSampler
+                    regionSampler.startRegionSampler()
+                }
+                updateTextColorFromWallpaper()
+            }
+            isContentUpdatedOnce = true
+        }
     }
 
     private val userTrackerCallback = object : UserTracker.Callback {
@@ -199,12 +237,31 @@
 
     init {
         deviceProvisionedController.addCallback(deviceProvisionedListener)
+        dumpManager.registerDumpable(this)
     }
 
     fun isEnabled(): Boolean {
         execution.assertIsMainThread()
 
-        return featureFlags.isEnabled(Flags.SMARTSPACE) && plugin != null
+        return plugin != null
+    }
+
+    fun isDateWeatherDecoupled(): Boolean {
+        execution.assertIsMainThread()
+
+        return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) &&
+                datePlugin != null && weatherPlugin != null
+    }
+
+    fun isWeatherEnabled(): Boolean {
+       execution.assertIsMainThread()
+       val defaultValue = context.getResources().getBoolean(
+               com.android.internal.R.bool.config_lockscreenWeatherEnabledByDefault)
+       val showWeather = secureSettings.getIntForUser(
+           LOCK_SCREEN_WEATHER_ENABLED,
+           if (defaultValue) 1 else 0,
+           userTracker.userId) == 1
+       return showWeather
     }
 
     private fun updateBypassEnabled() {
@@ -213,6 +270,44 @@
     }
 
     /**
+     * Constructs the date view and connects it to the smartspace service.
+     */
+    fun buildAndConnectDateView(parent: ViewGroup): View? {
+        execution.assertIsMainThread()
+
+        if (!isEnabled()) {
+            throw RuntimeException("Cannot build view when not enabled")
+        }
+        if (!isDateWeatherDecoupled()) {
+            throw RuntimeException("Cannot build date view when not decoupled")
+        }
+
+        val view = buildView(parent, datePlugin)
+        connectSession()
+
+        return view
+    }
+
+    /**
+     * Constructs the weather view and connects it to the smartspace service.
+     */
+    fun buildAndConnectWeatherView(parent: ViewGroup): View? {
+        execution.assertIsMainThread()
+
+        if (!isEnabled()) {
+            throw RuntimeException("Cannot build view when not enabled")
+        }
+        if (!isDateWeatherDecoupled()) {
+            throw RuntimeException("Cannot build weather view when not decoupled")
+        }
+
+        val view = buildView(parent, weatherPlugin)
+        connectSession()
+
+        return view
+    }
+
+    /**
      * Constructs the smartspace view and connects it to the smartspace service.
      */
     fun buildAndConnectView(parent: ViewGroup): View? {
@@ -222,17 +317,17 @@
             throw RuntimeException("Cannot build view when not enabled")
         }
 
-        val view = buildView(parent)
+        val view = buildView(parent, plugin, configPlugin)
         connectSession()
 
         return view
     }
 
-    fun requestSmartspaceUpdate() {
-        session?.requestSmartspaceUpdate()
-    }
-
-    private fun buildView(parent: ViewGroup): View? {
+    private fun buildView(
+            parent: ViewGroup,
+            plugin: BcSmartspaceDataPlugin?,
+            configPlugin: BcSmartspaceConfigPlugin? = null
+    ): View? {
         if (plugin == null) {
             return null
         }
@@ -240,6 +335,7 @@
         val ssView = plugin.getView(parent)
         ssView.setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
         ssView.registerDataProvider(plugin)
+        configPlugin?.let { ssView.registerConfigProvider(it) }
 
         ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
             override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) {
@@ -270,7 +366,8 @@
     }
 
     private fun connectSession() {
-        if (plugin == null || session != null || smartspaceViews.isEmpty()) {
+        if (datePlugin == null && weatherPlugin == null && plugin == null) return
+        if (session != null || smartspaceViews.isEmpty()) {
             return
         }
 
@@ -307,15 +404,22 @@
         statusBarStateController.addCallback(statusBarStateListener)
         bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
 
-        plugin.registerSmartspaceEventNotifier {
-                e -> session?.notifySmartspaceEvent(e)
-        }
+        datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
+        weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
+        plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
 
         updateBypassEnabled()
         reloadSmartspace()
     }
 
     /**
+     * Requests the smartspace session for an update.
+     */
+    fun requestSmartspaceUpdate() {
+        session?.requestSmartspaceUpdate()
+    }
+
+    /**
      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
      */
     fun disconnect() {
@@ -338,9 +442,15 @@
         bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
         session = null
 
+        datePlugin?.registerSmartspaceEventNotifier(null)
+
+        weatherPlugin?.registerSmartspaceEventNotifier(null)
+        weatherPlugin?.onTargetsAvailable(emptyList())
+
         plugin?.registerSmartspaceEventNotifier(null)
         plugin?.onTargetsAvailable(emptyList())
-        Log.d(TAG, "Ending smartspace session for lockscreen")
+
+        Log.d(TAG, "Ended smartspace session for lockscreen")
     }
 
     fun addListener(listener: SmartspaceTargetListener) {
@@ -354,8 +464,11 @@
     }
 
     private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean {
+        if (isDateWeatherDecoupled()) {
+            return t.featureType != SmartspaceTarget.FEATURE_WEATHER
+        }
         if (!showNotifications) {
-            return t.getFeatureType() == SmartspaceTarget.FEATURE_WEATHER
+            return t.featureType == SmartspaceTarget.FEATURE_WEATHER
         }
         return when (t.userHandle) {
             userTracker.userHandle -> {
@@ -394,7 +507,8 @@
 
     private fun updateTextColorFromWallpaper() {
         val wallpaperManager = WallpaperManager.getInstance(context)
-        if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists()) {
+        if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists() ||
+            regionSamplers.isEmpty()) {
             val wallpaperTextColor =
                     Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)
             smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) }
@@ -437,4 +551,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/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
index 0a5e986..11582d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
@@ -29,7 +29,6 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.app.SynchronousUserSwitchObserver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -52,7 +51,9 @@
 import com.android.systemui.CoreStartable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.NotificationChannels;
@@ -73,6 +74,8 @@
 
     private final Context mContext;
     private final Handler mHandler = new Handler();
+    private final UserTracker mUserTracker;
+    private final Executor mMainExecutor;
     private final Executor mUiBgExecutor;
     private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
     private final CommandQueue mCommandQueue;
@@ -82,10 +85,14 @@
     public InstantAppNotifier(
             Context context,
             CommandQueue commandQueue,
+            UserTracker userTracker,
+            @Main Executor mainExecutor,
             @UiBackground Executor uiBgExecutor,
             KeyguardStateController keyguardStateController) {
         mContext = context;
         mCommandQueue = commandQueue;
+        mUserTracker = userTracker;
+        mMainExecutor = mainExecutor;
         mUiBgExecutor = uiBgExecutor;
         mKeyguardStateController = keyguardStateController;
     }
@@ -93,11 +100,7 @@
     @Override
     public void start() {
         // listen for user / profile change.
-        try {
-            ActivityManager.getService().registerUserSwitchObserver(mUserSwitchListener, TAG);
-        } catch (RemoteException e) {
-            // Ignore
-        }
+        mUserTracker.addCallback(mUserSwitchListener, mMainExecutor);
 
         mCommandQueue.addCallback(this);
         mKeyguardStateController.addCallback(this);
@@ -129,13 +132,10 @@
         updateForegroundInstantApps();
     }
 
-    private final SynchronousUserSwitchObserver mUserSwitchListener =
-            new SynchronousUserSwitchObserver() {
+    private final UserTracker.Callback mUserSwitchListener =
+            new UserTracker.Callback() {
                 @Override
-                public void onUserSwitching(int newUserId) throws RemoteException {}
-
-                @Override
-                public void onUserSwitchComplete(int newUserId) throws RemoteException {
+                public void onUserChanged(int newUser, Context userContext) {
                     mHandler.post(
                             () -> {
                                 updateForegroundInstantApps();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 3072c81..fc89be2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -17,37 +17,46 @@
 package com.android.systemui.statusbar.notification
 
 import android.content.Context
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import javax.inject.Inject
 
 class NotifPipelineFlags @Inject constructor(
     val context: Context,
-    val featureFlags: FeatureFlags
+    val featureFlags: FeatureFlags,
+    val sysPropFlags: FlagResolver,
 ) {
+    init {
+        featureFlags.addListener(Flags.DISABLE_FSI) { event -> event.requestNoRestart() }
+    }
+
     fun isDevLoggingEnabled(): Boolean =
         featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
 
-    fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE)
-
     fun fullScreenIntentRequiresKeyguard(): Boolean =
         featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
 
     fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE)
 
-    val isStabilityIndexFixEnabled: Boolean by lazy {
-        featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
-    }
+    fun disableFsi(): Boolean = featureFlags.isEnabled(Flags.DISABLE_FSI)
 
-    val isSemiStableSortEnabled: Boolean by lazy {
-        featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
-    }
+    fun forceDemoteFsi(): Boolean =
+            sysPropFlags.isEnabled(NotificationFlags.FSI_FORCE_DEMOTE)
 
-    val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy {
-        featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD)
-    }
+    fun showStickyHunForDeniedFsi(): Boolean =
+            sysPropFlags.isEnabled(NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI)
 
-    val isNoHunForOldWhenEnabled: Boolean by lazy {
-        featureFlags.isEnabled(Flags.NO_HUN_FOR_OLD_WHEN)
-    }
+    fun allowDismissOngoing(): Boolean =
+            sysPropFlags.isEnabled(NotificationFlags.ALLOW_DISMISS_ONGOING)
+
+    fun isOtpRedactionEnabled(): Boolean =
+            sysPropFlags.isEnabled(NotificationFlags.OTP_REDACTION)
+
+    val shouldFilterUnseenNotifsOnKeyguard: Boolean
+        get() = featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD)
+
+    val isNoHunForOldWhenEnabled: Boolean
+        get() = featureFlags.isEnabled(Flags.NO_HUN_FOR_OLD_WHEN)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index aeae89c..8874f59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -18,6 +18,7 @@
 
 import android.animation.ObjectAnimator
 import android.util.FloatProperty
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
@@ -31,6 +32,7 @@
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.KeyguardBypassController.OnBypassStateChangedListener
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
@@ -38,7 +40,6 @@
 import javax.inject.Inject
 import kotlin.math.min
 
-
 @SysUISingleton
 class NotificationWakeUpCoordinator @Inject constructor(
     dumpManager: DumpManager,
@@ -68,6 +69,7 @@
     private var mLinearDozeAmount: Float = 0.0f
     private var mDozeAmount: Float = 0.0f
     private var mDozeAmountSource: String = "init"
+    private var mNotifsHiddenByDozeAmountOverride: Boolean = false
     private var mNotificationVisibleAmount = 0.0f
     private var mNotificationsVisible = false
     private var mNotificationsVisibleForExpansion = false
@@ -130,6 +132,7 @@
                 }
             }
         }
+
     /**
      * True if we can show pulsing heads up notifications
      */
@@ -149,10 +152,19 @@
             return canShow
         }
 
+    private val bypassStateChangedListener = object : OnBypassStateChangedListener {
+        override fun onBypassStateChanged(isEnabled: Boolean) {
+            // When the bypass state changes, we have to check whether we should re-show the
+            // notifications by clearing the doze amount override which hides them.
+            maybeClearDozeAmountOverrideHidingNotifs()
+        }
+    }
+
     init {
         dumpManager.registerDumpable(this)
         mHeadsUpManager.addListener(this)
         statusBarStateController.addCallback(this)
+        bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
         addListener(object : WakeUpListener {
             override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
                 if (isFullyHidden && mNotificationsVisibleForExpansion) {
@@ -261,12 +273,18 @@
         setDozeAmount(linear, eased, source = "StatusBar")
     }
 
-    fun setDozeAmount(linear: Float, eased: Float, source: String) {
+    fun setDozeAmount(
+        linear: Float,
+        eased: Float,
+        source: String,
+        hidesNotifsByOverride: Boolean = false
+    ) {
         val changed = linear != mLinearDozeAmount
         logger.logSetDozeAmount(linear, eased, source, statusBarStateController.state, changed)
         mLinearDozeAmount = linear
         mDozeAmount = eased
         mDozeAmountSource = source
+        mNotifsHiddenByDozeAmountOverride = hidesNotifsByOverride
         mStackScrollerController.setDozeAmount(mDozeAmount)
         updateHideAmount()
         if (changed && linear == 0.0f) {
@@ -285,27 +303,29 @@
             // the doze amount to 0f (not dozing) so that the notifications are no longer hidden.
             // See: UnlockedScreenOffAnimationController.onFinishedWakingUp()
             setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)")
+            this.state = newState
+            return
         }
 
         if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) {
+            this.state = newState
             return
         }
 
         if (overrideDozeAmountIfBypass()) {
+            this.state = newState
             return
         }
 
-        if (bypassController.bypassEnabled &&
-                newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED &&
-            (!statusBarStateController.isDozing || shouldAnimateVisibility())) {
-            // We're leaving shade locked. Let's animate the notifications away
-            setNotificationsVisible(visible = true, increaseSpeed = false, animate = false)
-            setNotificationsVisible(visible = false, increaseSpeed = false, animate = true)
-        }
+        maybeClearDozeAmountOverrideHidingNotifs()
 
         this.state = newState
     }
 
+    @VisibleForTesting
+    val statusBarState: Int
+        get() = state
+
     override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
         val collapsedEnough = event.fraction <= 0.9f
         if (collapsedEnough != this.collapsedEnoughToHide) {
@@ -325,7 +345,8 @@
     private fun overrideDozeAmountIfBypass(): Boolean {
         if (bypassController.bypassEnabled) {
             if (statusBarStateController.state == StatusBarState.KEYGUARD) {
-                setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
+                setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)",
+                        hidesNotifsByOverride = true)
             } else {
                 setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
             }
@@ -335,6 +356,37 @@
     }
 
     /**
+     * If the last [setDozeAmount] call was an override to hide notifications, then this call will
+     * check for the set of states that may have caused that override, and if none of them still
+     * apply, and the device is awake or not on the keyguard, then dozeAmount will be reset to 0.
+     * This fixes bugs where the bypass state changing could result in stale overrides, hiding
+     * notifications either on the inside screen or even after unlock.
+     */
+    private fun maybeClearDozeAmountOverrideHidingNotifs() {
+        if (mNotifsHiddenByDozeAmountOverride) {
+            val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
+            val dozing = statusBarStateController.isDozing
+            val bypass = bypassController.bypassEnabled
+            val animating =
+                    screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()
+            // Overrides are set by [overrideDozeAmountIfAnimatingScreenOff] and
+            // [overrideDozeAmountIfBypass] based on 'animating' and 'bypass' respectively, so only
+            // clear the override if both those conditions are cleared.  But also require either
+            // !dozing or !onKeyguard because those conditions should indicate that we intend
+            // notifications to be visible, and thus it is safe to unhide them.
+            val willRemove = (!onKeyguard || !dozing) && !bypass && !animating
+            logger.logMaybeClearDozeAmountOverrideHidingNotifs(
+                    willRemove = willRemove,
+                    onKeyguard = onKeyguard, dozing = dozing,
+                    bypass = bypass, animating = animating,
+            )
+            if (willRemove) {
+                setDozeAmount(0f, 0f, source = "Removed: $mDozeAmountSource")
+            }
+        }
+    }
+
+    /**
      * If we're playing the screen off animation, force the notification doze amount to be 1f (fully
      * dozing). This is needed so that the notifications aren't briefly visible as the screen turns
      * off and dozeAmount goes from 1f to 0f.
@@ -344,7 +396,8 @@
      */
     private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean {
         if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) {
-            setDozeAmount(1f, 1f, source = "Override: animating screen off")
+            setDozeAmount(1f, 1f, source = "Override: animating screen off",
+                    hidesNotifsByOverride = true)
             return true
         }
 
@@ -430,6 +483,7 @@
         pw.println("mLinearDozeAmount: $mLinearDozeAmount")
         pw.println("mDozeAmount: $mDozeAmount")
         pw.println("mDozeAmountSource: $mDozeAmountSource")
+        pw.println("mNotifsHiddenByDozeAmountOverride: $mNotifsHiddenByDozeAmountOverride")
         pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
         pw.println("mNotificationsVisible: $mNotificationsVisible")
         pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
index de18b0c..88d9ffc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
@@ -13,7 +13,7 @@
 
 package com.android.systemui.statusbar.notification
 
-import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.log.dagger.NotificationLockscreenLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.statusbar.StatusBarState
@@ -21,7 +21,12 @@
 
 class NotificationWakeUpCoordinatorLogger
 @Inject
-constructor(@NotificationLog private val buffer: LogBuffer) {
+constructor(@NotificationLockscreenLog private val buffer: LogBuffer) {
+    private var lastSetDozeAmountLogWasFractional = false
+    private var lastSetDozeAmountLogState = -1
+    private var lastSetDozeAmountLogSource = "undefined"
+    private var lastOnDozeAmountChangedLogWasFractional = false
+
     fun logSetDozeAmount(
         linear: Float,
         eased: Float,
@@ -29,6 +34,20 @@
         state: Int,
         changed: Boolean,
     ) {
+        // Avoid logging on every frame of the animation if important values are not changing
+        val isFractional = linear != 1f && linear != 0f
+        if (
+            lastSetDozeAmountLogWasFractional &&
+                isFractional &&
+                lastSetDozeAmountLogState == state &&
+                lastSetDozeAmountLogSource == source
+        ) {
+            return
+        }
+        lastSetDozeAmountLogWasFractional = isFractional
+        lastSetDozeAmountLogState = state
+        lastSetDozeAmountLogSource = source
+
         buffer.log(
             TAG,
             DEBUG,
@@ -46,7 +65,30 @@
         )
     }
 
+    fun logMaybeClearDozeAmountOverrideHidingNotifs(
+        willRemove: Boolean,
+        onKeyguard: Boolean,
+        dozing: Boolean,
+        bypass: Boolean,
+        animating: Boolean,
+    ) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 =
+                    "willRemove=$willRemove onKeyguard=$onKeyguard dozing=$dozing" +
+                        " bypass=$bypass animating=$animating"
+            },
+            { "maybeClearDozeAmountOverrideHidingNotifs() $str1" }
+        )
+    }
+
     fun logOnDozeAmountChanged(linear: Float, eased: Float) {
+        // Avoid logging on every frame of the animation when values are fractional
+        val isFractional = linear != 1f && linear != 0f
+        if (lastOnDozeAmountChangedLogWasFractional && isFractional) return
+        lastOnDozeAmountChangedLogWasFractional = isFractional
         buffer.log(
             TAG,
             DEBUG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index 84ab0d1..b5fce41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -98,13 +98,11 @@
      * This can happen if the entry is removed from a group that was broken up or if the entry was
      * filtered out during any of the filtering steps.
      */
-    fun detach(includingStableIndex: Boolean) {
+    fun detach() {
         parent = null
         section = null
         promoter = null
-        if (includingStableIndex) {
-            stableIndex = -1
-        }
+        stableIndex = -1
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index df35c9e..aa9a6c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -164,6 +164,11 @@
 
 
     private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
+    private final Runnable mRebuildListRunnable = () -> {
+        if (mBuildListener != null) {
+            mBuildListener.onBuildList(mReadOnlyNotificationSet, "asynchronousUpdate");
+        }
+    };
 
     private boolean mAttached = false;
     private boolean mAmDispatchingToOtherCode;
@@ -458,7 +463,7 @@
             int modificationType) {
         Assert.isMainThread();
         mEventQueue.add(new ChannelChangedEvent(pkgName, user, channel, modificationType));
-        dispatchEventsAndRebuildList("onNotificationChannelModified");
+        dispatchEventsAndAsynchronouslyRebuildList();
     }
 
     private void onNotificationsInitialized() {
@@ -613,15 +618,39 @@
 
     private void dispatchEventsAndRebuildList(String reason) {
         Trace.beginSection("NotifCollection.dispatchEventsAndRebuildList");
+        if (mMainHandler.hasCallbacks(mRebuildListRunnable)) {
+            mMainHandler.removeCallbacks(mRebuildListRunnable);
+        }
+
+        dispatchEvents();
+
+        if (mBuildListener != null) {
+            mBuildListener.onBuildList(mReadOnlyNotificationSet, reason);
+        }
+        Trace.endSection();
+    }
+
+    private void dispatchEventsAndAsynchronouslyRebuildList() {
+        Trace.beginSection("NotifCollection.dispatchEventsAndAsynchronouslyRebuildList");
+
+        dispatchEvents();
+
+        if (!mMainHandler.hasCallbacks(mRebuildListRunnable)) {
+            mMainHandler.postDelayed(mRebuildListRunnable, 1000L);
+        }
+
+        Trace.endSection();
+    }
+
+    private void dispatchEvents() {
+        Trace.beginSection("NotifCollection.dispatchEvents");
+
         mAmDispatchingToOtherCode = true;
         while (!mEventQueue.isEmpty()) {
             mEventQueue.remove().dispatchTo(mNotifCollectionListeners);
         }
         mAmDispatchingToOtherCode = false;
 
-        if (mBuildListener != null) {
-            mBuildListener.onBuildList(mReadOnlyNotificationSet, reason);
-        }
         Trace.endSection();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 65a21a4..4065b98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -965,8 +965,7 @@
      * filtered out during any of the filtering steps.
      */
     private void annulAddition(ListEntry entry) {
-        // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
-        entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
+        entry.getAttachState().detach();
     }
 
     private void assignSections() {
@@ -986,50 +985,10 @@
 
     private void sortListAndGroups() {
         Trace.beginSection("ShadeListBuilder.sortListAndGroups");
-        if (mFlags.isSemiStableSortEnabled()) {
-            sortWithSemiStableSort();
-        } else {
-            sortWithLegacyStability();
-        }
+        sortWithSemiStableSort();
         Trace.endSection();
     }
 
-    private void sortWithLegacyStability() {
-        // Sort all groups and the top level list
-        for (ListEntry entry : mNotifList) {
-            if (entry instanceof GroupEntry) {
-                GroupEntry parent = (GroupEntry) entry;
-                parent.sortChildren(mGroupChildrenComparator);
-            }
-        }
-        mNotifList.sort(mTopLevelComparator);
-        assignIndexes(mNotifList);
-
-        // Check for suppressed order changes
-        if (!getStabilityManager().isEveryChangeAllowed()) {
-            mForceReorderable = true;
-            boolean isSorted = isShadeSortedLegacy();
-            mForceReorderable = false;
-            if (!isSorted) {
-                getStabilityManager().onEntryReorderSuppressed();
-            }
-        }
-    }
-
-    private boolean isShadeSortedLegacy() {
-        if (!isSorted(mNotifList, mTopLevelComparator)) {
-            return false;
-        }
-        for (ListEntry entry : mNotifList) {
-            if (entry instanceof GroupEntry) {
-                if (!isSorted(((GroupEntry) entry).getChildren(), mGroupChildrenComparator)) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
     private void sortWithSemiStableSort() {
         // Sort each group's children
         boolean allSorted = true;
@@ -1100,29 +1059,16 @@
                 sectionMemberIndex = 0;
                 currentSection = section;
             }
-            if (mFlags.isStabilityIndexFixEnabled()) {
-                entry.getAttachState().setStableIndex(sectionMemberIndex++);
-                if (entry instanceof GroupEntry) {
-                    final GroupEntry parent = (GroupEntry) entry;
-                    final NotificationEntry summary = parent.getSummary();
-                    if (summary != null) {
-                        summary.getAttachState().setStableIndex(sectionMemberIndex++);
-                    }
-                    for (NotificationEntry child : parent.getChildren()) {
-                        child.getAttachState().setStableIndex(sectionMemberIndex++);
-                    }
+            entry.getAttachState().setStableIndex(sectionMemberIndex++);
+            if (entry instanceof GroupEntry) {
+                final GroupEntry parent = (GroupEntry) entry;
+                final NotificationEntry summary = parent.getSummary();
+                if (summary != null) {
+                    summary.getAttachState().setStableIndex(sectionMemberIndex++);
                 }
-            } else {
-                // This old implementation uses the same index number for the group as the first
-                // child, and fails to assign an index to the summary.  Remove once tested.
-                entry.getAttachState().setStableIndex(sectionMemberIndex);
-                if (entry instanceof GroupEntry) {
-                    final GroupEntry parent = (GroupEntry) entry;
-                    for (NotificationEntry child : parent.getChildren()) {
-                        child.getAttachState().setStableIndex(sectionMemberIndex++);
-                    }
+                for (NotificationEntry child : parent.getChildren()) {
+                    child.getAttachState().setStableIndex(sectionMemberIndex++);
                 }
-                sectionMemberIndex++;
             }
         }
     }
@@ -1272,11 +1218,6 @@
                 o2.getSectionIndex());
         if (cmp != 0) return cmp;
 
-        cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
-                getStableOrderIndex(o1),
-                getStableOrderIndex(o2));
-        if (cmp != 0) return cmp;
-
         NotifComparator sectionComparator = getSectionComparator(o1, o2);
         if (sectionComparator != null) {
             cmp = sectionComparator.compare(o1, o2);
@@ -1301,12 +1242,7 @@
 
 
     private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
-        int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
-                getStableOrderIndex(o1),
-                getStableOrderIndex(o2));
-        if (cmp != 0) return cmp;
-
-        cmp = Integer.compare(
+        int cmp = Integer.compare(
                 o1.getRepresentativeEntry().getRanking().getRank(),
                 o2.getRepresentativeEntry().getRanking().getRank());
         if (cmp != 0) return cmp;
@@ -1317,25 +1253,6 @@
         return cmp;
     };
 
-    /**
-     * A flag that is set to true when we want to run the comparators as if all reordering is
-     * allowed.  This is used to check if the list is "out of order" after the sort is complete.
-     */
-    private boolean mForceReorderable = false;
-
-    private int getStableOrderIndex(ListEntry entry) {
-        if (mForceReorderable) {
-            // this is used to determine if the list is correctly sorted
-            return -1;
-        }
-        if (getStabilityManager().isEntryReorderingAllowed(entry)) {
-            // let the stability manager constrain or allow reordering
-            return -1;
-        }
-        // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
-        return entry.getPreviousAttachState().getStableIndex();
-    }
-
     @Nullable
     private Integer getStableOrderRank(ListEntry entry) {
         if (getStabilityManager().isEntryReorderingAllowed(entry)) {
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 76252d0..82bd45c 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
@@ -14,18 +14,19 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 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,20 +36,25 @@
 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
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.launch
 
 /**
@@ -60,6 +66,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,37 +94,87 @@
     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
+        var clearUnseenOnUnlock = false
         keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing ->
-            if (!isKeyguardShowing) {
+            if (isKeyguardShowing) {
+                // Wait for the user to spend enough time on the lock screen before clearing unseen
+                // set when unlocked
+                awaitTimeSpentNotDozing(SEEN_TIMEOUT)
+                clearUnseenOnUnlock = true
+            } else {
                 unseenNotifFilter.invalidateList("keyguard no longer showing")
-                delay(SEEN_TIMEOUT)
+                if (clearUnseenOnUnlock) {
+                    clearUnseenOnUnlock = false
+                    unseenNotifications.clear()
+                }
+                trackUnseenNotifications()
+            }
+        }
+    }
+
+    private suspend fun awaitTimeSpentNotDozing(duration: Duration) {
+        keyguardRepository.isDozing
+            // Use transformLatest so that the timeout delay is cancelled if the device enters doze,
+            // and is restarted when doze ends.
+            .transformLatest { isDozing ->
+                if (!isDozing) {
+                    delay(duration)
+                    // Signal timeout has completed
+                    emit(Unit)
+                }
+            }
+            // Suspend until the first emission
+            .first()
+    }
+
+    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) }
             // for each change, lookup the new value
             .map {
-                secureSettings.getBoolForUser(
+                secureSettings.getIntForUser(
                     Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
                     UserHandle.USER_CURRENT,
-                )
+                ) == 1
             }
             // perform lookups on the bg thread pool
             .flowOn(bgDispatcher)
@@ -136,13 +193,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)
                 }
             }
@@ -215,15 +276,3 @@
         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/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 1399385..03a3ca5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -88,9 +88,7 @@
         mCoordinators.add(viewConfigCoordinator)
         mCoordinators.add(visualStabilityCoordinator)
         mCoordinators.add(sensitiveContentCoordinator)
-        if (notifPipelineFlags.isSmartspaceDedupingEnabled()) {
-            mCoordinators.add(smartspaceDedupingCoordinator)
-        }
+        mCoordinators.add(smartspaceDedupingCoordinator)
         mCoordinators.add(headsUpCoordinator)
         mCoordinators.add(gutsCoordinator)
         mCoordinators.add(preparationCoordinator)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 808638a..8436ff7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -16,27 +16,15 @@
 
 package com.android.systemui.statusbar.notification.dagger;
 
-import android.app.INotificationManager;
 import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.content.pm.ShortcutManager;
-import android.os.Handler;
-import android.view.accessibility.AccessibilityManager;
 
-import com.android.internal.logging.UiEventLogger;
 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.dagger.qualifiers.UiBackground;
-import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.settings.UserContextProvider;
-import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeEventsModule;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
 import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
 import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
@@ -50,7 +38,6 @@
 import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
 import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl;
 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderModule;
 import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
@@ -72,22 +59,17 @@
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
 import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl;
-import com.android.systemui.statusbar.notification.row.ChannelEditorDialogController;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
 import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager;
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.wmshell.BubblesManager;
 
-import java.util.Optional;
 import java.util.concurrent.Executor;
 
 import javax.inject.Provider;
 
 import dagger.Binds;
-import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -109,48 +91,6 @@
     @Binds
     StackScrollAlgorithm.BypassController bindBypassController(KeyguardBypassController impl);
 
-    /** Provides an instance of {@link NotificationGutsManager} */
-    @SysUISingleton
-    @Provides
-    static NotificationGutsManager provideNotificationGutsManager(
-            Context context,
-            Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
-            @Main Handler mainHandler,
-            @Background Handler bgHandler,
-            AccessibilityManager accessibilityManager,
-            HighPriorityProvider highPriorityProvider,
-            INotificationManager notificationManager,
-            PeopleSpaceWidgetManager peopleSpaceWidgetManager,
-            LauncherApps launcherApps,
-            ShortcutManager shortcutManager,
-            ChannelEditorDialogController channelEditorDialogController,
-            UserContextProvider contextTracker,
-            AssistantFeedbackController assistantFeedbackController,
-            Optional<BubblesManager> bubblesManagerOptional,
-            UiEventLogger uiEventLogger,
-            OnUserInteractionCallback onUserInteractionCallback,
-            ShadeController shadeController) {
-        return new NotificationGutsManager(
-                context,
-                centralSurfacesOptionalLazy,
-                mainHandler,
-                bgHandler,
-                accessibilityManager,
-                highPriorityProvider,
-                notificationManager,
-                peopleSpaceWidgetManager,
-                launcherApps,
-                shortcutManager,
-                channelEditorDialogController,
-                contextTracker,
-                assistantFeedbackController,
-                bubblesManagerOptional,
-                uiEventLogger,
-                onUserInteractionCallback,
-                shadeController
-        );
-    }
-
     /** Provides an instance of {@link NotifGutsViewManager} */
     @Binds
     NotifGutsViewManager bindNotifGutsViewManager(NotificationGutsManager notificationGutsManager);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 13b3aca..27fe747 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -70,7 +70,15 @@
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
         }, {
-            "No alerting: snoozed package: $str1"
+            "No heads up: snoozed package: $str1"
+        })
+    }
+
+    fun logHeadsUpPackageSnoozeBypassedHasFsi(entry: NotificationEntry) {
+        buffer.log(TAG, DEBUG, {
+            str1 = entry.logKey
+        }, {
+            "Heads up: package snooze bypassed because notification has full-screen intent: $str1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
index 7136cad..bc881ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
@@ -31,6 +31,10 @@
      */
     enum FullScreenIntentDecision {
         /**
+         * Full screen intents are disabled.
+         */
+        NO_FSI_DISABLED(false),
+        /**
          * No full screen intent included, so there is nothing to show.
          */
         NO_FULL_SCREEN_INTENT(false),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index d9dacfd..afeb72f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -19,6 +19,7 @@
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD;
 import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI;
 
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -28,7 +29,6 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.dreams.IDreamManager;
 import android.service.notification.StatusBarNotification;
@@ -40,6 +40,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -74,6 +75,7 @@
     private final NotifPipelineFlags mFlags;
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
     private final UiEventLogger mUiEventLogger;
+    private final UserTracker mUserTracker;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
@@ -86,7 +88,10 @@
         FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236),
 
         @UiEvent(doc = "HUN suppressed for old when")
-        HUN_SUPPRESSED_OLD_WHEN(1237);
+        HUN_SUPPRESSED_OLD_WHEN(1237),
+
+        @UiEvent(doc = "HUN snooze bypassed for potentially suppressed FSI")
+        HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI(1269);
 
         private final int mId;
 
@@ -114,7 +119,8 @@
             @Main Handler mainHandler,
             NotifPipelineFlags flags,
             KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            UserTracker userTracker) {
         mContentResolver = contentResolver;
         mPowerManager = powerManager;
         mDreamManager = dreamManager;
@@ -127,6 +133,7 @@
         mFlags = flags;
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
         mUiEventLogger = uiEventLogger;
+        mUserTracker = userTracker;
         ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -236,6 +243,9 @@
 
     @Override
     public FullScreenIntentDecision getFullScreenIntentDecision(NotificationEntry entry) {
+        if (mFlags.disableFsi()) {
+            return FullScreenIntentDecision.NO_FSI_DISABLED;
+        }
         if (entry.getSbn().getNotification().fullScreenIntent == null) {
             return FullScreenIntentDecision.NO_FULL_SCREEN_INTENT;
         }
@@ -325,6 +335,9 @@
         final int uid = entry.getSbn().getUid();
         final String packageName = entry.getSbn().getPackageName();
         switch (decision) {
+            case NO_FSI_DISABLED:
+                mLogger.logNoFullscreen(entry, "Disabled");
+                return;
             case NO_FULL_SCREEN_INTENT:
                 return;
             case NO_FSI_SUPPRESSED_BY_DND:
@@ -400,7 +413,15 @@
             return false;
         }
 
-        if (isSnoozedPackage(sbn)) {
+        final boolean isSnoozedPackage = isSnoozedPackage(sbn);
+        final boolean fsiRequiresKeyguard = mFlags.fullScreenIntentRequiresKeyguard();
+        final boolean hasFsi = sbn.getNotification().fullScreenIntent != null;
+
+        // Assume any notification with an FSI is time-sensitive (like an alarm or incoming call)
+        // and ignore whether HUNs have been snoozed for the package.
+        final boolean shouldBypassSnooze = fsiRequiresKeyguard && hasFsi;
+
+        if (isSnoozedPackage && !shouldBypassSnooze) {
             if (log) mLogger.logNoHeadsUpPackageSnoozed(entry);
             return false;
         }
@@ -438,6 +459,19 @@
                 return false;
             }
         }
+
+        if (isSnoozedPackage) {
+            if (log) {
+                mLogger.logHeadsUpPackageSnoozeBypassedHasFsi(entry);
+                final int uid = entry.getSbn().getUid();
+                final String packageName = entry.getSbn().getPackageName();
+                mUiEventLogger.log(HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI, uid,
+                        packageName);
+            }
+
+            return true;
+        }
+
         if (log) mLogger.logHeadsUp(entry);
         return true;
     }
@@ -450,7 +484,7 @@
      * @return true if the entry should ambient pulse, false otherwise
      */
     private boolean shouldHeadsUpWhenDozing(NotificationEntry entry, boolean log) {
-        if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
+        if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(mUserTracker.getUserId())) {
             if (log) mLogger.logNoPulsingSettingDisabled(entry);
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 5f6a5cb..26f97de 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -63,7 +63,7 @@
  * are not.
  */
 public class NotificationLogger implements StateListener {
-    private static final String TAG = "NotificationLogger";
+    static final String TAG = "NotificationLogger";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
 
     /** The minimum delay in ms between reports of notification visibility. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
index ffd931c..197ae1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
@@ -18,9 +18,12 @@
 package com.android.systemui.statusbar.notification.logging
 
 import android.stats.sysui.NotificationEnums
+import android.util.Log
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.dump.DumpsysTableLogger
+import com.android.systemui.dump.Row
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -33,6 +36,7 @@
 
     fun init() {
         dumpManager.registerNormalDumpable(javaClass.simpleName, this)
+        Log.i("NotificationMemory", "Registered dumpable.")
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -45,27 +49,36 @@
 
     /** Renders a table of notification object usage into passed [PrintWriter]. */
     private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) {
-        pw.println("Notification Object Usage")
-        pw.println("-----------")
-        pw.println(
-            "Package".padEnd(35) +
-                "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom"
-        )
-        pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView")
-        pw.println()
-
-        memoryUse.forEach { use ->
-            pw.println(
-                use.packageName.padEnd(35) +
-                    "\t\t" +
-                    "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" +
-                    (styleEnumToString(use.objectUsage.style).take(15) ?: "").padEnd(15) +
-                    "\t\t${use.objectUsage.styleIcon}\t" +
-                    "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" +
-                    "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" +
-                    use.notificationKey
+        val columns =
+            listOf(
+                "Package",
+                "Small Icon",
+                "Large Icon",
+                "Style",
+                "Style Icon",
+                "Big Picture",
+                "Extender",
+                "Extras",
+                "Custom View",
+                "Key"
             )
-        }
+        val rows: List<Row> =
+            memoryUse.map {
+                listOf(
+                    it.packageName,
+                    toKb(it.objectUsage.smallIcon),
+                    toKb(it.objectUsage.largeIcon),
+                    styleEnumToString(it.objectUsage.style),
+                    toKb(it.objectUsage.styleIcon),
+                    toKb(it.objectUsage.bigPicture),
+                    toKb(it.objectUsage.extender),
+                    toKb(it.objectUsage.extras),
+                    it.objectUsage.hasCustomView.toString(),
+                    // | is a  field delimiter in the output format so we need to replace
+                    // it to avoid breakage.
+                    it.notificationKey.replace('|', '│')
+                )
+            }
 
         // Calculate totals for easily glanceable summary.
         data class Totals(
@@ -88,18 +101,23 @@
                 t
             }
 
-        pw.println()
-        pw.println("TOTALS")
-        pw.println(
-            "".padEnd(35) +
-                "\t\t" +
-                "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" +
-                "".padEnd(15) +
-                "\t\t${toKb(totals.styleIcon)}\t" +
-                "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" +
-                toKb(totals.extras)
-        )
-        pw.println()
+        val totalsRow: List<Row> =
+            listOf(
+                listOf(
+                    "TOTALS",
+                    toKb(totals.smallIcon),
+                    toKb(totals.largeIcon),
+                    "",
+                    toKb(totals.styleIcon),
+                    toKb(totals.bigPicture),
+                    toKb(totals.extender),
+                    toKb(totals.extras),
+                    "",
+                    ""
+                )
+            )
+        val tableLogger = DumpsysTableLogger("Notification Object Usage", columns, rows + totalsRow)
+        tableLogger.printTableData(pw)
     }
 
     /** Renders a table of notification view usage into passed [PrintWriter] */
@@ -116,40 +134,65 @@
             var softwareBitmapsPenalty: Int = 0,
         )
 
-        val totals = Totals()
-        pw.println("Notification View Usage")
-        pw.println("-----------")
-        pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware")
-        pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps")
-        pw.println()
-        memoryUse
-            .filter { it.viewUsage.isNotEmpty() }
-            .forEach { use ->
-                pw.println(use.packageName + " " + use.notificationKey)
-                use.viewUsage.forEach { view ->
-                    pw.println(
-                        "  ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" +
-                            "\t${view.largeIcon}\t${view.style}" +
-                            "\t${view.customViews}\t${view.softwareBitmapsPenalty}"
-                    )
-
-                    if (view.viewType == ViewType.TOTAL) {
-                        totals.smallIcon += view.smallIcon
-                        totals.largeIcon += view.largeIcon
-                        totals.style += view.style
-                        totals.customViews += view.customViews
-                        totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
+        val columns =
+            listOf(
+                "Package",
+                "View Type",
+                "Small Icon",
+                "Large Icon",
+                "Style Use",
+                "Custom View",
+                "Software Bitmaps",
+                "Key"
+            )
+        val rows =
+            memoryUse
+                .filter { it.viewUsage.isNotEmpty() }
+                .flatMap { use ->
+                    use.viewUsage.map { view ->
+                        listOf(
+                            use.packageName,
+                            view.viewType.toString(),
+                            toKb(view.smallIcon),
+                            toKb(view.largeIcon),
+                            toKb(view.style),
+                            toKb(view.customViews),
+                            toKb(view.softwareBitmapsPenalty),
+                            // | is a  field delimiter in the output format so we need to replace
+                            // it to avoid breakage.
+                            use.notificationKey.replace('|', '│')
+                        )
                     }
                 }
+
+        val totals = Totals()
+        memoryUse
+            .filter { it.viewUsage.isNotEmpty() }
+            .map { it.viewUsage.firstOrNull { view -> view.viewType == ViewType.TOTAL } }
+            .filterNotNull()
+            .forEach { view ->
+                totals.smallIcon += view.smallIcon
+                totals.largeIcon += view.largeIcon
+                totals.style += view.style
+                totals.customViews += view.customViews
+                totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
             }
-        pw.println()
-        pw.println("TOTALS")
-        pw.println(
-            "  ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" +
-                "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" +
-                "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}"
-        )
-        pw.println()
+
+        val totalsRow: List<Row> =
+            listOf(
+                listOf(
+                    "TOTALS",
+                    "",
+                    toKb(totals.smallIcon),
+                    toKb(totals.largeIcon),
+                    toKb(totals.style),
+                    toKb(totals.customViews),
+                    toKb(totals.softwareBitmapsPenalty),
+                    ""
+                )
+            )
+        val tableLogger = DumpsysTableLogger("Notification View Usage", columns, rows + totalsRow)
+        tableLogger.printTableData(pw)
     }
 
     private fun styleEnumToString(styleEnum: Int): String =
@@ -168,6 +211,10 @@
         }
 
     private fun toKb(bytes: Int): String {
-        return (bytes / 1024).toString() + " KB"
+        if (bytes == 0) {
+            return "--"
+        }
+
+        return "%.2f KB".format(bytes / 1024f)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
index ec8501a..abe0670 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
@@ -18,13 +18,16 @@
 package com.android.systemui.statusbar.notification.logging
 
 import android.app.StatsManager
+import android.util.Log
 import android.util.StatsEvent
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.shared.system.SysUiStatsLog
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.util.traceSection
+import java.lang.Exception
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlin.math.roundToInt
@@ -82,43 +85,56 @@
                 return StatsManager.PULL_SKIP
             }
 
-            // Notifications can only be retrieved on the main thread, so switch to that thread.
-            val notifications = getAllNotificationsOnMainThread()
-            val notificationMemoryUse =
-                NotificationMemoryMeter.notificationMemoryUse(notifications)
-                    .sortedWith(
-                        compareBy(
-                            { it.packageName },
-                            { it.objectUsage.style },
-                            { it.notificationKey }
+            try {
+                // Notifications can only be retrieved on the main thread, so switch to that thread.
+                val notifications = getAllNotificationsOnMainThread()
+                val notificationMemoryUse =
+                    NotificationMemoryMeter.notificationMemoryUse(notifications)
+                        .sortedWith(
+                            compareBy(
+                                { it.packageName },
+                                { it.objectUsage.style },
+                                { it.notificationKey }
+                            )
+                        )
+                val usageData = aggregateMemoryUsageData(notificationMemoryUse)
+                usageData.forEach { (_, use) ->
+                    data.add(
+                        SysUiStatsLog.buildStatsEvent(
+                            SysUiStatsLog.NOTIFICATION_MEMORY_USE,
+                            use.uid,
+                            use.style,
+                            use.count,
+                            use.countWithInflatedViews,
+                            toKb(use.smallIconObject),
+                            use.smallIconBitmapCount,
+                            toKb(use.largeIconObject),
+                            use.largeIconBitmapCount,
+                            toKb(use.bigPictureObject),
+                            use.bigPictureBitmapCount,
+                            toKb(use.extras),
+                            toKb(use.extenders),
+                            toKb(use.smallIconViews),
+                            toKb(use.largeIconViews),
+                            toKb(use.systemIconViews),
+                            toKb(use.styleViews),
+                            toKb(use.customViews),
+                            toKb(use.softwareBitmaps),
+                            use.seenCount
                         )
                     )
-            val usageData = aggregateMemoryUsageData(notificationMemoryUse)
-            usageData.forEach { (_, use) ->
-                data.add(
-                    SysUiStatsLog.buildStatsEvent(
-                        SysUiStatsLog.NOTIFICATION_MEMORY_USE,
-                        use.uid,
-                        use.style,
-                        use.count,
-                        use.countWithInflatedViews,
-                        toKb(use.smallIconObject),
-                        use.smallIconBitmapCount,
-                        toKb(use.largeIconObject),
-                        use.largeIconBitmapCount,
-                        toKb(use.bigPictureObject),
-                        use.bigPictureBitmapCount,
-                        toKb(use.extras),
-                        toKb(use.extenders),
-                        toKb(use.smallIconViews),
-                        toKb(use.largeIconViews),
-                        toKb(use.systemIconViews),
-                        toKb(use.styleViews),
-                        toKb(use.customViews),
-                        toKb(use.softwareBitmaps),
-                        use.seenCount
-                    )
-                )
+                }
+            } catch (e: InterruptedException) {
+                // This can happen if the device is sleeping or view walking takes too long.
+                // The statsd collector will interrupt the thread and we need to handle it
+                // gracefully.
+                Log.w(NotificationLogger.TAG, "Timed out when measuring notification memory.", e)
+                return@traceSection StatsManager.PULL_SKIP
+            } catch (e: Exception) {
+                // Error while collecting data, this should not crash prod SysUI. Just
+                // log WTF and move on.
+                Log.wtf(NotificationLogger.TAG, "Failed to measure notification memory.", e)
+                return@traceSection StatsManager.PULL_SKIP
             }
 
             return StatsManager.PULL_SUCCESS
@@ -128,67 +144,70 @@
         runBlocking(mainDispatcher) {
             traceSection("NML#getNotifications") { notificationPipeline.allNotifs }
         }
-
-    /** Aggregates memory usage data by package and style, returning sums. */
-    private fun aggregateMemoryUsageData(
-        notificationMemoryUse: List<NotificationMemoryUsage>
-    ): Map<Pair<String, Int>, NotificationMemoryUseAtomBuilder> {
-        return notificationMemoryUse
-            .groupingBy { Pair(it.packageName, it.objectUsage.style) }
-            .aggregate {
-                _,
-                accumulator: NotificationMemoryUseAtomBuilder?,
-                element: NotificationMemoryUsage,
-                first ->
-                val use =
-                    if (first) {
-                        NotificationMemoryUseAtomBuilder(element.uid, element.objectUsage.style)
-                    } else {
-                        accumulator!!
-                    }
-
-                use.count++
-                // If the views of the notification weren't inflated, the list of memory usage
-                // parameters will be empty.
-                if (element.viewUsage.isNotEmpty()) {
-                    use.countWithInflatedViews++
-                }
-
-                use.smallIconObject += element.objectUsage.smallIcon
-                if (element.objectUsage.smallIcon > 0) {
-                    use.smallIconBitmapCount++
-                }
-
-                use.largeIconObject += element.objectUsage.largeIcon
-                if (element.objectUsage.largeIcon > 0) {
-                    use.largeIconBitmapCount++
-                }
-
-                use.bigPictureObject += element.objectUsage.bigPicture
-                if (element.objectUsage.bigPicture > 0) {
-                    use.bigPictureBitmapCount++
-                }
-
-                use.extras += element.objectUsage.extras
-                use.extenders += element.objectUsage.extender
-
-                // Use totals count which are more accurate when aggregated
-                // in this manner.
-                element.viewUsage
-                    .firstOrNull { vu -> vu.viewType == ViewType.TOTAL }
-                    ?.let {
-                        use.smallIconViews += it.smallIcon
-                        use.largeIconViews += it.largeIcon
-                        use.systemIconViews += it.systemIcons
-                        use.styleViews += it.style
-                        use.customViews += it.style
-                        use.softwareBitmaps += it.softwareBitmapsPenalty
-                    }
-
-                return@aggregate use
-            }
-    }
-
-    /** Rounds the passed value to the nearest KB - e.g. 700B rounds to 1KB. */
-    private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt()
 }
+
+/** Aggregates memory usage data by package and style, returning sums. */
+@VisibleForTesting
+internal fun aggregateMemoryUsageData(
+    notificationMemoryUse: List<NotificationMemoryUsage>
+): Map<Pair<String, Int>, NotificationMemoryLogger.NotificationMemoryUseAtomBuilder> {
+    return notificationMemoryUse
+        .groupingBy { Pair(it.packageName, it.objectUsage.style) }
+        .aggregate {
+            _,
+            accumulator: NotificationMemoryLogger.NotificationMemoryUseAtomBuilder?,
+            element: NotificationMemoryUsage,
+            first ->
+            val use =
+                if (first) {
+                    NotificationMemoryLogger.NotificationMemoryUseAtomBuilder(
+                        element.uid,
+                        element.objectUsage.style
+                    )
+                } else {
+                    accumulator!!
+                }
+
+            use.count++
+            // If the views of the notification weren't inflated, the list of memory usage
+            // parameters will be empty.
+            if (element.viewUsage.isNotEmpty()) {
+                use.countWithInflatedViews++
+            }
+
+            use.smallIconObject += element.objectUsage.smallIcon
+            if (element.objectUsage.smallIcon > 0) {
+                use.smallIconBitmapCount++
+            }
+
+            use.largeIconObject += element.objectUsage.largeIcon
+            if (element.objectUsage.largeIcon > 0) {
+                use.largeIconBitmapCount++
+            }
+
+            use.bigPictureObject += element.objectUsage.bigPicture
+            if (element.objectUsage.bigPicture > 0) {
+                use.bigPictureBitmapCount++
+            }
+
+            use.extras += element.objectUsage.extras
+            use.extenders += element.objectUsage.extender
+
+            // Use totals count which are more accurate when aggregated
+            // in this manner.
+            element.viewUsage
+                .firstOrNull { vu -> vu.viewType == ViewType.TOTAL }
+                ?.let {
+                    use.smallIconViews += it.smallIcon
+                    use.largeIconViews += it.largeIcon
+                    use.systemIconViews += it.systemIcons
+                    use.styleViews += it.style
+                    use.customViews += it.customViews
+                    use.softwareBitmaps += it.softwareBitmapsPenalty
+                }
+
+            return@aggregate use
+        }
+}
+/** Rounds the passed value to the nearest KB - e.g. 700B rounds to 1KB. */
+private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 2d04211..6491223 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -184,19 +184,21 @@
     private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int =
         when (drawable) {
             is BitmapDrawable -> {
-                val ref = System.identityHashCode(drawable.bitmap)
-                if (seenObjects.contains(ref)) {
-                    0
-                } else {
-                    seenObjects.add(ref)
-                    drawable.bitmap.allocationByteCount
-                }
+                drawable.bitmap?.let {
+                    val ref = System.identityHashCode(it)
+                    if (seenObjects.contains(ref)) {
+                        0
+                    } else {
+                        seenObjects.add(ref)
+                        it.allocationByteCount
+                    }
+                } ?: 0
             }
             else -> 0
         }
 
     private fun isDrawableSoftwareBitmap(drawable: Drawable) =
-        drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE
+        drawable is BitmapDrawable && drawable.bitmap?.config != Bitmap.Config.HARDWARE
 
     private fun identifierForView(view: View) =
         if (view.id == View.NO_ID) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index fbe88df..68ad49be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -23,7 +23,9 @@
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.util.AttributeSet;
+import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
+import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityManager;
@@ -42,7 +44,9 @@
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.util.DumpUtilsKt;
 
+import java.io.PrintWriter;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -492,12 +496,9 @@
         if (animationListener != null) {
             mAppearAnimator.addListener(animationListener);
         }
-        if (delay > 0) {
-            // we need to apply the initial state already to avoid drawn frames in the wrong state
-            updateAppearAnimationAlpha();
-            updateAppearRect();
-            mAppearAnimator.setStartDelay(delay);
-        }
+        // we need to apply the initial state already to avoid drawn frames in the wrong state
+        updateAppearAnimationAlpha();
+        updateAppearRect();
         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
             private boolean mWasCancelled;
 
@@ -528,7 +529,20 @@
                 mWasCancelled = true;
             }
         });
-        mAppearAnimator.start();
+
+        // Cache the original animator so we can check if the animation should be started in the
+        // Choreographer callback. It's possible that the original animator (mAppearAnimator) is
+        // replaced with a new value before the callback is called.
+        ValueAnimator cachedAnimator = mAppearAnimator;
+        // Even when delay=0, starting the animation on the next frame is necessary to avoid jank.
+        // Not doing so will increase the chances our Animator will be forced to skip a value of
+        // the animation's progression, causing stutter.
+        Choreographer.getInstance().postFrameCallbackDelayed(
+                frameTimeNanos -> {
+                    if (mAppearAnimator == cachedAnimator) {
+                        mAppearAnimator.start();
+                    }
+                }, delay);
     }
 
     private int getCujType(boolean isAppearing) {
@@ -640,11 +654,6 @@
         mBackgroundNormal.setRadius(topRadius, bottomRadius);
     }
 
-    @Override
-    protected void setBackgroundTop(int backgroundTop) {
-        mBackgroundNormal.setBackgroundTop(backgroundTop);
-    }
-
     protected abstract View getContentView();
 
     public int calculateBgColor() {
@@ -808,6 +817,22 @@
         mOnDetachResetRoundness.add(sourceType);
     }
 
+    @Override
+    public void dump(PrintWriter pwOriginal, String[] args) {
+        IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
+        super.dump(pw, args);
+        if (DUMP_VERBOSE) {
+            DumpUtilsKt.withIncreasedIndent(pw, () -> {
+                pw.println("mBackgroundNormal: " + mBackgroundNormal);
+                if (mBackgroundNormal != null) {
+                    DumpUtilsKt.withIncreasedIndent(pw, () -> {
+                        mBackgroundNormal.dump(pw, args);
+                    });
+                }
+            });
+        }
+    }
+
     public interface OnActivatedListener {
         void onActivated(ActivatableNotificationView view);
         void onActivationReset(ActivatableNotificationView view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java
index c496102..b084a76 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java
@@ -109,7 +109,7 @@
                 return true;
             }
             if (ev.getAction() == MotionEvent.ACTION_UP) {
-                mView.setLastActionUpTime(SystemClock.uptimeMillis());
+                mView.setLastActionUpTime(ev.getEventTime());
             }
             // With a11y, just do nothing.
             if (mAccessibilityManager.isTouchExplorationEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 9f50aef..64c1a59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -79,6 +79,8 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -177,6 +179,7 @@
     private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
     private Optional<BubblesManager> mBubblesManagerOptional;
     private MetricsLogger mMetricsLogger;
+    private FeatureFlags mFeatureFlags;
     private int mIconTransformContentShift;
     private int mMaxHeadsUpHeightBeforeN;
     private int mMaxHeadsUpHeightBeforeP;
@@ -277,7 +280,7 @@
     private boolean mChildIsExpanding;
 
     private boolean mJustClicked;
-    private boolean mIconAnimationRunning;
+    private boolean mAnimationRunning;
     private boolean mShowNoBackground;
     private ExpandableNotificationRow mNotificationParent;
     private OnExpandClickListener mOnExpandClickListener;
@@ -451,10 +454,26 @@
         return mPublicLayout;
     }
 
-    public void setIconAnimationRunning(boolean running) {
-        for (NotificationContentView l : mLayouts) {
-            setIconAnimationRunning(running, l);
+    /**
+     * Sets animations running in the layouts of this row, including public, private, and children.
+     * @param running whether the animations should be started running or stopped.
+     */
+    public void setAnimationRunning(boolean running) {
+        // Sets animations running in the private/public layouts.
+        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE)) {
+            for (NotificationContentView l : mLayouts) {
+                if (l != null) {
+                    l.setContentAnimationRunning(running);
+                    setIconAnimationRunning(running, l);
+                }
+            }
+        } else {
+            for (NotificationContentView l : mLayouts) {
+                setIconAnimationRunning(running, l);
+            }
         }
+        // For groups summaries with children, we want to set the children containers
+        // animating as well.
         if (mIsSummaryWithChildren) {
             NotificationViewWrapper viewWrapper = mChildrenContainer.getNotificationViewWrapper();
             if (viewWrapper != null) {
@@ -468,12 +487,18 @@
                     mChildrenContainer.getAttachedChildren();
             for (int i = 0; i < notificationChildren.size(); i++) {
                 ExpandableNotificationRow child = notificationChildren.get(i);
-                child.setIconAnimationRunning(running);
+                child.setAnimationRunning(running);
             }
         }
-        mIconAnimationRunning = running;
+        mAnimationRunning = running;
     }
 
+    /**
+     * Starts or stops animations of the icons in all potential content views (regardless of
+     * whether they're contracted, expanded, etc).
+     *
+     * @param running whether to start or stop the icon's animation.
+     */
     private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
         if (layout != null) {
             View contractedChild = layout.getContractedChild();
@@ -485,16 +510,29 @@
         }
     }
 
+    /**
+     * Starts or stops animations of the icon in the provided view's icon and right icon.
+     *
+     * @param running whether to start or stop the icon's animation.
+     * @param child   the view with the icon to start or stop.
+     */
     private void setIconAnimationRunningForChild(boolean running, View child) {
         if (child != null) {
             ImageView icon = child.findViewById(com.android.internal.R.id.icon);
-            setIconRunning(icon, running);
+            setImageViewAnimationRunning(icon, running);
             ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon);
-            setIconRunning(rightIcon, running);
+            setImageViewAnimationRunning(rightIcon, running);
         }
     }
 
-    private void setIconRunning(ImageView imageView, boolean running) {
+    /**
+     * Starts or stops the animation of a provided image view if it's an AnimationDrawable or an
+     * AnimatedVectorDrawable.
+     *
+     * @param imageView the image view on which to start/stop animation.
+     * @param running   whether to start or stop the view's animation.
+     */
+    private void setImageViewAnimationRunning(ImageView imageView, boolean running) {
         if (imageView != null) {
             Drawable drawable = imageView.getDrawable();
             if (drawable instanceof AnimationDrawable) {
@@ -561,8 +599,8 @@
             mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation());
             mChildrenContainer.onNotificationUpdated();
         }
-        if (mIconAnimationRunning) {
-            setIconAnimationRunning(true);
+        if (mAnimationRunning) {
+            setAnimationRunning(true);
         }
         if (mLastChronometerRunning) {
             setChronometerRunning(true);
@@ -1038,7 +1076,7 @@
             notifyHeightChanged(false /* needsAnimation */);
         }
         if (pinned) {
-            setIconAnimationRunning(true);
+            setAnimationRunning(true);
             mExpandedWhenPinned = false;
         } else if (mExpandedWhenPinned) {
             setUserExpanded(true);
@@ -1158,6 +1196,22 @@
         return getShowingLayout().getVisibleWrapper();
     }
 
+    /**
+     * @return whether the notification row is long clickable or not.
+     */
+    public boolean isNotificationRowLongClickable() {
+        if (mLongPressListener == null) {
+            return false;
+        }
+
+        if (!areGutsExposed()) { // guts is not opened
+            return true;
+        }
+
+        // if it is leave behind, it shouldn't be long clickable.
+        return !isGutsLeaveBehind();
+    }
+
     public void setLongPressListener(LongPressListener longPressListener) {
         mLongPressListener = longPressListener;
     }
@@ -1627,6 +1681,11 @@
         );
     }
 
+    /**
+     * Constructs an ExpandableNotificationRow.
+     * @param context context passed to image resolver
+     * @param attrs attributes used to initialize parent view
+     */
     public ExpandableNotificationRow(Context context, AttributeSet attrs) {
         super(context, attrs);
         mImageResolver = new NotificationInlineImageResolver(context,
@@ -1662,7 +1721,8 @@
             NotificationGutsManager gutsManager,
             MetricsLogger metricsLogger,
             SmartReplyConstants smartReplyConstants,
-            SmartReplyController smartReplyController) {
+            SmartReplyController smartReplyController,
+            FeatureFlags featureFlags) {
         mEntry = entry;
         mAppName = appName;
         if (mMenuRow == null) {
@@ -1697,6 +1757,7 @@
         mBubblesManagerOptional = bubblesManagerOptional;
         mNotificationGutsManager = gutsManager;
         mMetricsLogger = metricsLogger;
+        mFeatureFlags = featureFlags;
     }
 
     private void initDimens() {
@@ -2902,6 +2963,10 @@
         return (mGuts != null && mGuts.isExposed());
     }
 
+    private boolean isGutsLeaveBehind() {
+        return (mGuts != null && mGuts.isLeavebehind());
+    }
+
     @Override
     public boolean isContentExpandable() {
         if (mIsSummaryWithChildren && !shouldShowPublic()) {
@@ -3349,7 +3414,12 @@
     @Override
     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfoInternal(info);
-        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
+        final boolean isLongClickable = isNotificationRowLongClickable();
+        if (isLongClickable) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
+        }
+        info.setLongClickable(isLongClickable);
+
         if (canViewBeDismissed() && !mIsSnoozed) {
             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
         }
@@ -3588,11 +3658,13 @@
     @VisibleForTesting
     protected void setPrivateLayout(NotificationContentView privateLayout) {
         mPrivateLayout = privateLayout;
+        mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
     }
 
     @VisibleForTesting
     protected void setPublicLayout(NotificationContentView publicLayout) {
         mPublicLayout = publicLayout;
+        mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index d113860..bb92dfc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -219,7 +219,8 @@
                 mNotificationGutsManager,
                 mMetricsLogger,
                 mSmartReplyConstants,
-                mSmartReplyController
+                mSmartReplyController,
+                mFeatureFlags
         );
         mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
         if (mAllowLongPress) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 2041245..197caa2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -24,12 +24,16 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.util.AttributeSet;
+import android.util.IndentingPrintWriter;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
+import com.android.systemui.util.DumpUtilsKt;
+
+import java.io.PrintWriter;
 
 /**
  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
@@ -43,7 +47,6 @@
     private float mOutlineAlpha = -1f;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    private int mBackgroundTop;
 
     /**
      * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
@@ -59,7 +62,7 @@
                 // Only when translating just the contents, does the outline need to be shifted.
                 int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0;
                 int left = Math.max(translation, 0);
-                int top = mClipTopAmount + mBackgroundTop;
+                int top = mClipTopAmount;
                 int right = getWidth() + Math.min(translation, 0);
                 int bottom = Math.max(getActualHeight() - mClipBottomAmount, top);
                 outline.setRect(left, top, right, bottom);
@@ -92,7 +95,7 @@
                     ? (int) getTranslation() : 0;
             int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f);
             left = Math.max(translation, 0) - halfExtraWidth;
-            top = mClipTopAmount + mBackgroundTop;
+            top = mClipTopAmount;
             right = getWidth() + halfExtraWidth + Math.min(translation, 0);
             // If the top is rounded we want the bottom to be at most at the top roundness, in order
             // to avoid the shadow changing when scrolling up.
@@ -228,13 +231,6 @@
         super.applyRoundnessAndInvalidate();
     }
 
-    protected void setBackgroundTop(int backgroundTop) {
-        if (mBackgroundTop != backgroundTop) {
-            mBackgroundTop = backgroundTop;
-            invalidateOutline();
-        }
-    }
-
     public void onDensityOrFontScaleChanged() {
         initDimens();
         applyRoundnessAndInvalidate();
@@ -350,4 +346,18 @@
     public Path getCustomClipPath(View child) {
         return null;
     }
+
+    @Override
+    public void dump(PrintWriter pwOriginal, String[] args) {
+        IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
+        super.dump(pw, args);
+        DumpUtilsKt.withIncreasedIndent(pw, () -> {
+            pw.println("Roundness: " + getRoundableState().debugString());
+            if (DUMP_VERBOSE) {
+                pw.println("mCustomOutline: " + mCustomOutline + " mOutlineRect: " + mOutlineRect);
+                pw.println("mOutlineAlpha: " + mOutlineAlpha);
+                pw.println("mAlwaysRoundBothCorners: " + mAlwaysRoundBothCorners);
+            }
+        });
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 955d7c1..25c7264 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -51,6 +51,8 @@
  */
 public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
     private static final String TAG = "ExpandableView";
+    /** whether the dump() for this class should include verbose details */
+    protected static final boolean DUMP_VERBOSE = false;
 
     private RoundableState mRoundableState = null;
     protected OnHeightChangedListener mOnHeightChangedListener;
@@ -825,6 +827,14 @@
                 viewState.dump(pw, args);
                 pw.println();
             }
+            if (DUMP_VERBOSE) {
+                pw.println("mClipTopAmount: " + mClipTopAmount);
+                pw.println("mClipBottomAmount " + mClipBottomAmount);
+                pw.println("mClipToActualHeight: " + mClipToActualHeight);
+                pw.println("mExtraWidthForClipping: " + mExtraWidthForClipping);
+                pw.println("mMinimumHeightForClipping: " + mMinimumHeightForClipping);
+                pw.println("getClipBounds(): " + getClipBounds());
+            }
         });
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
index 49dc655..49f17b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
@@ -16,15 +16,22 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.IndentingPrintWriter;
 import android.view.View;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 
+import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -41,6 +48,11 @@
     private String mManageNotificationText;
     private String mManageNotificationHistoryText;
 
+    // Footer label
+    private TextView mSeenNotifsFooterTextView;
+    private @StringRes int mSeenNotifsFilteredText;
+    private int mUnlockIconSize;
+
     public FooterView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -73,8 +85,40 @@
         super.onFinishInflate();
         mClearAllButton = (FooterViewButton) findSecondaryView();
         mManageButton = findViewById(R.id.manage_text);
+        mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer);
         updateResources();
         updateText();
+        updateColors();
+    }
+
+    public void setFooterLabelTextAndIcon(@StringRes int text, @DrawableRes int icon) {
+        mSeenNotifsFilteredText = text;
+        if (mSeenNotifsFilteredText != 0) {
+            mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
+        } else {
+            mSeenNotifsFooterTextView.setText(null);
+        }
+        Drawable drawable;
+        if (icon == 0) {
+            drawable = null;
+        } else {
+            drawable = getResources().getDrawable(icon);
+            drawable.setBounds(0, 0, mUnlockIconSize, mUnlockIconSize);
+        }
+        mSeenNotifsFooterTextView.setCompoundDrawablesRelative(drawable, null, null, null);
+        updateFooterVisibilityMode();
+    }
+
+    private void updateFooterVisibilityMode() {
+        if (mSeenNotifsFilteredText != 0) {
+            mManageButton.setVisibility(View.GONE);
+            mClearAllButton.setVisibility(View.GONE);
+            mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
+        } else {
+            mManageButton.setVisibility(View.VISIBLE);
+            mClearAllButton.setVisibility(View.VISIBLE);
+            mSeenNotifsFooterTextView.setVisibility(View.GONE);
+        }
     }
 
     public void setManageButtonClickListener(OnClickListener listener) {
@@ -135,12 +179,19 @@
         mClearAllButton.setTextColor(textColor);
         mManageButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background));
         mManageButton.setTextColor(textColor);
+        final @ColorInt int labelTextColor =
+                Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary);
+        mSeenNotifsFooterTextView.setTextColor(labelTextColor);
+        mSeenNotifsFooterTextView.setCompoundDrawableTintList(
+                ColorStateList.valueOf(labelTextColor));
     }
 
     private void updateResources() {
         mManageNotificationText = getContext().getString(R.string.manage_notifications_text);
         mManageNotificationHistoryText = getContext()
                 .getString(R.string.manage_notifications_history_text);
+        mUnlockIconSize = getResources()
+                .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index 5171569..da8d2d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -28,12 +28,16 @@
 import android.view.View;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 
+import java.io.PrintWriter;
+import java.util.Arrays;
+
 /**
  * A view that can be used for both the dimmed and normal background of an notification.
  */
-public class NotificationBackgroundView extends View {
+public class NotificationBackgroundView extends View implements Dumpable {
 
     private final boolean mDontModifyCorners;
     private Drawable mBackground;
@@ -42,7 +46,6 @@
     private int mTintColor;
     private final float[] mCornerRadii = new float[8];
     private boolean mBottomIsRounded;
-    private int mBackgroundTop;
     private boolean mBottomAmountClips = true;
     private int mActualHeight = -1;
     private int mActualWidth = -1;
@@ -60,8 +63,7 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        if (mClipTopAmount + mClipBottomAmount < getActualHeight() - mBackgroundTop
-                || mExpandAnimationRunning) {
+        if (mClipTopAmount + mClipBottomAmount < getActualHeight() || mExpandAnimationRunning) {
             canvas.save();
             if (!mExpandAnimationRunning) {
                 canvas.clipRect(0, mClipTopAmount, getWidth(),
@@ -74,7 +76,7 @@
 
     private void draw(Canvas canvas, Drawable drawable) {
         if (drawable != null) {
-            int top = mBackgroundTop;
+            int top = 0;
             int bottom = getActualHeight();
             if (mBottomIsRounded
                     && mBottomAmountClips
@@ -261,11 +263,6 @@
         }
     }
 
-    public void setBackgroundTop(int backgroundTop) {
-        mBackgroundTop = backgroundTop;
-        invalidate();
-    }
-
     /** Set the current expand animation size. */
     public void setExpandAnimationSize(int width, int height) {
         mExpandAnimationHeight = height;
@@ -291,4 +288,16 @@
     public void setPressedAllowed(boolean allowed) {
         mIsPressedAllowed = allowed;
     }
+
+    @Override
+    public void dump(PrintWriter pw, String[] args) {
+        pw.println("mDontModifyCorners: " + mDontModifyCorners);
+        pw.println("mClipTopAmount: " + mClipTopAmount);
+        pw.println("mClipBottomAmount: " + mClipBottomAmount);
+        pw.println("mCornerRadii: " + Arrays.toString(mCornerRadii));
+        pw.println("mBottomIsRounded: " + mBottomIsRounded);
+        pw.println("mBottomAmountClips: " + mBottomAmountClips);
+        pw.println("mActualWidth: " + mActualWidth);
+        pw.println("mActualHeight: " + mActualHeight);
+    }
 }
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/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 7c21ffb..d93c12b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -184,6 +184,8 @@
     private boolean mRemoteInputVisible;
     private int mUnrestrictedContentHeight;
 
+    private boolean mContentAnimating;
+
     public NotificationContentView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mHybridGroupManager = new HybridGroupManager(getContext());
@@ -2129,8 +2131,49 @@
         return false;
     }
 
+    /**
+     * Starts and stops animations in the underlying views.
+     * Avoids restarting the animations by checking whether they're already running first.
+     * Return value is used for testing.
+     *
+     * @param running whether to start animations running, or stop them.
+     * @return true if the state of animations changed.
+     */
+    public boolean setContentAnimationRunning(boolean running) {
+        boolean stateChangeRequired = (running != mContentAnimating);
+        if (stateChangeRequired) {
+            // Starts or stops the animations in the potential views.
+            if (mContractedWrapper != null) {
+                mContractedWrapper.setAnimationsRunning(running);
+            }
+            if (mExpandedWrapper != null) {
+                mExpandedWrapper.setAnimationsRunning(running);
+            }
+            if (mHeadsUpWrapper != null) {
+                mHeadsUpWrapper.setAnimationsRunning(running);
+            }
+            // Updates the state tracker.
+            mContentAnimating = running;
+            return true;
+        }
+        return false;
+    }
+
     private static class RemoteInputViewData {
         @Nullable RemoteInputView mView;
         @Nullable RemoteInputViewController mController;
     }
+
+    @VisibleForTesting
+    protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) {
+        mContractedWrapper = contractedWrapper;
+    }
+    @VisibleForTesting
+    protected void setExpandedWrapper(NotificationViewWrapper expandedWrapper) {
+        mExpandedWrapper = expandedWrapper;
+    }
+    @VisibleForTesting
+    protected void setHeadsUpWrapper(NotificationViewWrapper headsUpWrapper) {
+        mHeadsUpWrapper = headsUpWrapper;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index ea12b82..06d4080 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -44,12 +44,10 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.settingslib.notification.ConversationIconFactory;
-import com.android.systemui.Dependency;
-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.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -65,28 +63,29 @@
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener;
 import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
-import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.wmshell.BubblesManager;
 
-import java.io.PrintWriter;
 import java.util.Optional;
 
+import javax.inject.Inject;
+
 import dagger.Lazy;
 
 /**
  * Handles various NotificationGuts related tasks, such as binding guts to a row, opening and
  * closing guts, and keeping track of the currently exposed notification guts.
  */
+@SysUISingleton
 public class NotificationGutsManager implements NotifGutsViewManager {
     private static final String TAG = "NotificationGutsManager";
 
     // Must match constant in Settings. Used to highlight preferences when linking to Settings.
     private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
 
-    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+    private final MetricsLogger mMetricsLogger;
     private final Context mContext;
     private final AccessibilityManager mAccessibilityManager;
     private final HighPriorityProvider mHighPriorityProvider;
@@ -94,12 +93,9 @@
     private final OnUserInteractionCallback mOnUserInteractionCallback;
 
     // Dependencies:
-    private final NotificationLockscreenUserManager mLockscreenUserManager =
-            Dependency.get(NotificationLockscreenUserManager.class);
-    private final StatusBarStateController mStatusBarStateController =
-            Dependency.get(StatusBarStateController.class);
-    private final DeviceProvisionedController mDeviceProvisionedController =
-            Dependency.get(DeviceProvisionedController.class);
+    private final NotificationLockscreenUserManager mLockscreenUserManager;
+    private final StatusBarStateController mStatusBarStateController;
+    private final DeviceProvisionedController mDeviceProvisionedController;
     private final AssistantFeedbackController mAssistantFeedbackController;
 
     // which notification is currently being longpress-examined by the user
@@ -124,9 +120,7 @@
     private final ShadeController mShadeController;
     private NotifGutsViewListener mGutsListener;
 
-    /**
-     * Injected constructor. See {@link NotificationsModule}.
-     */
+    @Inject
     public NotificationGutsManager(Context context,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
             @Main Handler mainHandler,
@@ -143,7 +137,11 @@
             Optional<BubblesManager> bubblesManagerOptional,
             UiEventLogger uiEventLogger,
             OnUserInteractionCallback onUserInteractionCallback,
-            ShadeController shadeController) {
+            ShadeController shadeController,
+            NotificationLockscreenUserManager notificationLockscreenUserManager,
+            StatusBarStateController statusBarStateController,
+            DeviceProvisionedController deviceProvisionedController,
+            MetricsLogger metricsLogger) {
         mContext = context;
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
         mMainHandler = mainHandler;
@@ -161,6 +159,10 @@
         mUiEventLogger = uiEventLogger;
         mOnUserInteractionCallback = onUserInteractionCallback;
         mShadeController = shadeController;
+        mLockscreenUserManager = notificationLockscreenUserManager;
+        mStatusBarStateController = statusBarStateController;
+        mDeviceProvisionedController = deviceProvisionedController;
+        mMetricsLogger = metricsLogger;
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter,
@@ -372,7 +374,8 @@
                 mDeviceProvisionedController.isDeviceProvisioned(),
                 row.getIsNonblockable(),
                 mHighPriorityProvider.isHighPriority(row.getEntry()),
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
     }
 
     /**
@@ -583,7 +586,9 @@
         }
 
         final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
-        view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        if (row.isNotificationRowLongClickable()) {
+            view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        }
         if (row.areGutsExposed()) {
             closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
                     true /* removeControls */, -1 /* x */, -1 /* y */,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index ea0060a..8a50f2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -204,10 +204,11 @@
             boolean isDeviceProvisioned,
             boolean isNonblockable,
             boolean wasShownHighPriority,
-            AssistantFeedbackController assistantFeedbackController)
+            AssistantFeedbackController assistantFeedbackController,
+            MetricsLogger metricsLogger)
             throws RemoteException {
         mINotificationManager = iNotificationManager;
-        mMetricsLogger = Dependency.get(MetricsLogger.class);
+        mMetricsLogger = metricsLogger;
         mOnUserInteractionCallback = onUserInteractionCallback;
         mChannelEditorDialogController = channelEditorDialogController;
         mAssistantFeedbackController = assistantFeedbackController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
index 8732696..175ba15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
@@ -18,11 +18,15 @@
 
 import android.app.Notification;
 import android.content.Context;
+import android.graphics.drawable.AnimatedImageDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
 import android.view.View;
 
+import com.android.internal.R;
+import com.android.internal.widget.BigPictureNotificationImageView;
 import com.android.systemui.statusbar.notification.ImageTransformState;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
@@ -31,6 +35,8 @@
  */
 public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper {
 
+    private BigPictureNotificationImageView mImageView;
+
     protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view,
             ExpandableNotificationRow row) {
         super(ctx, view, row);
@@ -39,9 +45,14 @@
     @Override
     public void onContentUpdated(ExpandableNotificationRow row) {
         super.onContentUpdated(row);
+        resolveViews();
         updateImageTag(row.getEntry().getSbn());
     }
 
+    private void resolveViews() {
+        mImageView = mView.findViewById(R.id.big_picture);
+    }
+
     private void updateImageTag(StatusBarNotification sbn) {
         final Bundle extras = sbn.getNotification().extras;
         Icon bigLargeIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG, Icon.class);
@@ -54,4 +65,25 @@
             mRightIcon.setTag(ImageTransformState.ICON_TAG, getLargeIcon(sbn.getNotification()));
         }
     }
+
+    /**
+     * Starts or stops the animations in any drawables contained in this BigPicture Notification.
+     *
+     * @param running Whether the animations should be set to run.
+     */
+    @Override
+    public void setAnimationsRunning(boolean running) {
+        if (mImageView == null) {
+            return;
+        }
+        Drawable d = mImageView.getDrawable();
+        if (d instanceof AnimatedImageDrawable) {
+            AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d;
+            if (running) {
+                animatedImageDrawable.start();
+            } else {
+                animatedImageDrawable.stop();
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
index e136055..10753f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
@@ -17,16 +17,20 @@
 package com.android.systemui.statusbar.notification.row.wrapper
 
 import android.content.Context
+import android.graphics.drawable.AnimatedImageDrawable
 import android.view.View
 import android.view.ViewGroup
 import com.android.internal.widget.CachingIconView
 import com.android.internal.widget.ConversationLayout
+import com.android.internal.widget.MessagingGroup
+import com.android.internal.widget.MessagingImageMessage
 import com.android.internal.widget.MessagingLinearLayout
 import com.android.systemui.R
 import com.android.systemui.statusbar.notification.NotificationFadeAware
 import com.android.systemui.statusbar.notification.NotificationUtils
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform
+import com.android.systemui.util.children
 
 /**
  * Wraps a notification containing a conversation template
@@ -49,6 +53,7 @@
     private lateinit var expandBtn: View
     private lateinit var expandBtnContainer: View
     private lateinit var imageMessageContainer: ViewGroup
+    private lateinit var messageContainers: ArrayList<MessagingGroup>
     private lateinit var messagingLinearLayout: MessagingLinearLayout
     private lateinit var conversationTitleView: View
     private lateinit var importanceRing: View
@@ -60,6 +65,7 @@
     private fun resolveViews() {
         messagingLinearLayout = conversationLayout.messagingLinearLayout
         imageMessageContainer = conversationLayout.imageMessageContainer
+        messageContainers = conversationLayout.messagingGroups
         with(conversationLayout) {
             conversationIconContainer =
                     requireViewById(com.android.internal.R.id.conversation_icon_container)
@@ -146,4 +152,26 @@
         NotificationFadeAware.setLayerTypeForFaded(expandBtn, faded)
         NotificationFadeAware.setLayerTypeForFaded(conversationIconContainer, faded)
     }
+
+    // Starts or stops the animations in any drawables contained in this Conversation Notification.
+    override fun setAnimationsRunning(running: Boolean) {
+        // We apply to both the child message containers in a conversation group,
+        // and the top level image message container.
+        val containers = messageContainers.asSequence().map { it.messageContainer } +
+                sequenceOf(imageMessageContainer)
+        val drawables =
+                containers
+                        .flatMap { it.children }
+                        .mapNotNull { child ->
+                            (child as? MessagingImageMessage)?.let { imageMessage ->
+                                imageMessage.drawable as? AnimatedImageDrawable
+                            }
+                        }
+        drawables.toSet().forEach {
+            when {
+                running -> it.start()
+                !running -> it.stop()
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java
index c587ce0..4592fde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java
@@ -17,9 +17,13 @@
 package com.android.systemui.statusbar.notification.row.wrapper;
 
 import android.content.Context;
+import android.graphics.drawable.AnimatedImageDrawable;
+import android.graphics.drawable.Drawable;
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.internal.widget.MessagingGroup;
+import com.android.internal.widget.MessagingImageMessage;
 import com.android.internal.widget.MessagingLayout;
 import com.android.internal.widget.MessagingLinearLayout;
 import com.android.systemui.R;
@@ -127,4 +131,40 @@
         }
         return super.getMinLayoutHeight();
     }
+
+    /**
+     * Starts or stops the animations in any drawables contained in this Messaging Notification.
+     *
+     * @param running Whether the animations should be set to run.
+     */
+    @Override
+    public void setAnimationsRunning(boolean running) {
+        if (mMessagingLayout == null) {
+            return;
+        }
+
+        for (MessagingGroup group : mMessagingLayout.getMessagingGroups()) {
+            for (int i = 0; i < group.getMessageContainer().getChildCount(); i++) {
+                View view = group.getMessageContainer().getChildAt(i);
+                // We only need to set animations in MessagingImageMessages.
+                if (!(view instanceof MessagingImageMessage)) {
+                    continue;
+                }
+                MessagingImageMessage imageMessage =
+                        (com.android.internal.widget.MessagingImageMessage) view;
+
+                // If the drawable isn't an AnimatedImageDrawable, we can't set it to animate.
+                Drawable d = imageMessage.getDrawable();
+                if (!(d instanceof AnimatedImageDrawable)) {
+                    continue;
+                }
+                AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d;
+                if (running) {
+                    animatedImageDrawable.start();
+                } else {
+                    animatedImageDrawable.stop();
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index 1c22f09..ff5b9cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -403,4 +403,12 @@
         NotificationFadeAware.setLayerTypeForFaded(getIcon(), faded);
         NotificationFadeAware.setLayerTypeForFaded(getExpandButton(), faded);
     }
+
+    /**
+     * Starts or stops the animations in any drawables contained in this Notification.
+     *
+     * @param running Whether the animations should be set to run.
+     */
+    public void setAnimationsRunning(boolean running) {
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 2f54325..d2087ba6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
-import static android.os.Trace.TRACE_TAG_ALWAYS;
+import static android.os.Trace.TRACE_TAG_APP;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
@@ -147,7 +147,7 @@
     private boolean mShadeNeedsToClose = false;
 
     @VisibleForTesting
-    static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+    static final float RUBBER_BAND_FACTOR_NORMAL = 0.1f;
     private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
     private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
     /**
@@ -538,6 +538,7 @@
     private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private boolean mShouldUseSplitNotificationShade;
+    private boolean mHasFilteredOutSeenNotifications;
 
     private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener =
             new ExpandableView.OnHeightChangedListener() {
@@ -684,6 +685,10 @@
         updateFooter();
     }
 
+    void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
+        mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications;
+    }
+
     @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void updateFooter() {
@@ -1116,7 +1121,7 @@
 
     @Override
     public void requestLayout() {
-        Trace.instant(TRACE_TAG_ALWAYS, "NotificationStackScrollLayout#requestLayout");
+        Trace.instant(TRACE_TAG_APP, "NotificationStackScrollLayout#requestLayout");
         super.requestLayout();
     }
 
@@ -1811,9 +1816,7 @@
     @Override
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mBottomInset = insets.getSystemWindowInsetBottom()
-                + insets.getInsets(WindowInsets.Type.ime()).bottom;
-
+        mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom;
         mWaterfallTopInset = 0;
         final DisplayCutout cutout = insets.getDisplayCutout();
         if (cutout != null) {
@@ -2262,7 +2265,11 @@
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private int getImeInset() {
-        return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight()));
+        // The NotificationStackScrollLayout does not extend all the way to the bottom of the
+        // display. Therefore, subtract that space from the mBottomInset, in order to only include
+        // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout.
+        return Math.max(0, mBottomInset
+                - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1]));
     }
 
     /**
@@ -2970,12 +2977,19 @@
             childInGroup = (ExpandableNotificationRow) requestedView;
             requestedView = requestedRow = childInGroup.getNotificationParent();
         }
-        int position = 0;
+        final float scrimTopPadding = mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings;
+        int position = (int) scrimTopPadding;
+        int visibleIndex = -1;
+        ExpandableView lastVisibleChild = null;
         for (int i = 0; i < getChildCount(); i++) {
             ExpandableView child = getChildAtIndex(i);
             boolean notGone = child.getVisibility() != View.GONE;
+            if (notGone) visibleIndex++;
             if (notGone && !child.hasNoContentHeight()) {
-                if (position != 0) {
+                if (position != scrimTopPadding) {
+                    if (lastVisibleChild != null) {
+                        position += calculateGapHeight(lastVisibleChild, child, visibleIndex);
+                    }
                     position += mPaddingBetweenElements;
                 }
             }
@@ -2987,6 +3001,7 @@
             }
             if (notGone) {
                 position += getIntrinsicHeight(child);
+                lastVisibleChild = child;
             }
         }
         return 0;
@@ -3122,7 +3137,7 @@
     private void updateAnimationState(boolean running, View child) {
         if (child instanceof ExpandableNotificationRow) {
             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-            row.setIconAnimationRunning(running);
+            row.setAnimationRunning(running);
         }
     }
 
@@ -4602,13 +4617,12 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    void updateEmptyShadeView(
-            boolean visible, boolean areNotificationsHiddenInShade, boolean areSeenNotifsFiltered) {
+    void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
         mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
 
         if (areNotificationsHiddenInShade) {
             updateEmptyShadeView(R.string.dnd_suppressing_shade_text, 0, 0);
-        } else if (areSeenNotifsFiltered) {
+        } else if (mHasFilteredOutSeenNotifications) {
             updateEmptyShadeView(
                     R.string.no_unseen_notif_text,
                     R.string.unlock_to_see_notif_text,
@@ -4647,13 +4661,20 @@
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) {
-        if (mFooterView == null) {
+        if (mFooterView == null || mNotificationStackSizeCalculator == null) {
             return;
         }
         boolean animate = mIsExpanded && mAnimationsEnabled;
         mFooterView.setVisible(visible, animate);
         mFooterView.setSecondaryVisible(showDismissView, animate);
         mFooterView.showHistory(showHistory);
+        if (mHasFilteredOutSeenNotifications) {
+            mFooterView.setFooterLabelTextAndIcon(
+                    R.string.unlock_to_see_notif_text,
+                    R.drawable.ic_friction_lock_closed);
+        } else {
+            mFooterView.setFooterLabelTextAndIcon(0, 0);
+        }
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -5193,6 +5214,7 @@
             println(pw, "intrinsicPadding", mIntrinsicPadding);
             println(pw, "topPadding", mTopPadding);
             println(pw, "bottomPadding", mBottomPadding);
+            mNotificationStackSizeCalculator.dump(pw, args);
         });
         pw.println();
         pw.println("Contents:");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5891948..14b0763 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -120,6 +120,7 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.Compile;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -189,6 +190,7 @@
     private final FeatureFlags mFeatureFlags;
     private final boolean mUseRoundnessSourceTypes;
     private final NotificationTargetsHelper mNotificationTargetsHelper;
+    private final SecureSettings mSecureSettings;
 
     private View mLongPressedView;
 
@@ -207,7 +209,11 @@
                 public void onViewAttachedToWindow(View v) {
                     mConfigurationController.addCallback(mConfigurationListener);
                     mZenModeController.addCallback(mZenModeControllerCallback);
-                    mBarState = mStatusBarStateController.getState();
+                    final int newBarState = mStatusBarStateController.getState();
+                    if (newBarState != mBarState) {
+                        mStateListener.onStateChanged(newBarState);
+                        mStateListener.onStatePostChange();
+                    }
                     mStatusBarStateController.addCallback(
                             mStateListener, SysuiStatusBarStateController.RANK_STACK_SCROLLER);
                 }
@@ -667,7 +673,8 @@
             NotificationStackScrollLogger logger,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             FeatureFlags featureFlags,
-            NotificationTargetsHelper notificationTargetsHelper) {
+            NotificationTargetsHelper notificationTargetsHelper,
+            SecureSettings secureSettings) {
         mStackStateLogger = stackLogger;
         mLogger = logger;
         mAllowLongPress = allowLongPress;
@@ -709,6 +716,7 @@
         mFeatureFlags = featureFlags;
         mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
         mNotificationTargetsHelper = notificationTargetsHelper;
+        mSecureSettings = secureSettings;
         updateResources();
     }
 
@@ -1015,8 +1023,7 @@
                 Log.wtf(TAG, "isHistoryEnabled failed to initialize its value");
                 return false;
             }
-            mHistoryEnabled = historyEnabled = Settings.Secure.getIntForUser(
-                    mView.getContext().getContentResolver(),
+            mHistoryEnabled = historyEnabled = mSecureSettings.getIntForUser(
                     Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
                     0,
                     UserHandle.USER_CURRENT) == 1;
@@ -1242,11 +1249,7 @@
                 // For more details, see: b/228790482
                 && !isInTransitionToKeyguard();
 
-        mView.updateEmptyShadeView(
-                shouldShow,
-                mZenModeController.areNotificationsHiddenInShade(),
-                mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()
-                        && mSeenNotificationsProvider.getHasFilteredOutSeenNotifications());
+        mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade());
 
         Trace.endSection();
     }
@@ -1942,6 +1945,9 @@
         @Override
         public void setNotifStats(@NonNull NotifStats notifStats) {
             mNotifStats = notifStats;
+            mView.setHasFilteredOutSeenNotifications(
+                    mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()
+                            && mSeenNotificationsProvider.getHasFilteredOutSeenNotifications());
             updateFooter();
             updateShowEmptyShadeView();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index ae854e2..25f99c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.util.Compile
 import com.android.systemui.util.children
+import java.io.PrintWriter
 import javax.inject.Inject
 import kotlin.math.max
 import kotlin.math.min
@@ -53,6 +54,8 @@
     @Main private val resources: Resources
 ) {
 
+    private lateinit var lastComputeHeightLog : String
+
     /**
      * Maximum # notifications to show on Keyguard; extras will be collapsed in an overflow shelf.
      * If there are exactly 1 + mMaxKeyguardNotifications, and they fit in the available space
@@ -114,7 +117,9 @@
         shelfIntrinsicHeight: Float
     ): Int {
         log { "\n" }
-        val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight)
+
+        val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight,
+            /* computeHeight= */ false)
 
         var maxNotifications =
             stackHeightSequence.lastIndexWhile { heightResult ->
@@ -157,18 +162,21 @@
         shelfIntrinsicHeight: Float
     ): Float {
         log { "\n" }
+        lastComputeHeightLog = ""
         val heightPerMaxNotifications =
-            computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight)
+            computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight,
+                    /* computeHeight= */ true)
 
         val (notificationsHeight, shelfHeightWithSpaceBefore) =
             heightPerMaxNotifications.elementAtOrElse(maxNotifications) {
                 heightPerMaxNotifications.last() // Height with all notifications visible.
             }
-        log {
-            "computeHeight(maxNotifications=$maxNotifications," +
+        lastComputeHeightLog += "\ncomputeHeight(maxNotifications=$maxNotifications," +
                 "shelfIntrinsicHeight=$shelfIntrinsicHeight) -> " +
                 "${notificationsHeight + shelfHeightWithSpaceBefore}" +
                 " = ($notificationsHeight + $shelfHeightWithSpaceBefore)"
+        log {
+            lastComputeHeightLog
         }
         return notificationsHeight + shelfHeightWithSpaceBefore
     }
@@ -184,7 +192,8 @@
 
     private fun computeHeightPerNotificationLimit(
         stack: NotificationStackScrollLayout,
-        shelfHeight: Float
+        shelfHeight: Float,
+        computeHeight: Boolean
     ): Sequence<StackHeight> = sequence {
         log { "computeHeightPerNotificationLimit" }
 
@@ -213,9 +222,14 @@
                             currentIndex = firstViewInShelfIndex)
                     spaceBeforeShelf + shelfHeight
                 }
+
+            val currentLog = "computeHeight | i=$i notificationsHeight=$notifications " +
+                "shelfHeightWithSpaceBefore=$shelfWithSpaceBefore"
+            if (computeHeight) {
+                lastComputeHeightLog += "\n" + currentLog
+            }
             log {
-                "i=$i notificationsHeight=$notifications " +
-                    "shelfHeightWithSpaceBefore=$shelfWithSpaceBefore"
+                currentLog
             }
             yield(
                 StackHeight(
@@ -260,6 +274,10 @@
         return size
     }
 
+    fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("NotificationStackSizeCalculator lastComputeHeightLog = $lastComputeHeightLog")
+    }
+
     private fun ExpandableView.isShowable(onLockscreen: Boolean): Boolean {
         if (visibility == GONE || hasNoContentHeight()) return false
         if (onLockscreen) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index aaf9300..c6f56d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -251,13 +251,13 @@
                 || (isFastNonDismissGesture && isAbleToShowMenu);
         int menuSnapTarget = menuRow.getMenuSnapTarget();
         boolean isNonFalseMenuRevealingGesture =
-                !isFalseGesture() && isMenuRevealingGestureAwayFromMenu;
+                isMenuRevealingGestureAwayFromMenu && !isFalseGesture();
         if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture)
                 && menuSnapTarget != 0) {
             // Menu has not been snapped to previously and this is menu revealing gesture
             snapOpen(animView, menuSnapTarget, velocity);
             menuRow.onSnapOpen();
-        } else if (isDismissGesture(ev) && !gestureTowardsMenu) {
+        } else if (isDismissGesture && !gestureTowardsMenu) {
             dismiss(animView, velocity);
             menuRow.onDismiss();
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 6e63960..a425792 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -870,7 +870,8 @@
         }
 
         for (int i = childCount - 1; i >= 0; i--) {
-            updateChildZValue(i, algorithmState, ambientState, i == topHunIndex);
+            childrenOnTop = updateChildZValue(i, childrenOnTop,
+                    algorithmState, ambientState, i == topHunIndex);
         }
     }
 
@@ -880,34 +881,42 @@
      *
      * @param isTopHun      Whether the child is a top HUN. A top HUN means a HUN that shows on the
      *                      vertically top of screen. Top HUNs should have drop shadows
+     * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
+     * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
+     *                      that overlaps with QQS Panel. The integer part represents the count of
+     *                      previous HUNs whose Z positions are greater than 0.
      */
-    protected void updateChildZValue(int i,
-                                     StackScrollAlgorithmState algorithmState,
-                                     AmbientState ambientState,
-                                     boolean isTopHun) {
+    protected float updateChildZValue(int i, float childrenOnTop,
+                                      StackScrollAlgorithmState algorithmState,
+                                      AmbientState ambientState,
+                                      boolean isTopHun) {
         ExpandableView child = algorithmState.visibleChildren.get(i);
         ExpandableViewState childViewState = child.getViewState();
         float baseZ = ambientState.getBaseZHeight();
 
-        // Handles HUN shadow when Shade is opened
-
         if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
                 && !ambientState.isDozingAndNotPulsing(child)
                 && childViewState.getYTranslation() < ambientState.getTopPadding()
                 + ambientState.getStackTranslation()) {
-            // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0
-            // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel.
-            // When scrolling down shade to make HUN back to in-position in Notification Panel,
-            // the overlapFraction goes to 0, and the pinned HUN's shadows hides gradually.
-            float overlap = ambientState.getTopPadding()
-                    + ambientState.getStackTranslation() - childViewState.getYTranslation();
 
-            if (childViewState.height > 0) { // To avoid 0/0 problems
-                // To prevent over-shadow
-                float overlapFraction = MathUtils.saturate(overlap / childViewState.height);
-                childViewState.setZTranslation(baseZ
-                        + overlapFraction * mPinnedZTranslationExtra);
+            if (childrenOnTop != 0.0f) {
+                // To elevate the later HUN over previous HUN when multiple HUNs exist
+                childrenOnTop++;
+            } else {
+                // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0
+                // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel.
+                // When scrolling down shade to make HUN back to in-position in Notification Panel,
+                // The overlapping fraction goes to 0, and shadows hides gradually.
+                float overlap = ambientState.getTopPadding()
+                        + ambientState.getStackTranslation() - childViewState.getYTranslation();
+                // To prevent over-shadow during HUN entry
+                childrenOnTop += Math.min(
+                        1.0f,
+                        overlap / childViewState.height
+                );
             }
+            childViewState.setZTranslation(baseZ
+                    + childrenOnTop * mPinnedZTranslationExtra);
         } else if (isTopHun) {
             // In case this is a new view that has never been measured before, we don't want to
             // elevate if we are currently expanded more than the notification
@@ -934,15 +943,15 @@
             childViewState.setZTranslation(baseZ);
         }
 
-        // Handles HUN shadow when shade is closed.
-        // While shade is closed, and during HUN's entry: headerVisibleAmount stays 0, shadow stays.
-        // While shade is closed, and HUN is showing: headerVisibleAmount stays 0, shadow stays.
+        // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays.
         // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes
         // gradually from 0 to 1, shadow hides gradually.
         // Header visibility is a deprecated concept, we are using headerVisibleAmount only because
         // this value nicely goes from 0 to 1 during the HUN-to-Shade process.
+
         childViewState.setZTranslation(childViewState.getZTranslation()
                 + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra);
+        return childrenOnTop;
     }
 
     public void setIsExpanded(boolean isExpanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
index 3ccef9d..eb81c46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
@@ -16,25 +16,35 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
+
 import android.content.Context;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.IWindowManager;
 import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.NonNull;
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.AutoHideUiElement;
 
+import java.io.PrintWriter;
+
 import javax.inject.Inject;
 
 /** A controller to control all auto-hide things. Also see {@link AutoHideUiElement}. */
 @SysUISingleton
 public class AutoHideController {
     private static final String TAG = "AutoHideController";
-    private static final long AUTO_HIDE_TIMEOUT_MS = 2250;
+    private static final int AUTO_HIDE_TIMEOUT_MS = 2250;
+    private static final int USER_AUTO_HIDE_TIMEOUT_MS = 350;
 
+    private final AccessibilityManager mAccessibilityManager;
     private final IWindowManager mWindowManagerService;
     private final Handler mHandler;
 
@@ -52,11 +62,12 @@
     };
 
     @Inject
-    public AutoHideController(Context context, @Main Handler handler,
+    public AutoHideController(Context context,
+            @Main Handler handler,
             IWindowManager iWindowManager) {
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
         mHandler = handler;
         mWindowManagerService = iWindowManager;
-
         mDisplayId = context.getDisplayId();
     }
 
@@ -138,7 +149,12 @@
 
     private void scheduleAutoHide() {
         cancelAutoHide();
-        mHandler.postDelayed(mAutoHide, AUTO_HIDE_TIMEOUT_MS);
+        mHandler.postDelayed(mAutoHide, getAutoHideTimeout());
+    }
+
+    private int getAutoHideTimeout() {
+        return mAccessibilityManager.getRecommendedTimeoutMillis(AUTO_HIDE_TIMEOUT_MS,
+                FLAG_CONTENT_CONTROLS);
     }
 
     public void checkUserAutoHide(MotionEvent event) {
@@ -160,7 +176,13 @@
 
     private void userAutoHide() {
         cancelAutoHide();
-        mHandler.postDelayed(mAutoHide, 350); // longer than app gesture -> flag clear
+        // longer than app gesture -> flag clear
+        mHandler.postDelayed(mAutoHide, getUserAutoHideTimeout());
+    }
+
+    private int getUserAutoHideTimeout() {
+        return mAccessibilityManager.getRecommendedTimeoutMillis(USER_AUTO_HIDE_TIMEOUT_MS,
+                FLAG_CONTENT_CONTROLS);
     }
 
     private boolean isAnyTransientBarShown() {
@@ -175,6 +197,15 @@
         return false;
     }
 
+    public void dump(@NonNull PrintWriter pw) {
+        pw.println("AutoHideController:");
+        pw.println("\tmAutoHideSuspended=" + mAutoHideSuspended);
+        pw.println("\tisAnyTransientBarShown=" + isAnyTransientBarShown());
+        pw.println("\thasPendingAutoHide=" + mHandler.hasCallbacks(mAutoHide));
+        pw.println("\tgetAutoHideTimeout=" + getAutoHideTimeout());
+        pw.println("\tgetUserAutoHideTimeout=" + getUserAutoHideTimeout());
+    }
+
     /**
      * Injectable factory for creating a {@link AutoHideController}.
      */
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 149ec54..f6d53b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -29,8 +29,9 @@
 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.QSHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.SettingObserver;
 import com.android.systemui.qs.external.CustomTile;
@@ -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;
@@ -73,7 +75,7 @@
     private final String mSafetySpec;
 
     protected final Context mContext;
-    protected final QSTileHost mHost;
+    protected final QSHost mHost;
     protected final Handler mHandler;
     protected final SecureSettings mSecureSettings;
     protected final AutoAddTracker mAutoTracker;
@@ -90,7 +92,7 @@
     private final ArrayList<AutoAddSetting> mAutoAddSettingList = new ArrayList<>();
 
     public AutoTileManager(Context context, AutoAddTracker.Builder autoAddTrackerBuilder,
-            QSTileHost host,
+            QSHost host,
             @Background Handler handler,
             SecureSettings secureSettings,
             HotspotController hotspotController,
@@ -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();
         }
@@ -279,7 +282,8 @@
                 public void onManagedProfileChanged() {
                     if (mManagedProfileController.hasActiveProfile()) {
                         if (mAutoTracker.isAdded(WORK)) return;
-                        mHost.addTile(WORK);
+                        final int position = mAutoTracker.getRestoredTilePosition(WORK);
+                        mHost.addTile(WORK, position);
                         mAutoTracker.setTileAdded(WORK);
                     } else {
                         if (!mAutoTracker.isAdded(WORK)) return;
@@ -322,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/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 895a293..993c4e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -18,6 +18,8 @@
 
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
 
+import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME;
+
 import android.annotation.IntDef;
 import android.content.res.Resources;
 import android.hardware.biometrics.BiometricFaceConstants;
@@ -27,7 +29,6 @@
 import android.metrics.LogMaker;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.SystemClock;
 import android.os.Trace;
 
 import androidx.annotation.Nullable;
@@ -62,6 +63,7 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.time.SystemClock;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -78,6 +80,7 @@
  */
 @SysUISingleton
 public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable {
+    private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L;
     private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000;
     private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
     private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
@@ -169,9 +172,11 @@
     private final MetricsLogger mMetricsLogger;
     private final AuthController mAuthController;
     private final StatusBarStateController mStatusBarStateController;
+    private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final LatencyTracker mLatencyTracker;
     private final VibratorHelper mVibratorHelper;
     private final BiometricUnlockLogger mLogger;
+    private final SystemClock mSystemClock;
 
     private long mLastFpFailureUptimeMillis;
     private int mNumConsecutiveFpFailures;
@@ -279,14 +284,17 @@
             SessionTracker sessionTracker,
             LatencyTracker latencyTracker,
             ScreenOffAnimationController screenOffAnimationController,
-            VibratorHelper vibrator) {
+            VibratorHelper vibrator,
+            SystemClock systemClock
+    ) {
         mPowerManager = powerManager;
         mShadeController = shadeController;
         mUpdateMonitor = keyguardUpdateMonitor;
         mUpdateMonitor.registerCallback(this);
         mMediaManager = notificationMediaManager;
         mLatencyTracker = latencyTracker;
-        wakefulnessLifecycle.addObserver(mWakefulnessObserver);
+        mWakefulnessLifecycle = wakefulnessLifecycle;
+        mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
         screenLifecycle.addObserver(mScreenObserver);
 
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -306,6 +314,7 @@
         mScreenOffAnimationController = screenOffAnimationController;
         mVibratorHelper = vibrator;
         mLogger = biometricUnlockLogger;
+        mSystemClock = systemClock;
 
         dumpManager.registerDumpable(getClass().getName(), this);
     }
@@ -374,6 +383,17 @@
     }
 
     @Override
+    public void onBiometricDetected(int userId, BiometricSourceType biometricSourceType,
+            boolean isStrongBiometric) {
+        Trace.beginSection("BiometricUnlockController#onBiometricDetected");
+        if (mUpdateMonitor.isGoingToSleep()) {
+            Trace.endSection();
+            return;
+        }
+        startWakeAndUnlock(MODE_SHOW_BOUNCER);
+    }
+
+    @Override
     public void onBiometricAuthenticated(int userId, BiometricSourceType biometricSourceType,
             boolean isStrongBiometric) {
         Trace.beginSection("BiometricUnlockController#onBiometricAuthenticated");
@@ -429,8 +449,11 @@
         Runnable wakeUp = ()-> {
             if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
                 mLogger.i("bio wakelock: Authenticated, waking up...");
-                mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
-                        "android.policy:BIOMETRIC");
+                mPowerManager.wakeUp(
+                        mSystemClock.uptimeMillis(),
+                        PowerManager.WAKE_REASON_BIOMETRIC,
+                        "android.policy:BIOMETRIC"
+                );
             }
             Trace.beginSection("release wake-and-unlock");
             releaseBiometricWakeLock();
@@ -670,7 +693,7 @@
             startWakeAndUnlock(MODE_ONLY_WAKE);
         } else if (biometricSourceType == BiometricSourceType.FINGERPRINT
                 && mUpdateMonitor.isUdfpsSupported()) {
-            long currUptimeMillis = SystemClock.uptimeMillis();
+            long currUptimeMillis = mSystemClock.uptimeMillis();
             if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) {
                 mNumConsecutiveFpFailures += 1;
             } else {
@@ -718,12 +741,26 @@
         cleanup();
     }
 
-    //these haptics are for device-entry only
+    // these haptics are for device-entry only
     private void vibrateSuccess(BiometricSourceType type) {
+        if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())
+                && lastWakeupFromPowerButtonWithinHapticThreshold()) {
+            mLogger.d("Skip auth success haptic. Power button was recently pressed.");
+            return;
+        }
         mVibratorHelper.vibrateAuthSuccess(
                 getClass().getSimpleName() + ", type =" + type + "device-entry::success");
     }
 
+    private boolean lastWakeupFromPowerButtonWithinHapticThreshold() {
+        final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason()
+                == PowerManager.WAKE_REASON_POWER_BUTTON;
+        return lastWakeupFromPowerButton
+                && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME
+                && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime()
+                < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS;
+    }
+
     private void vibrateError(BiometricSourceType type) {
         mVibratorHelper.vibrateAuthError(
                 getClass().getSimpleName() + ", type =" + type + "device-entry::error");
@@ -816,7 +853,7 @@
         if (mUpdateMonitor.isUdfpsSupported()) {
             pw.print("   mNumConsecutiveFpFailures="); pw.println(mNumConsecutiveFpFailures);
             pw.print("   time since last failure=");
-            pw.println(SystemClock.uptimeMillis() - mLastFpFailureUptimeMillis);
+            pw.println(mSystemClock.uptimeMillis() - mLastFpFailureUptimeMillis);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 4d0ad40..311728f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -372,8 +372,6 @@
     void fadeKeyguardAfterLaunchTransition(Runnable beforeFading,
             Runnable endRunnable, Runnable cancelRunnable);
 
-    void animateKeyguardUnoccluding();
-
     void startLaunchTransitionTimeout();
 
     boolean hideKeyguardImpl(boolean forceStateChange);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 2dad8e0..df850ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -31,7 +31,6 @@
 import android.os.Bundle;
 import android.os.PowerManager;
 import android.os.SystemClock;
-import android.os.UserHandle;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
@@ -54,9 +53,12 @@
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger;
@@ -99,10 +101,13 @@
     private final Optional<Vibrator> mVibratorOptional;
     private final DisableFlagsLogger mDisableFlagsLogger;
     private final int mDisplayId;
+    private final UserTracker mUserTracker;
     private final boolean mVibrateOnOpening;
     private final VibrationEffect mCameraLaunchGestureVibrationEffect;
     private final SystemBarAttributesListener mSystemBarAttributesListener;
     private final Lazy<CameraLauncher> mCameraLauncherLazy;
+    private final QuickSettingsController mQsController;
+    private final QSHost mQSHost;
 
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
@@ -110,6 +115,7 @@
     @Inject
     CentralSurfacesCommandQueueCallbacks(
             CentralSurfaces centralSurfaces,
+            QuickSettingsController quickSettingsController,
             Context context,
             @Main Resources resources,
             ShadeController shadeController,
@@ -133,8 +139,11 @@
             DisableFlagsLogger disableFlagsLogger,
             @DisplayId int displayId,
             SystemBarAttributesListener systemBarAttributesListener,
-            Lazy<CameraLauncher> cameraLauncherLazy) {
+            Lazy<CameraLauncher> cameraLauncherLazy,
+            UserTracker userTracker,
+            QSHost qsHost) {
         mCentralSurfaces = centralSurfaces;
+        mQsController = quickSettingsController;
         mContext = context;
         mShadeController = shadeController;
         mCommandQueue = commandQueue;
@@ -157,6 +166,8 @@
         mDisableFlagsLogger = disableFlagsLogger;
         mDisplayId = displayId;
         mCameraLauncherLazy = cameraLauncherLazy;
+        mUserTracker = userTracker;
+        mQSHost = qsHost;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -177,22 +188,17 @@
 
     @Override
     public void addQsTile(ComponentName tile) {
-        QSPanelController qsPanelController = mCentralSurfaces.getQSPanelController();
-        if (qsPanelController != null && qsPanelController.getHost() != null) {
-            qsPanelController.getHost().addTile(tile);
-        }
+        mQSHost.addTile(tile);
     }
 
     @Override
     public void remQsTile(ComponentName tile) {
-        QSPanelController qsPanelController = mCentralSurfaces.getQSPanelController();
-        if (qsPanelController != null && qsPanelController.getHost() != null) {
-            qsPanelController.getHost().removeTileByUser(tile);
-        }
+        mQSHost.removeTileByUser(tile);
     }
 
     @Override
     public void clickTile(ComponentName tile) {
+        // Can't inject this because it changes with the QS fragment
         QSPanelController qsPanelController = mCentralSurfaces.getQSPanelController();
         if (qsPanelController != null) {
             qsPanelController.clickTile(tile);
@@ -215,7 +221,7 @@
             return;
         }
 
-        mNotificationPanelViewController.expandWithoutQs();
+        mNotificationPanelViewController.expandShadeToNotifications();
     }
 
     @Override
@@ -331,9 +337,9 @@
                 mNotificationStackScrollLayoutController.setWillExpand(true);
                 mHeadsUpManager.unpinAll(true /* userUnpinned */);
                 mMetricsLogger.count("panel_open", 1);
-            } else if (!mNotificationPanelViewController.isInSettings()
+            } else if (!mQsController.getExpanded()
                     && !mNotificationPanelViewController.isExpanding()) {
-                mNotificationPanelViewController.flingSettings(0 /* velocity */,
+                mQsController.flingQs(0 /* velocity */,
                         NotificationPanelViewController.FLING_EXPAND);
                 mMetricsLogger.count("panel_open_qs", 1);
             }
@@ -375,7 +381,7 @@
             mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent,
                     false /* onlyProvisioned */, true /* dismissShade */,
                     true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
-                    null /* animationController */, UserHandle.CURRENT);
+                    null /* animationController */, mUserTracker.getUserHandle());
         } else {
             if (!mCentralSurfaces.isDeviceInteractive()) {
                 // Avoid flickering of the scrim when we instant launch the camera and the bouncer
@@ -432,7 +438,7 @@
             mCentralSurfaces.startActivityDismissingKeyguard(emergencyIntent,
                     false /* onlyProvisioned */, true /* dismissShade */,
                     true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
-                    null /* animationController */, UserHandle.CURRENT);
+                    null /* animationController */, mUserTracker.getUserHandle());
             return;
         }
 
@@ -447,7 +453,7 @@
             if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                 mStatusBarKeyguardViewManager.reset(true /* hide */);
             }
-            mContext.startActivityAsUser(emergencyIntent, UserHandle.CURRENT);
+            mContext.startActivityAsUser(emergencyIntent, mUserTracker.getUserHandle());
             return;
         }
         // We need to defer the emergency action launch until the screen comes on, since otherwise
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 688ce7b..48446cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -64,7 +64,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
@@ -162,6 +161,7 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
 import com.android.systemui.navigationbar.NavigationBarController;
@@ -179,11 +179,13 @@
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.scrim.ScrimView;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
+import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -291,26 +293,9 @@
 
     private static final UiEventLogger sUiEventLogger = new UiEventLoggerImpl();
 
-    /**
-     * If true, the system is in the half-boot-to-decryption-screen state.
-     * Prudently disable QS and notifications.
-     */
-    public static final boolean ONLY_CORE_APPS;
-
-    static {
-        boolean onlyCoreApps;
-        try {
-            IPackageManager packageManager =
-                    IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
-            onlyCoreApps = packageManager != null && packageManager.isOnlyCoreApps();
-        } catch (RemoteException e) {
-            onlyCoreApps = false;
-        }
-        ONLY_CORE_APPS = onlyCoreApps;
-    }
-
     private final Context mContext;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private final DeviceStateManager mDeviceStateManager;
     private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
     private float mTransitionToFullShadeProgress = 0f;
     private NotificationListContainer mNotifListContainer;
@@ -485,6 +470,7 @@
     private final ShadeController mShadeController;
     private final InitController mInitController;
     private final Lazy<CameraLauncher> mCameraLauncherLazy;
+    private final AlternateBouncerInteractor mAlternateBouncerInteractor;
 
     private final PluginDependencyProvider mPluginDependencyProvider;
     private final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -503,6 +489,8 @@
 
     // settings
     private QSPanelController mQSPanelController;
+    @VisibleForTesting
+    QuickSettingsController mQsController;
 
     KeyguardIndicationController mKeyguardIndicationController;
 
@@ -521,6 +509,7 @@
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final MessageRouter mMessageRouter;
     private final WallpaperManager mWallpaperManager;
+    private final UserTracker mUserTracker;
 
     private CentralSurfacesComponent mCentralSurfacesComponent;
 
@@ -763,7 +752,10 @@
             WiredChargingRippleController wiredChargingRippleController,
             IDreamManager dreamManager,
             Lazy<CameraLauncher> cameraLauncherLazy,
-            Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy) {
+            Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy,
+            AlternateBouncerInteractor alternateBouncerInteractor,
+            UserTracker userTracker
+    ) {
         mContext = context;
         mNotificationsController = notificationsController;
         mFragmentService = fragmentService;
@@ -841,6 +833,8 @@
         mWallpaperManager = wallpaperManager;
         mJankMonitor = jankMonitor;
         mCameraLauncherLazy = cameraLauncherLazy;
+        mAlternateBouncerInteractor = alternateBouncerInteractor;
+        mUserTracker = userTracker;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
@@ -874,11 +868,15 @@
         mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT,
                 id -> onLaunchTransitionTimeout());
 
-        deviceStateManager.registerCallback(mMainExecutor,
-                new FoldStateListener(mContext, this::onFoldedStateChanged));
+        mDeviceStateManager = deviceStateManager;
         wiredChargingRippleController.registerCallbacks();
 
         mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
+
+        // Based on teamfood flag, turn predictive back dispatch on at runtime.
+        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) {
+            mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
+        }
     }
 
     @Override
@@ -993,11 +991,6 @@
         // Lastly, call to the icon policy to install/update all the icons.
         mIconPolicy.init();
 
-        // Based on teamfood flag, turn predictive back dispatch on at runtime.
-        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) {
-            mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
-        }
-
         mKeyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onUnlockedChanged() {
@@ -1064,6 +1057,8 @@
             }
         });
 
+        registerCallbacks();
+
         mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
 
         mPluginManager.addPluginListener(
@@ -1119,6 +1114,14 @@
     }
 
     @VisibleForTesting
+    /** Registers listeners/callbacks with external dependencies. */
+    void registerCallbacks() {
+        //TODO(b/264502026) move the rest of the listeners here.
+        mDeviceStateManager.registerCallback(mMainExecutor,
+                new FoldStateListener(mContext, this::onFoldedStateChanged));
+    }
+
+    @VisibleForTesting
     void initShadeVisibilityListener() {
         mShadeController.setVisibilityListener(new ShadeController.ShadeVisibilityListener() {
             @Override
@@ -1306,8 +1309,13 @@
         // Set up the quick settings tile panel
         final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame);
         if (container != null) {
-            FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
-            ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame,
+            FragmentHostManager fragmentHostManager =
+                    mFragmentService.getFragmentHostManager(container);
+            ExtensionFragmentListener.attachExtensonToFragment(
+                    mFragmentService,
+                    container,
+                    QS.TAG,
+                    R.id.qs_frame,
                     mExtensionController
                             .newExtension(QS.class)
                             .withPlugin(QS.class)
@@ -1399,6 +1407,7 @@
         // Things that mean we're not swiping to dismiss the keyguard, and should ignore this
         // expansion:
         // - Keyguard isn't even visible.
+        // - We're swiping on the bouncer, not the lockscreen.
         // - Keyguard is occluded. Expansion changes here are the shade being expanded over the
         //   occluding activity.
         // - Keyguard is visible, but can't be dismissed (swiping up will show PIN/password prompt).
@@ -1407,11 +1416,14 @@
         //   to dismiss the lock screen until entering the SIM PIN.
         // - QS is expanded and we're swiping - swiping up now will hide QS, not dismiss the
         //   keyguard.
+        // - Shade is in QQS over keyguard - swiping up should take us back to keyguard
         if (!isKeyguardShowing()
+                || mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
                 || isOccluded()
                 || !mKeyguardStateController.canDismissLockScreen()
                 || mKeyguardViewMediator.isAnySimPinSecure()
-                || (mNotificationPanelViewController.isQsExpanded() && trackingTouch)) {
+                || (mQsController.getExpanded() && trackingTouch)
+                || mNotificationPanelViewController.getBarState() == StatusBarState.SHADE_LOCKED) {
             return;
         }
 
@@ -1474,7 +1486,9 @@
     }
 
     protected QS createDefaultQSFragment() {
-        return FragmentHostManager.get(mNotificationShadeWindowView).create(QSFragment.class);
+        return mFragmentService
+                .getFragmentHostManager(mNotificationShadeWindowView)
+                .create(QSFragment.class);
     }
 
     private void setUpPresenter() {
@@ -1569,6 +1583,7 @@
         mCentralSurfacesComponent.getLockIconViewController().init();
         mStackScrollerController =
                 mCentralSurfacesComponent.getNotificationStackScrollLayoutController();
+        mQsController = mCentralSurfacesComponent.getQuickSettingsController();
         mStackScroller = mStackScrollerController.getView();
         mNotifListContainer = mCentralSurfacesComponent.getNotificationListContainer();
         mPresenter = mCentralSurfacesComponent.getNotificationPresenter();
@@ -1687,9 +1702,8 @@
                         || !mUserSwitcherController.isSimpleUserSwitcher())
                 && !isShadeDisabled()
                 && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0)
-                && !mDozing
-                && !ONLY_CORE_APPS;
-        mNotificationPanelViewController.setQsExpansionEnabledPolicy(expandEnabled);
+                && !mDozing;
+        mQsController.setExpansionEnabledPolicy(expandEnabled);
         Log.d(TAG, "updateQsExpansionEnabled - QS Expand enabled: " + expandEnabled);
     }
 
@@ -2197,10 +2211,10 @@
             pw.println("Current Status Bar state:");
             pw.println("  mExpandedVisible=" + mShadeController.isExpandedVisible());
             pw.println("  mDisplayMetrics=" + mDisplayMetrics);
-            pw.println("  mStackScroller: " + CentralSurfaces.viewInfo(mStackScroller));
-            pw.println("  mStackScroller: " + CentralSurfaces.viewInfo(mStackScroller)
-                    + " scroll " + mStackScroller.getScrollX()
+            pw.print("  mStackScroller: " + CentralSurfaces.viewInfo(mStackScroller));
+            pw.print(" scroll " + mStackScroller.getScrollX()
                     + "," + mStackScroller.getScrollY());
+            pw.println(" translationX " + mStackScroller.getTranslationX());
         }
 
         pw.print("  mInteractingWindows="); pw.println(mInteractingWindows);
@@ -2970,16 +2984,6 @@
     }
 
     /**
-     * Plays the animation when an activity that was occluding Keyguard goes away.
-     */
-    @Override
-    public void animateKeyguardUnoccluding() {
-        mNotificationPanelViewController.setExpandedFraction(0f);
-        mCommandQueueCallbacks.animateExpandNotificationsPanel();
-        mScrimController.setUnocclusionAnimationRunning(true);
-    }
-
-    /**
      * Starts the timeout when we try to start the affordances on Keyguard. We usually rely that
      * Keyguard goes away via fadeKeyguardAfterLaunchTransition, however, that might not happen
      * because the launched app crashed or something else went wrong.
@@ -3224,12 +3228,12 @@
             mStatusBarKeyguardViewManager.onBackPressed();
             return true;
         }
-        if (mNotificationPanelViewController.isQsCustomizing()) {
-            mNotificationPanelViewController.closeQsCustomizer();
+        if (mQsController.isCustomizing()) {
+            mQsController.closeQsCustomizer();
             return true;
         }
-        if (mNotificationPanelViewController.isQsExpanded()) {
-                mNotificationPanelViewController.animateCloseQs(false /* animateAway */);
+        if (mQsController.getExpanded()) {
+            mNotificationPanelViewController.animateCloseQs(false);
             return true;
         }
         if (mNotificationPanelViewController.closeUserSwitcherIfOpen()) {
@@ -3257,8 +3261,7 @@
     private void showBouncerOrLockScreenIfKeyguard() {
         // If the keyguard is animating away, we aren't really the keyguard anymore and should not
         // show the bouncer/lockscreen.
-        if (!mKeyguardViewMediator.isHiding()
-                && !mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
+        if (!mKeyguardViewMediator.isHiding() && !mKeyguardUpdateMonitor.isKeyguardGoingAway()) {
             if (mState == StatusBarState.SHADE_LOCKED) {
                 // shade is showing while locked on the keyguard, so go back to showing the
                 // lock screen where users can use the UDFPS affordance to enter the device
@@ -3592,7 +3595,7 @@
             mFalsingCollector.onScreenOff();
             mScrimController.onScreenTurnedOff();
             if (mCloseQsBeforeScreenOff) {
-                mNotificationPanelViewController.closeQs();
+                mQsController.closeQs();
                 mCloseQsBeforeScreenOff = false;
             }
             updateIsKeyguard();
@@ -3693,6 +3696,12 @@
     @Override
     public void notifyBiometricAuthModeChanged() {
         mDozeServiceHost.updateDozing();
+        if (mBiometricUnlockController.getMode()
+                == BiometricUnlockController.MODE_DISMISS_BOUNCER) {
+            // Don't update the scrim controller at this time, in favor of the transition repository
+            // updating the scrim
+            return;
+        }
         updateScrimController();
     }
 
@@ -3738,13 +3747,16 @@
         boolean launchingAffordanceWithPreview = mLaunchingAffordance;
         mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview);
 
-        if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+        if (mAlternateBouncerInteractor.isVisibleState()) {
             if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED
                     || mTransitionToFullShadeProgress > 0f) {
                 mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
             } else {
                 mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED);
             }
+            // This will cancel the keyguardFadingAway animation if it is running. We need to do
+            // this as otherwise it can remain pending and leave keyguard in a weird state.
+            mUnlockScrimCallback.onCancelled();
         } else if (mBouncerShowing && !unlocking) {
             // Bouncer needs the front scrim when it's on top of an activity,
             // tapping on a notification, editing QS or being dismissed by
@@ -3771,6 +3783,9 @@
             });
         } else if (mDozing && !unlocking) {
             mScrimController.transitionTo(ScrimState.AOD);
+            // This will cancel the keyguardFadingAway animation if it is running. We need to do
+            // this as otherwise it can remain pending and leave keyguard in a weird state.
+            mUnlockScrimCallback.onCancelled();
         } else if (mKeyguardStateController.isShowing() && !isOccluded() && !unlocking) {
             mScrimController.transitionTo(ScrimState.KEYGUARD);
         } else if (mKeyguardStateController.isShowing() && mKeyguardUpdateMonitor.isDreaming()
@@ -4159,7 +4174,7 @@
                 Log.wtf(TAG, "WallpaperManager not supported");
                 return;
             }
-            WallpaperInfo info = mWallpaperManager.getWallpaperInfo(UserHandle.USER_CURRENT);
+            WallpaperInfo info = mWallpaperManager.getWallpaperInfo(mUserTracker.getUserId());
             mWallpaperController.onWallpaperInfoUpdated(info);
 
             final boolean deviceSupportsAodWallpaper = mContext.getResources().getBoolean(
@@ -4263,8 +4278,7 @@
 
                 @Override
                 public void onDozeAmountChanged(float linear, float eased) {
-                    if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)
-                            && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)
+                    if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)
                             && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) {
                         mLightRevealScrim.setRevealAmount(1f - linear);
                     }
@@ -4384,6 +4398,6 @@
                 return new UserHandle(UserHandle.myUserId());
             }
         }
-        return UserHandle.CURRENT;
+        return mUserTracker.getUserHandle();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index de7b152..b88531e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -44,10 +44,10 @@
 import com.android.systemui.doze.AlwaysOnDisplayPolicy;
 import com.android.systemui.doze.DozeScreenState;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 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.statusbar.policy.DevicePostureController;
 import com.android.systemui.tuner.TunerService;
@@ -82,10 +82,10 @@
     private final AlwaysOnDisplayPolicy mAlwaysOnPolicy;
     private final Resources mResources;
     private final BatteryController mBatteryController;
-    private final FeatureFlags mFeatureFlags;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final FoldAodAnimationController mFoldAodAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    private final UserTracker mUserTracker;
 
     private final Set<Callback> mCallbacks = new HashSet<>();
 
@@ -125,13 +125,13 @@
             BatteryController batteryController,
             TunerService tunerService,
             DumpManager dumpManager,
-            FeatureFlags featureFlags,
             ScreenOffAnimationController screenOffAnimationController,
             Optional<SysUIUnfoldComponent> sysUiUnfoldComponent,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             ConfigurationController configurationController,
-            StatusBarStateController statusBarStateController) {
+            StatusBarStateController statusBarStateController,
+            UserTracker userTracker) {
         mResources = resources;
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
         mAlwaysOnPolicy = alwaysOnDisplayPolicy;
@@ -141,9 +141,9 @@
         mControlScreenOffAnimation = !getDisplayNeedsBlanking();
         mPowerManager = powerManager;
         mPowerManager.setDozeAfterScreenOff(!mControlScreenOffAnimation);
-        mFeatureFlags = featureFlags;
         mScreenOffAnimationController = screenOffAnimationController;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
+        mUserTracker = userTracker;
 
         keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
         tunerService.addTunable(
@@ -162,11 +162,18 @@
 
         SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler);
         quickPickupSettingsObserver.observe();
+
+        batteryController.addCallback(new BatteryStateChangeCallback() {
+                @Override
+                public void onPowerSaveChanged(boolean isPowerSave) {
+                    dispatchAlwaysOnEvent();
+                }
+            });
     }
 
     private void updateQuickPickupEnabled() {
         mIsQuickPickupEnabled =
-                mAmbientDisplayConfiguration.quickPickupSensorEnabled(UserHandle.USER_CURRENT);
+                mAmbientDisplayConfiguration.quickPickupSensorEnabled(mUserTracker.getUserId());
     }
 
     public boolean getDisplayStateSupported() {
@@ -300,13 +307,10 @@
 
     /**
      * Whether we're capable of controlling the screen off animation if we want to. This isn't
-     * possible if AOD isn't even enabled or if the flag is disabled, or if the display needs
-     * blanking.
+     * possible if AOD isn't even enabled or if the display needs blanking.
      */
     public boolean canControlUnlockedScreenOff() {
-        return getAlwaysOn()
-                && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)
-                && !getDisplayNeedsBlanking();
+        return getAlwaysOn() && !getDisplayNeedsBlanking();
     }
 
     /**
@@ -348,7 +352,7 @@
     }
 
     private boolean willAnimateFromLockScreenToAod() {
-        return getAlwaysOn() && mKeyguardVisible;
+        return shouldControlScreenOff() && mKeyguardVisible;
     }
 
     private boolean getBoolean(String propName, int resId) {
@@ -418,15 +422,13 @@
 
     @Override
     public void onTuningChanged(String key, String newValue) {
-        mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
+        mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(mUserTracker.getUserId());
 
         if (key.equals(Settings.Secure.DOZE_ALWAYS_ON)) {
             updateControlScreenOff();
         }
 
-        for (Callback callback : mCallbacks) {
-            callback.onAlwaysOnChange();
-        }
+        dispatchAlwaysOnEvent();
         mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn());
     }
 
@@ -463,6 +465,12 @@
         pw.print("isQuickPickupEnabled(): "); pw.println(isQuickPickupEnabled());
     }
 
+    private void dispatchAlwaysOnEvent() {
+        for (Callback callback : mCallbacks) {
+            callback.onAlwaysOnChange();
+        }
+    }
+
     private boolean getPostureSpecificBool(
             int[] postureMapping,
             boolean defaultSensorBool,
@@ -477,7 +485,8 @@
         return bool;
     }
 
-    interface Callback {
+    /** Callbacks for doze parameter related information */
+    public interface Callback {
         /**
          * Invoked when the value of getAlwaysOn may have changed.
          */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
deleted file mode 100644
index 000fe14..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ /dev/null
@@ -1,762 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.phone;
-
-import static com.android.keyguard.KeyguardSecurityModel.SecurityMode;
-import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.hardware.biometrics.BiometricSourceType;
-import android.os.Handler;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-
-import com.android.internal.policy.SystemBarUtils;
-import com.android.keyguard.KeyguardHostViewController;
-import com.android.keyguard.KeyguardSecurityModel;
-import com.android.keyguard.KeyguardSecurityView;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.ViewMediatorCallback;
-import com.android.keyguard.dagger.KeyguardBouncerComponent;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
-import com.android.systemui.shared.system.SysUiStatsLog;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.ListenerSet;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * A class which manages the primary (pin/pattern/password) bouncer on the lockscreen.
- * @deprecated Use KeyguardBouncerRepository
- */
-@Deprecated
-public class KeyguardBouncer {
-
-    private static final String TAG = "PrimaryKeyguardBouncer";
-    static final long BOUNCER_FACE_DELAY = 1200;
-    public static final float ALPHA_EXPANSION_THRESHOLD = 0.95f;
-    /**
-     * Values for the bouncer expansion represented as the panel expansion.
-     * Panel expansion 1f = panel fully showing = bouncer fully hidden
-     * Panel expansion 0f = panel fully hiding = bouncer fully showing
-     */
-    public static final float EXPANSION_HIDDEN = 1f;
-    public static final float EXPANSION_VISIBLE = 0f;
-
-    protected final Context mContext;
-    protected final ViewMediatorCallback mCallback;
-    protected final ViewGroup mContainer;
-    private final FalsingCollector mFalsingCollector;
-    private final DismissCallbackRegistry mDismissCallbackRegistry;
-    private final Handler mHandler;
-    private final List<PrimaryBouncerExpansionCallback> mExpansionCallbacks = new ArrayList<>();
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final KeyguardStateController mKeyguardStateController;
-    private final KeyguardSecurityModel mKeyguardSecurityModel;
-    private final KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
-    private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
-            new KeyguardUpdateMonitorCallback() {
-                @Override
-                public void onStrongAuthStateChanged(int userId) {
-                    mBouncerPromptReason = mCallback.getBouncerPromptReason();
-                }
-
-                @Override
-                public void onLockedOutStateChanged(BiometricSourceType type) {
-                    if (type == BiometricSourceType.FINGERPRINT) {
-                        mBouncerPromptReason = mCallback.getBouncerPromptReason();
-                    }
-                }
-
-                @Override
-                public void onNonStrongBiometricAllowedChanged(int userId) {
-                    mBouncerPromptReason = mCallback.getBouncerPromptReason();
-                }
-            };
-    private final Runnable mRemoveViewRunnable = this::removeView;
-    private final KeyguardBypassController mKeyguardBypassController;
-    private KeyguardHostViewController mKeyguardViewController;
-    private final ListenerSet<KeyguardResetCallback> mResetCallbacks = new ListenerSet<>();
-    private final Runnable mResetRunnable = ()-> {
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.resetSecurityContainer();
-            for (KeyguardResetCallback callback : mResetCallbacks) {
-                callback.onKeyguardReset();
-            }
-        }
-    };
-
-    private int mStatusBarHeight;
-    private float mExpansion = EXPANSION_HIDDEN;
-    private boolean mShowingSoon;
-    private int mBouncerPromptReason;
-    private boolean mIsAnimatingAway;
-    private boolean mIsScrimmed;
-    private boolean mInitialized;
-
-    private KeyguardBouncer(Context context, ViewMediatorCallback callback,
-            ViewGroup container,
-            DismissCallbackRegistry dismissCallbackRegistry, FalsingCollector falsingCollector,
-            PrimaryBouncerExpansionCallback expansionCallback,
-            KeyguardStateController keyguardStateController,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
-            KeyguardBypassController keyguardBypassController, @Main Handler handler,
-            KeyguardSecurityModel keyguardSecurityModel,
-            KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory) {
-        mContext = context;
-        mCallback = callback;
-        mContainer = container;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mFalsingCollector = falsingCollector;
-        mDismissCallbackRegistry = dismissCallbackRegistry;
-        mHandler = handler;
-        mKeyguardStateController = keyguardStateController;
-        mKeyguardSecurityModel = keyguardSecurityModel;
-        mKeyguardBouncerComponentFactory = keyguardBouncerComponentFactory;
-        mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback);
-        mKeyguardBypassController = keyguardBypassController;
-        mExpansionCallbacks.add(expansionCallback);
-    }
-
-    /**
-     * Get the KeyguardBouncer expansion
-     * @return 1=HIDDEN, 0=SHOWING, in between 0 and 1 means the bouncer is in transition.
-     */
-    public float getExpansion() {
-        return mExpansion;
-    }
-
-    /**
-     * Enable/disable only the back button
-     */
-    public void setBackButtonEnabled(boolean enabled) {
-        int vis = mContainer.getSystemUiVisibility();
-        if (enabled) {
-            vis &= ~View.STATUS_BAR_DISABLE_BACK;
-        } else {
-            vis |= View.STATUS_BAR_DISABLE_BACK;
-        }
-        mContainer.setSystemUiVisibility(vis);
-    }
-
-    public void show(boolean resetSecuritySelection) {
-        show(resetSecuritySelection, true /* scrimmed */);
-    }
-
-    /**
-     * Shows the bouncer.
-     *
-     * @param resetSecuritySelection Cleans keyguard view
-     * @param isScrimmed true when the bouncer show show scrimmed, false when the user will be
-     *                 dragging it and translation should be deferred.
-     */
-    public void show(boolean resetSecuritySelection, boolean isScrimmed) {
-        final int keyguardUserId = KeyguardUpdateMonitor.getCurrentUser();
-        if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
-            // In split system user mode, we never unlock system user.
-            return;
-        }
-
-        try {
-            Trace.beginSection("KeyguardBouncer#show");
-
-            ensureView();
-            mIsScrimmed = isScrimmed;
-
-            // On the keyguard, we want to show the bouncer when the user drags up, but it's
-            // not correct to end the falsing session. We still need to verify if those touches
-            // are valid.
-            // Later, at the end of the animation, when the bouncer is at the top of the screen,
-            // onFullyShown() will be called and FalsingManager will stop recording touches.
-            if (isScrimmed) {
-                setExpansion(EXPANSION_VISIBLE);
-            }
-
-            if (resetSecuritySelection) {
-                // showPrimarySecurityScreen() updates the current security method. This is needed
-                // in case we are already showing and the current security method changed.
-                showPrimarySecurityScreen();
-            }
-
-            if (mContainer.getVisibility() == View.VISIBLE || mShowingSoon) {
-                // Calls to reset must resume the ViewControllers when in fullscreen mode
-                if (needsFullscreenBouncer()) {
-                    mKeyguardViewController.onResume();
-                }
-                return;
-            }
-
-            final int activeUserId = KeyguardUpdateMonitor.getCurrentUser();
-            final boolean isSystemUser =
-                UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM;
-            final boolean allowDismissKeyguard = !isSystemUser && activeUserId == keyguardUserId;
-
-            // If allowed, try to dismiss the Keyguard. If no security auth (password/pin/pattern)
-            // is set, this will dismiss the whole Keyguard. Otherwise, show the bouncer.
-            if (allowDismissKeyguard && mKeyguardViewController.dismiss(activeUserId)) {
-                return;
-            }
-
-            // This condition may indicate an error on Android, so log it.
-            if (!allowDismissKeyguard) {
-                Log.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != "
-                        + keyguardUserId);
-            }
-
-            mShowingSoon = true;
-
-            // Split up the work over multiple frames.
-            DejankUtils.removeCallbacks(mResetRunnable);
-            if (mKeyguardStateController.isFaceAuthEnabled()
-                    && !mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
-                            KeyguardUpdateMonitor.getCurrentUser())
-                    && !needsFullscreenBouncer()
-                    && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                            BiometricSourceType.FACE)
-                    && !mKeyguardBypassController.getBypassEnabled()) {
-                mHandler.postDelayed(mShowRunnable, BOUNCER_FACE_DELAY);
-            } else {
-                DejankUtils.postAfterTraversal(mShowRunnable);
-            }
-
-            mKeyguardStateController.notifyBouncerShowing(true /* showing */);
-            dispatchStartingToShow();
-        } finally {
-            Trace.endSection();
-        }
-    }
-
-    public boolean isScrimmed() {
-        return mIsScrimmed;
-    }
-
-    /**
-     * This method must be called at the end of the bouncer animation when
-     * the translation is performed manually by the user, otherwise FalsingManager
-     * will never be notified and its internal state will be out of sync.
-     */
-    private void onFullyShown() {
-        mFalsingCollector.onBouncerShown();
-        if (mKeyguardViewController == null) {
-            Log.e(TAG, "onFullyShown when view was null");
-        } else {
-            mKeyguardViewController.onResume();
-            mContainer.announceForAccessibility(
-                    mKeyguardViewController.getAccessibilityTitleForCurrentMode());
-        }
-    }
-
-    /**
-     * @see #onFullyShown()
-     */
-    private void onFullyHidden() {
-
-    }
-
-    private void setVisibility(@View.Visibility int visibility) {
-        mContainer.setVisibility(visibility);
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.onBouncerVisibilityChanged(visibility);
-        }
-        dispatchVisibilityChanged();
-    }
-
-    private final Runnable mShowRunnable = new Runnable() {
-        @Override
-        public void run() {
-            setVisibility(View.VISIBLE);
-            showPromptReason(mBouncerPromptReason);
-            final CharSequence customMessage = mCallback.consumeCustomMessage();
-            if (customMessage != null) {
-                mKeyguardViewController.showErrorMessage(customMessage);
-            }
-            mKeyguardViewController.appear(mStatusBarHeight);
-            mShowingSoon = false;
-            if (mExpansion == EXPANSION_VISIBLE) {
-                mKeyguardViewController.onResume();
-                mKeyguardViewController.resetSecurityContainer();
-                showPromptReason(mBouncerPromptReason);
-            }
-        }
-    };
-
-    /**
-     * Show a string explaining why the security view needs to be solved.
-     *
-     * @param reason a flag indicating which string should be shown, see
-     *               {@link KeyguardSecurityView#PROMPT_REASON_NONE}
-     *               and {@link KeyguardSecurityView#PROMPT_REASON_RESTART}
-     */
-    public void showPromptReason(int reason) {
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.showPromptReason(reason);
-        } else {
-            Log.w(TAG, "Trying to show prompt reason on empty bouncer");
-        }
-    }
-
-    public void showMessage(String message, ColorStateList colorState) {
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.showMessage(message, colorState);
-        } else {
-            Log.w(TAG, "Trying to show message on empty bouncer");
-        }
-    }
-
-    private void cancelShowRunnable() {
-        DejankUtils.removeCallbacks(mShowRunnable);
-        mHandler.removeCallbacks(mShowRunnable);
-        mShowingSoon = false;
-    }
-
-    public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) {
-        ensureView();
-        setDismissAction(r, cancelAction);
-        show(false /* resetSecuritySelection */);
-    }
-
-    /**
-     * Set the actions to run when the keyguard is dismissed or when the dismiss is cancelled. Those
-     * actions will still be run even if this bouncer is not shown, for instance when authenticating
-     * with an alternate authenticator like the UDFPS.
-     */
-    public void setDismissAction(OnDismissAction r, Runnable cancelAction) {
-        mKeyguardViewController.setOnDismissAction(r, cancelAction);
-    }
-
-    public void hide(boolean destroyView) {
-        Trace.beginSection("KeyguardBouncer#hide");
-        if (isShowing()) {
-            SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
-                    SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN);
-            mDismissCallbackRegistry.notifyDismissCancelled();
-        }
-        mIsScrimmed = false;
-        mFalsingCollector.onBouncerHidden();
-        mKeyguardStateController.notifyBouncerShowing(false /* showing */);
-        cancelShowRunnable();
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.cancelDismissAction();
-            mKeyguardViewController.cleanUp();
-        }
-        mIsAnimatingAway = false;
-        setVisibility(View.INVISIBLE);
-        if (destroyView) {
-
-            // We have a ViewFlipper that unregisters a broadcast when being detached, which may
-            // be slow because of AM lock contention during unlocking. We can delay it a bit.
-            mHandler.postDelayed(mRemoveViewRunnable, 50);
-        }
-        Trace.endSection();
-    }
-
-    /**
-     * See {@link StatusBarKeyguardViewManager#startPreHideAnimation}.
-     */
-    public void startPreHideAnimation(Runnable runnable) {
-        mIsAnimatingAway = true;
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.startDisappearAnimation(runnable);
-        } else if (runnable != null) {
-            runnable.run();
-        }
-    }
-
-    /**
-     * Reset the state of the view.
-     */
-    public void reset() {
-        cancelShowRunnable();
-        inflateView();
-        mFalsingCollector.onBouncerHidden();
-    }
-
-    public void onScreenTurnedOff() {
-        if (mKeyguardViewController != null && mContainer.getVisibility() == View.VISIBLE) {
-            mKeyguardViewController.onPause();
-        }
-    }
-
-    public boolean isShowing() {
-        return (mShowingSoon || mContainer.getVisibility() == View.VISIBLE)
-                && mExpansion == EXPANSION_VISIBLE && !isAnimatingAway();
-    }
-
-    /**
-     * {@link #show(boolean)} was called but we're not showing yet, or being dragged.
-     */
-    public boolean inTransit() {
-        return mShowingSoon || mExpansion != EXPANSION_HIDDEN && mExpansion != EXPANSION_VISIBLE;
-    }
-
-    /**
-     * @return {@code true} when bouncer's pre-hide animation already started but isn't completely
-     *         hidden yet, {@code false} otherwise.
-     */
-    public boolean isAnimatingAway() {
-        return mIsAnimatingAway;
-    }
-
-    public void prepare() {
-        boolean wasInitialized = mInitialized;
-        ensureView();
-        if (wasInitialized) {
-            showPrimarySecurityScreen();
-        }
-        mBouncerPromptReason = mCallback.getBouncerPromptReason();
-    }
-
-    private void showPrimarySecurityScreen() {
-        mKeyguardViewController.showPrimarySecurityScreen();
-    }
-
-    /**
-     * Current notification panel expansion
-     * @param fraction 0 when notification panel is collapsed and 1 when expanded.
-     * @see StatusBarKeyguardViewManager#onPanelExpansionChanged
-     */
-    public void setExpansion(float fraction) {
-        float oldExpansion = mExpansion;
-        boolean expansionChanged = mExpansion != fraction;
-        mExpansion = fraction;
-        if (mKeyguardViewController != null && !mIsAnimatingAway) {
-            mKeyguardViewController.setExpansion(fraction);
-        }
-
-        if (fraction == EXPANSION_VISIBLE && oldExpansion != EXPANSION_VISIBLE) {
-            onFullyShown();
-            dispatchFullyShown();
-        } else if (fraction == EXPANSION_HIDDEN && oldExpansion != EXPANSION_HIDDEN) {
-            DejankUtils.postAfterTraversal(mResetRunnable);
-            /*
-             * There are cases where #hide() was not invoked, such as when
-             * NotificationPanelViewController controls the hide animation. Make sure the state gets
-             * updated by calling #hide() directly.
-             */
-            hide(false /* destroyView */);
-            dispatchFullyHidden();
-        } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) {
-            dispatchStartingToHide();
-            if (mKeyguardViewController != null) {
-                mKeyguardViewController.onStartingToHide();
-            }
-        }
-
-        if (expansionChanged) {
-            dispatchExpansionChanged();
-        }
-    }
-
-    public boolean willDismissWithAction() {
-        return mKeyguardViewController != null && mKeyguardViewController.hasDismissActions();
-    }
-
-    public int getTop() {
-        if (mKeyguardViewController == null) {
-            return 0;
-        }
-
-        return mKeyguardViewController.getTop();
-    }
-
-    protected void ensureView() {
-        // Removal of the view might be deferred to reduce unlock latency,
-        // in this case we need to force the removal, otherwise we'll
-        // end up in an unpredictable state.
-        boolean forceRemoval = mHandler.hasCallbacks(mRemoveViewRunnable);
-        if (!mInitialized || forceRemoval) {
-            inflateView();
-        }
-    }
-
-    protected void inflateView() {
-        removeView();
-        mHandler.removeCallbacks(mRemoveViewRunnable);
-
-        KeyguardBouncerComponent component = mKeyguardBouncerComponentFactory.create(mContainer);
-        mKeyguardViewController = component.getKeyguardHostViewController();
-        mKeyguardViewController.init();
-
-        mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
-        setVisibility(View.INVISIBLE);
-
-        final WindowInsets rootInsets = mContainer.getRootWindowInsets();
-        if (rootInsets != null) {
-            mContainer.dispatchApplyWindowInsets(rootInsets);
-        }
-        mInitialized = true;
-    }
-
-    protected void removeView() {
-        mContainer.removeAllViews();
-        mInitialized = false;
-    }
-
-    /**
-     * @return True if and only if the security method should be shown before showing the
-     * notifications on Keyguard, like SIM PIN/PUK.
-     */
-    public boolean needsFullscreenBouncer() {
-        SecurityMode mode = mKeyguardSecurityModel.getSecurityMode(
-                KeyguardUpdateMonitor.getCurrentUser());
-        return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk;
-    }
-
-    /**
-     * Like {@link #needsFullscreenBouncer}, but uses the currently visible security method, which
-     * makes this method much faster.
-     */
-    public boolean isFullscreenBouncer() {
-        if (mKeyguardViewController != null) {
-            SecurityMode mode = mKeyguardViewController.getCurrentSecurityMode();
-            return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk;
-        }
-        return false;
-    }
-
-    /**
-     * WARNING: This method might cause Binder calls.
-     */
-    public boolean isSecure() {
-        return mKeyguardSecurityModel.getSecurityMode(
-                KeyguardUpdateMonitor.getCurrentUser()) != SecurityMode.None;
-    }
-
-    public boolean shouldDismissOnMenuPressed() {
-        return mKeyguardViewController.shouldEnableMenuKey();
-    }
-
-    public boolean interceptMediaKey(KeyEvent event) {
-        ensureView();
-        return mKeyguardViewController.interceptMediaKey(event);
-    }
-
-    /**
-     * @return true if the pre IME back event should be handled
-     */
-    public boolean dispatchBackKeyEventPreIme() {
-        ensureView();
-        return mKeyguardViewController.dispatchBackKeyEventPreIme();
-    }
-
-    public void notifyKeyguardAuthenticated(boolean strongAuth) {
-        ensureView();
-        mKeyguardViewController.finish(strongAuth, KeyguardUpdateMonitor.getCurrentUser());
-    }
-
-    private void dispatchFullyShown() {
-        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
-            callback.onFullyShown();
-        }
-    }
-
-    private void dispatchStartingToHide() {
-        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
-            callback.onStartingToHide();
-        }
-    }
-
-    private void dispatchStartingToShow() {
-        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
-            callback.onStartingToShow();
-        }
-    }
-
-    private void dispatchFullyHidden() {
-        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
-            callback.onFullyHidden();
-        }
-    }
-
-    private void dispatchExpansionChanged() {
-        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
-            callback.onExpansionChanged(mExpansion);
-        }
-    }
-
-    private void dispatchVisibilityChanged() {
-        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
-            callback.onVisibilityChanged(mContainer.getVisibility() == View.VISIBLE);
-        }
-    }
-
-    /**
-     * Apply keyguard configuration from the currently active resources. This can be called when the
-     * device configuration changes, to re-apply some resources that are qualified on the device
-     * configuration.
-     */
-    public void updateResources() {
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.updateResources();
-        }
-    }
-
-    public void dump(PrintWriter pw) {
-        pw.println("KeyguardBouncer");
-        pw.println("  isShowing(): " + isShowing());
-        pw.println("  mStatusBarHeight: " + mStatusBarHeight);
-        pw.println("  mExpansion: " + mExpansion);
-        pw.println("  mKeyguardViewController; " + mKeyguardViewController);
-        pw.println("  mShowingSoon: " + mShowingSoon);
-        pw.println("  mBouncerPromptReason: " + mBouncerPromptReason);
-        pw.println("  mIsAnimatingAway: " + mIsAnimatingAway);
-        pw.println("  mInitialized: " + mInitialized);
-    }
-
-    /** Update keyguard position based on a tapped X coordinate. */
-    public void updateKeyguardPosition(float x) {
-        if (mKeyguardViewController != null) {
-            mKeyguardViewController.updateKeyguardPosition(x);
-        }
-    }
-
-    public void addKeyguardResetCallback(KeyguardResetCallback callback) {
-        mResetCallbacks.addIfAbsent(callback);
-    }
-
-    public void removeKeyguardResetCallback(KeyguardResetCallback callback) {
-        mResetCallbacks.remove(callback);
-    }
-
-    /**
-     * Adds a callback to listen to bouncer expansion updates.
-     */
-    public void addBouncerExpansionCallback(PrimaryBouncerExpansionCallback callback) {
-        if (!mExpansionCallbacks.contains(callback)) {
-            mExpansionCallbacks.add(callback);
-        }
-    }
-
-    /**
-     * Removes a previously added callback. If the callback was never added, this methood
-     * does nothing.
-     */
-    public void removeBouncerExpansionCallback(PrimaryBouncerExpansionCallback callback) {
-        mExpansionCallbacks.remove(callback);
-    }
-
-    /**
-     * Callback updated when the primary bouncer's show and hide states change.
-     */
-    public interface PrimaryBouncerExpansionCallback {
-        /**
-         * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_VISIBLE}.
-         * This is NOT called each time the bouncer is shown, but rather only when the fully
-         * shown amount has changed based on the panel expansion. The bouncer's visibility
-         * can still change when the expansion amount hasn't changed.
-         * See {@link KeyguardBouncer#isShowing()} for the checks for the bouncer showing state.
-         */
-        default void onFullyShown() {
-        }
-
-        /**
-         * Invoked when the bouncer is starting to transition to a hidden state.
-         */
-        default void onStartingToHide() {
-        }
-
-        /**
-         * Invoked when the bouncer is starting to transition to a visible state.
-         */
-        default void onStartingToShow() {
-        }
-
-        /**
-         * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_HIDDEN}.
-         */
-        default void onFullyHidden() {
-        }
-
-        /**
-         * From 0f {@link KeyguardBouncer#EXPANSION_VISIBLE} when fully visible
-         * to 1f {@link KeyguardBouncer#EXPANSION_HIDDEN} when fully hidden
-         */
-        default void onExpansionChanged(float bouncerHideAmount) {}
-
-        /**
-         * Invoked when visibility of KeyguardBouncer has changed.
-         * Note the bouncer expansion can be {@link KeyguardBouncer#EXPANSION_VISIBLE}, but the
-         * view's visibility can be {@link View.INVISIBLE}.
-         */
-        default void onVisibilityChanged(boolean isVisible) {}
-    }
-
-    public interface KeyguardResetCallback {
-        void onKeyguardReset();
-    }
-
-    /** Create a {@link KeyguardBouncer} once a container and bouncer callback are available. */
-    public static class Factory {
-        private final Context mContext;
-        private final ViewMediatorCallback mCallback;
-        private final DismissCallbackRegistry mDismissCallbackRegistry;
-        private final FalsingCollector mFalsingCollector;
-        private final KeyguardStateController mKeyguardStateController;
-        private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-        private final KeyguardBypassController mKeyguardBypassController;
-        private final Handler mHandler;
-        private final KeyguardSecurityModel mKeyguardSecurityModel;
-        private final KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
-
-        @Inject
-        public Factory(Context context, ViewMediatorCallback callback,
-                DismissCallbackRegistry dismissCallbackRegistry, FalsingCollector falsingCollector,
-                KeyguardStateController keyguardStateController,
-                KeyguardUpdateMonitor keyguardUpdateMonitor,
-                KeyguardBypassController keyguardBypassController, @Main Handler handler,
-                KeyguardSecurityModel keyguardSecurityModel,
-                KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory) {
-            mContext = context;
-            mCallback = callback;
-            mDismissCallbackRegistry = dismissCallbackRegistry;
-            mFalsingCollector = falsingCollector;
-            mKeyguardStateController = keyguardStateController;
-            mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-            mKeyguardBypassController = keyguardBypassController;
-            mHandler = handler;
-            mKeyguardSecurityModel = keyguardSecurityModel;
-            mKeyguardBouncerComponentFactory = keyguardBouncerComponentFactory;
-        }
-
-        /**
-         * Construct a KeyguardBouncer that will exist in the given container.
-         */
-        public KeyguardBouncer create(ViewGroup container,
-                PrimaryBouncerExpansionCallback expansionCallback) {
-            return new KeyguardBouncer(mContext, mCallback, container,
-                    mDismissCallbackRegistry, mFalsingCollector, expansionCallback,
-                    mKeyguardStateController, mKeyguardUpdateMonitor,
-                    mKeyguardBypassController, mHandler, mKeyguardSecurityModel,
-                    mKeyguardBouncerComponentFactory);
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index b965ac9..ff1b31d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -30,6 +30,9 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.tuner.TunerService
 import java.io.PrintWriter
@@ -40,11 +43,19 @@
 
     private val mKeyguardStateController: KeyguardStateController
     private val statusBarStateController: StatusBarStateController
+    private val devicePostureController: DevicePostureController
     @BypassOverride private val bypassOverride: Int
     private var hasFaceFeature: Boolean
+    @DevicePostureInt private val configFaceAuthSupportedPosture: Int
+    @DevicePostureInt private var postureState: Int = DEVICE_POSTURE_UNKNOWN
     private var pendingUnlock: PendingUnlock? = null
     private val listeners = mutableListOf<OnBypassStateChangedListener>()
-
+    private val postureCallback = DevicePostureController.Callback { posture ->
+        if (postureState != posture) {
+            postureState = posture
+            notifyListeners()
+        }
+    }
     private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback {
         override fun onFaceAuthEnabledChanged() = notifyListeners()
     }
@@ -86,7 +97,8 @@
                 FACE_UNLOCK_BYPASS_NEVER -> false
                 else -> field
             }
-            return enabled && mKeyguardStateController.isFaceAuthEnabled
+            return enabled && mKeyguardStateController.isFaceAuthEnabled &&
+                    isPostureAllowedForFaceAuth()
         }
         private set(value) {
             field = value
@@ -106,18 +118,31 @@
         lockscreenUserManager: NotificationLockscreenUserManager,
         keyguardStateController: KeyguardStateController,
         shadeExpansionStateManager: ShadeExpansionStateManager,
+        devicePostureController: DevicePostureController,
         dumpManager: DumpManager
     ) {
         this.mKeyguardStateController = keyguardStateController
         this.statusBarStateController = statusBarStateController
+        this.devicePostureController = devicePostureController
 
         bypassOverride = context.resources.getInteger(R.integer.config_face_unlock_bypass_override)
+        configFaceAuthSupportedPosture =
+            context.resources.getInteger(R.integer.config_face_auth_supported_posture)
 
-        hasFaceFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)
+        hasFaceFeature = context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)
         if (!hasFaceFeature) {
             return
         }
 
+        if (configFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) {
+            devicePostureController.addCallback { posture ->
+                if (postureState != posture) {
+                    postureState = posture
+                    notifyListeners()
+                }
+            }
+        }
+
         dumpManager.registerDumpable("KeyguardBypassController", this)
         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
             override fun onStateChanged(newState: Int) {
@@ -203,6 +228,13 @@
         pendingUnlock = null
     }
 
+    fun isPostureAllowedForFaceAuth(): Boolean {
+        return when (configFaceAuthSupportedPosture) {
+            DEVICE_POSTURE_UNKNOWN -> true
+            else -> (postureState == configFaceAuthSupportedPosture)
+        }
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("KeyguardBypassController:")
         if (pendingUnlock != null) {
@@ -219,6 +251,7 @@
         pw.println("  launchingAffordance: $launchingAffordance")
         pw.println("  qSExpanded: $qsExpanded")
         pw.println("  hasFaceFeature: $hasFaceFeature")
+        pw.println("  postureState: $postureState")
     }
 
     /** Registers a listener for bypass state changes. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 01af486..c163a89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -89,14 +89,19 @@
     private float mPanelExpansion;
 
     /**
-     * Burn-in prevention x translation.
+     * Max burn-in prevention x translation.
      */
-    private int mBurnInPreventionOffsetX;
+    private int mMaxBurnInPreventionOffsetX;
 
     /**
-     * Burn-in prevention y translation for clock layouts.
+     * Max burn-in prevention y translation for clock layouts.
      */
-    private int mBurnInPreventionOffsetYClock;
+    private int mMaxBurnInPreventionOffsetYClock;
+
+    /**
+     * Current burn-in prevention y translation.
+     */
+    private float mCurrentBurnInOffsetY;
 
     /**
      * Doze/AOD transition amount.
@@ -155,9 +160,9 @@
 
         mContainerTopPadding =
                 res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
-        mBurnInPreventionOffsetX = res.getDimensionPixelSize(
+        mMaxBurnInPreventionOffsetX = res.getDimensionPixelSize(
                 R.dimen.burn_in_prevention_offset_x);
-        mBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
+        mMaxBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
                 R.dimen.burn_in_prevention_offset_y_clock);
     }
 
@@ -215,7 +220,10 @@
         if (mBypassEnabled) {
             return (int) (mUnlockedStackScrollerPadding + mOverStretchAmount);
         } else if (mIsSplitShade) {
-            return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight;
+            // mCurrentBurnInOffsetY is subtracted to make notifications not follow clock adjustment
+            // for burn-in. It can make pulsing notification go too high and it will get clipped
+            return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight
+                    - (int) mCurrentBurnInOffsetY;
         } else {
             return clockYPosition + mKeyguardStatusHeight;
         }
@@ -255,11 +263,11 @@
 
         // This will keep the clock at the top but out of the cutout area
         float shift = 0;
-        if (clockY - mBurnInPreventionOffsetYClock < mCutoutTopInset) {
-            shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYClock);
+        if (clockY - mMaxBurnInPreventionOffsetYClock < mCutoutTopInset) {
+            shift = mCutoutTopInset - (clockY - mMaxBurnInPreventionOffsetYClock);
         }
 
-        int burnInPreventionOffsetY = mBurnInPreventionOffsetYClock; // requested offset
+        int burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; // requested offset
         final boolean hasUdfps = mUdfpsTop > -1;
         if (hasUdfps && !mIsClockTopAligned) {
             // ensure clock doesn't overlap with the udfps icon
@@ -267,8 +275,8 @@
                 // sometimes the clock textView extends beyond udfps, so let's just use the
                 // space above the KeyguardStatusView/clock as our burn-in offset
                 burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2;
-                if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
-                    burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+                if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+                    burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
                 }
                 shift = -burnInPreventionOffsetY;
             } else {
@@ -276,16 +284,18 @@
                 float lowerSpace = mUdfpsTop - mClockBottom;
                 // center the burn-in offset within the upper + lower space
                 burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2;
-                if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
-                    burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+                if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+                    burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
                 }
                 shift = (lowerSpace - upperSpace) / 2;
             }
         }
 
+        float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
         float clockYDark = clockY
-                + burnInPreventionOffsetY(burnInPreventionOffsetY)
+                + fullyDarkBurnInOffset
                 + shift;
+        mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
         return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
     }
 
@@ -325,7 +335,7 @@
     }
 
     private float burnInPreventionOffsetX() {
-        return getBurnInOffset(mBurnInPreventionOffsetX, true /* xAxis */);
+        return getBurnInOffset(mMaxBurnInPreventionOffsetX, true /* xAxis */);
     }
 
     public static class Result {
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/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index 4550cb2..8ee2c6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -76,7 +76,7 @@
                 FaceAuthApiRequestReason.PICK_UP_GESTURE_TRIGGERED
             )
             keyguardUpdateMonitor.requestActiveUnlock(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE,
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
                 "KeyguardLiftController")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 3483574..753032c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -21,9 +21,6 @@
 
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -36,15 +33,19 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.animation.Animator;
+import androidx.core.animation.AnimatorListenerAdapter;
+import androidx.core.animation.ValueAnimator;
 
 import com.android.keyguard.CarrierTextController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.InterpolatorsAndroidX;
 import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -76,6 +77,7 @@
 
 /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
 public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
+    private static final String TAG = "KeyguardStatusBarViewController";
     private static final AnimationProperties KEYGUARD_HUN_PROPERTIES =
             new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
 
@@ -164,7 +166,8 @@
 
     private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener =
             animation -> {
-                mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue();
+                mKeyguardStatusBarAnimateAlpha =
+                        (float) ((ValueAnimator) animation).getAnimatedValue();
                 updateViewState();
             };
 
@@ -355,7 +358,7 @@
         mView.setOnApplyWindowInsetsListener(
                 (view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
         mSecureSettings.registerContentObserverForUser(
-                Settings.Secure.getUriFor(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON),
+                Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
                 false,
                 mVolumeSettingObserver,
                 UserHandle.USER_ALL);
@@ -422,7 +425,7 @@
 
     /** Animate the keyguard status bar in. */
     public void animateKeyguardStatusBarIn() {
-        mLogger.d("animating status bar in");
+        mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in");
         if (mDisableStateTracker.isDisabled()) {
             // If our view is disabled, don't allow us to animate in.
             return;
@@ -432,18 +435,18 @@
         ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
         anim.addUpdateListener(mAnimatorUpdateListener);
         anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        anim.setInterpolator(InterpolatorsAndroidX.LINEAR_OUT_SLOW_IN);
         anim.start();
     }
 
     /** Animate the keyguard status bar out. */
     public void animateKeyguardStatusBarOut(long startDelay, long duration) {
-        mLogger.d("animating status bar out");
+        mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out");
         ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
         anim.addUpdateListener(mAnimatorUpdateListener);
         anim.setStartDelay(startDelay);
         anim.setDuration(duration);
-        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        anim.setInterpolator(InterpolatorsAndroidX.LINEAR_OUT_SLOW_IN);
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index 4d14542..fe2a913 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
@@ -38,6 +37,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.policy.BatteryController;
 
 import java.io.PrintWriter;
@@ -94,7 +94,8 @@
             DarkIconDispatcher darkIconDispatcher,
             BatteryController batteryController,
             NavigationModeController navModeController,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            DisplayTracker displayTracker) {
         mDarkIconColor = ctx.getColor(R.color.dark_mode_icon_color_single_tone);
         mLightIconColor = ctx.getColor(R.color.light_mode_icon_color_single_tone);
         mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
@@ -104,7 +105,7 @@
             mNavigationMode = mode;
         });
 
-        if (ctx.getDisplayId() == DEFAULT_DISPLAY) {
+        if (ctx.getDisplayId() == displayTracker.getDefaultDisplayId()) {
             dumpManager.registerDumpable(getClass().getSimpleName(), this);
         }
     }
@@ -317,24 +318,27 @@
         private final BatteryController mBatteryController;
         private final NavigationModeController mNavModeController;
         private final DumpManager mDumpManager;
+        private final DisplayTracker mDisplayTracker;
 
         @Inject
         public Factory(
                 DarkIconDispatcher darkIconDispatcher,
                 BatteryController batteryController,
                 NavigationModeController navModeController,
-                DumpManager dumpManager) {
+                DumpManager dumpManager,
+                DisplayTracker displayTracker) {
 
             mDarkIconDispatcher = darkIconDispatcher;
             mBatteryController = batteryController;
             mNavModeController = navModeController;
             mDumpManager = dumpManager;
+            mDisplayTracker = displayTracker;
         }
 
         /** Create an {@link LightBarController} */
         public LightBarController create(Context context) {
             return new LightBarController(context, mDarkIconDispatcher, mBatteryController,
-                    mNavModeController, mDumpManager);
+                    mNavModeController, mDumpManager, mDisplayTracker);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 48e58fc..e6b76ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -22,8 +22,6 @@
 import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
 import android.app.AlarmManager.AlarmClockInfo;
-import android.app.IActivityManager;
-import android.app.SynchronousUserSwitchObserver;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -35,7 +33,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings.Global;
 import android.service.notification.ZenModeConfig;
@@ -58,6 +55,7 @@
 import com.android.systemui.qs.tiles.DndTile;
 import com.android.systemui.qs.tiles.RotationLockTile;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.CastController;
@@ -134,8 +132,8 @@
     private final NextAlarmController mNextAlarmController;
     private final AlarmManager mAlarmManager;
     private final UserInfoController mUserInfoController;
-    private final IActivityManager mIActivityManager;
     private final UserManager mUserManager;
+    private final UserTracker mUserTracker;
     private final DevicePolicyManager mDevicePolicyManager;
     private final StatusBarIconController mIconController;
     private final CommandQueue mCommandQueue;
@@ -148,6 +146,7 @@
     private final KeyguardStateController mKeyguardStateController;
     private final LocationController mLocationController;
     private final PrivacyItemController mPrivacyItemController;
+    private final Executor mMainExecutor;
     private final Executor mUiBgExecutor;
     private final SensorPrivacyController mSensorPrivacyController;
     private final RecordingController mRecordingController;
@@ -167,16 +166,17 @@
     @Inject
     public PhoneStatusBarPolicy(StatusBarIconController iconController,
             CommandQueue commandQueue, BroadcastDispatcher broadcastDispatcher,
-            @UiBackground Executor uiBgExecutor, @Main Looper looper, @Main Resources resources,
-            CastController castController, HotspotController hotspotController,
-            BluetoothController bluetoothController, NextAlarmController nextAlarmController,
-            UserInfoController userInfoController, RotationLockController rotationLockController,
-            DataSaverController dataSaverController, ZenModeController zenModeController,
+            @Main Executor mainExecutor, @UiBackground Executor uiBgExecutor, @Main Looper looper,
+            @Main Resources resources, CastController castController,
+            HotspotController hotspotController, BluetoothController bluetoothController,
+            NextAlarmController nextAlarmController, UserInfoController userInfoController,
+            RotationLockController rotationLockController, DataSaverController dataSaverController,
+            ZenModeController zenModeController,
             DeviceProvisionedController deviceProvisionedController,
             KeyguardStateController keyguardStateController,
             LocationController locationController,
-            SensorPrivacyController sensorPrivacyController, IActivityManager iActivityManager,
-            AlarmManager alarmManager, UserManager userManager,
+            SensorPrivacyController sensorPrivacyController, AlarmManager alarmManager,
+            UserManager userManager, UserTracker userTracker,
             DevicePolicyManager devicePolicyManager, RecordingController recordingController,
             @Nullable TelecomManager telecomManager, @DisplayId int displayId,
             @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
@@ -194,8 +194,8 @@
         mNextAlarmController = nextAlarmController;
         mAlarmManager = alarmManager;
         mUserInfoController = userInfoController;
-        mIActivityManager = iActivityManager;
         mUserManager = userManager;
+        mUserTracker = userTracker;
         mDevicePolicyManager = devicePolicyManager;
         mRotationLockController = rotationLockController;
         mDataSaver = dataSaverController;
@@ -206,6 +206,7 @@
         mPrivacyItemController = privacyItemController;
         mSensorPrivacyController = sensorPrivacyController;
         mRecordingController = recordingController;
+        mMainExecutor = mainExecutor;
         mUiBgExecutor = uiBgExecutor;
         mTelecomManager = telecomManager;
         mRingerModeTracker = ringerModeTracker;
@@ -254,11 +255,7 @@
         mRingerModeTracker.getRingerModeInternal().observeForever(observer);
 
         // listen for user / profile change.
-        try {
-            mIActivityManager.registerUserSwitchObserver(mUserSwitchListener, TAG);
-        } catch (RemoteException e) {
-            // Ignore
-        }
+        mUserTracker.addCallback(mUserSwitchListener, mMainExecutor);
 
         // TTY status
         updateTTY();
@@ -366,7 +363,7 @@
     }
 
     private void updateAlarm() {
-        final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+        final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(mUserTracker.getUserId());
         final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
         int zen = mZenController.getZen();
         final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
@@ -553,15 +550,15 @@
         });
     }
 
-    private final SynchronousUserSwitchObserver mUserSwitchListener =
-            new SynchronousUserSwitchObserver() {
+    private final UserTracker.Callback mUserSwitchListener =
+            new UserTracker.Callback() {
                 @Override
-                public void onUserSwitching(int newUserId) throws RemoteException {
+                public void onUserChanging(int newUser, Context userContext) {
                     mHandler.post(() -> mUserInfoController.reloadUserInfo());
                 }
 
                 @Override
-                public void onUserSwitchComplete(int newUserId) throws RemoteException {
+                public void onUserChanged(int newUser, Context userContext) {
                     mHandler.post(() -> {
                         updateAlarm();
                         updateManagedProfile();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index ee8b861..c01137a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+
 import static java.lang.Float.isNaN;
 
 import android.animation.Animator;
@@ -53,6 +55,11 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -70,6 +77,8 @@
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+
 /**
  * Controls both the scrim behind the notifications and in front of the notifications (when a
  * security method gets shown).
@@ -137,25 +146,11 @@
     private boolean mTransitioningToFullShade;
 
     /**
-     * Is there currently an unocclusion animation running. Used to avoid bright flickers
-     * of the notification scrim.
-     */
-    private boolean mUnOcclusionAnimationRunning;
-
-    /**
      * The percentage of the bouncer which is hidden. If 1, the bouncer is completely hidden. If
      * 0, the bouncer is visible.
      */
     @FloatRange(from = 0, to = 1)
-    private float mBouncerHiddenFraction = KeyguardBouncer.EXPANSION_HIDDEN;
-
-    /**
-     * Set whether an unocclusion animation is currently running on the notification panel. Used
-     * to avoid bright flickers of the notification scrim.
-     */
-    public void setUnocclusionAnimationRunning(boolean unocclusionAnimationRunning) {
-        mUnOcclusionAnimationRunning = unocclusionAnimationRunning;
-    }
+    private float mBouncerHiddenFraction = KeyguardBouncerConstants.EXPANSION_HIDDEN;
 
     @IntDef(prefix = {"VISIBILITY_"}, value = {
             TRANSPARENT,
@@ -264,6 +259,28 @@
     private boolean mWakeLockHeld;
     private boolean mKeyguardOccluded;
 
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private CoroutineDispatcher mMainDispatcher;
+    private boolean mIsBouncerToGoneTransitionStarted = false;
+    private boolean mIsBouncerToGoneTransitionRunning = false;
+    private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
+    private final Consumer<Float> mScrimAlphaConsumer =
+            (Float alpha) -> {
+                mScrimInFront.setViewAlpha(0f);
+                mNotificationsScrim.setViewAlpha(0f);
+                mScrimBehind.setViewAlpha(alpha);
+            };
+    final Consumer<TransitionStep> mPrimaryBouncerToGoneTransition =
+            (TransitionStep step) -> {
+                mIsBouncerToGoneTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+                mIsBouncerToGoneTransitionStarted =
+                    step.getTransitionState() == TransitionState.STARTED;
+                if (mIsBouncerToGoneTransitionStarted) {
+                    transitionTo(ScrimState.UNLOCKED);
+                }
+            };
+
     @Inject
     public ScrimController(
             LightBarController lightBarController,
@@ -278,7 +295,10 @@
             @Main Executor mainExecutor,
             ScreenOffAnimationController screenOffAnimationController,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
+            @Main CoroutineDispatcher mainDispatcher) {
         mScrimStateListener = lightBarController::setScrimState;
         mDefaultScrimAlpha = BUSY_SCRIM_ALPHA;
 
@@ -317,6 +337,9 @@
             }
         });
         mColors = new GradientColors();
+        mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+        mMainDispatcher = mainDispatcher;
     }
 
     /**
@@ -356,6 +379,11 @@
         for (ScrimState state : ScrimState.values()) {
             state.prepare(state);
         }
+
+        collectFlow(behindScrim, mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition(),
+                mPrimaryBouncerToGoneTransition, mMainDispatcher);
+        collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha(),
+                mScrimAlphaConsumer, mMainDispatcher);
     }
 
     /**
@@ -378,6 +406,11 @@
     }
 
     public void transitionTo(ScrimState state, Callback callback) {
+        if (mIsBouncerToGoneTransitionRunning) {
+            Log.i(TAG, "Skipping transition to: " + state
+                    + " while mIsBouncerToGoneTransitionRunning");
+            return;
+        }
         if (state == mState) {
             // Call the callback anyway, unless it's already enqueued
             if (callback != null && mCallback != callback) {
@@ -531,10 +564,6 @@
         }
     }
 
-    public void onExpandingFinished() {
-        setUnocclusionAnimationRunning(false);
-    }
-
     @VisibleForTesting
     protected void onHideWallpaperTimeout() {
         if (mState != ScrimState.AOD && mState != ScrimState.PULSING) {
@@ -790,27 +819,36 @@
             if (!mScreenOffAnimationController.shouldExpandNotifications()
                     && !mAnimatingPanelExpansionOnUnlock
                     && !occluding) {
-                float behindFraction = getInterpolatedFraction();
-                behindFraction = (float) Math.pow(behindFraction, 0.8f);
                 if (mClipsQsScrim) {
+                    float behindFraction = getInterpolatedFraction();
+                    behindFraction = (float) Math.pow(behindFraction, 0.8f);
                     mBehindAlpha = mTransparentScrimBackground ? 0 : 1;
                     mNotificationsAlpha =
                             mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
                 } else {
-                    mBehindAlpha =
-                            mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
-                    // Delay fade-in of notification scrim a bit further, to coincide with the
-                    // view fade in. Otherwise the empty panel can be quite jarring.
-                    mNotificationsAlpha = mTransparentScrimBackground
-                            ? 0 : MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f,
-                            mPanelExpansionFraction);
+                    if (mTransparentScrimBackground) {
+                        mBehindAlpha = 0;
+                        mNotificationsAlpha = 0;
+                    } else {
+                        float behindFraction = MathUtils
+                                .constrainedMap(0f, 1f, 0f, 0.3f, mPanelExpansionFraction);
+                        if (!mIsBouncerToGoneTransitionStarted) {
+                            mBehindAlpha = behindFraction * mDefaultScrimAlpha;
+                        }
+                        // Delay fade-in of notification scrim a bit further, to coincide with the
+                        // behind scrim finishing fading in.
+                        // Also to coincide with the view starting to fade in, otherwise the empty
+                        // panel can be quite jarring.
+                        mNotificationsAlpha = MathUtils
+                                .constrainedMap(0f, 1f, 0.3f, 0.75f, mPanelExpansionFraction);
+                    }
                 }
                 mBehindTint = mState.getBehindTint();
                 mInFrontAlpha = 0;
             }
 
             if (mState == ScrimState.DREAMING
-                    && mBouncerHiddenFraction != KeyguardBouncer.EXPANSION_HIDDEN) {
+                    && mBouncerHiddenFraction != KeyguardBouncerConstants.EXPANSION_HIDDEN) {
                 final float interpolatedFraction =
                         BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(
                                 mBouncerHiddenFraction);
@@ -866,13 +904,6 @@
             if (mKeyguardOccluded || hideNotificationScrim) {
                 mNotificationsAlpha = 0;
             }
-            if (mUnOcclusionAnimationRunning && mState == ScrimState.KEYGUARD) {
-                // We're unoccluding the keyguard and don't want to have a bright flash.
-                mNotificationsAlpha = ScrimState.KEYGUARD.getNotifAlpha();
-                mNotificationsTint = ScrimState.KEYGUARD.getNotifTint();
-                mBehindAlpha = ScrimState.KEYGUARD.getBehindAlpha();
-                mBehindTint = ScrimState.KEYGUARD.getBehindTint();
-            }
         }
         if (mState != ScrimState.UNLOCKED) {
             mAnimatingPanelExpansionOnUnlock = false;
@@ -1141,7 +1172,9 @@
             Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint",
                     Color.alpha(tint));
             scrimView.setTint(tint);
-            scrimView.setViewAlpha(alpha);
+            if (!mIsBouncerToGoneTransitionRunning) {
+                scrimView.setViewAlpha(alpha);
+            }
         } else {
             scrim.setAlpha(alpha);
         }
@@ -1489,6 +1522,9 @@
     }
 
     public void setKeyguardOccluded(boolean keyguardOccluded) {
+        if (mKeyguardOccluded == keyguardOccluded) {
+            return;
+        }
         mKeyguardOccluded = keyguardOccluded;
         updateScrims();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 24ad55d..11863627 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -501,7 +501,7 @@
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
             if (mStatusBarPipelineFlags.useNewWifiIcon()) {
-                throw new IllegalStateException("Attempting to add a mobile icon while the new "
+                throw new IllegalStateException("Attempting to add a wifi icon while the new "
                         + "icons are enabled is not supported");
             }
 
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..0727c5a 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,7 @@
         for (int i = currentSlots.size() - 1; i >= 0; i--) {
             Slot s = currentSlots.get(i);
             slotsToReAdd.put(s, s.getHolderList());
-            removeAllIconsForSlot(s.getName());
+            removeAllIconsForSlot(s.getName(), /* fromNewPipeline */ false);
         }
 
         // Add them all back
@@ -285,7 +285,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, /* fromNewPipeline */ true);
 
         Collections.reverse(subIds);
 
@@ -428,6 +428,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 +452,18 @@
     /** */
     @Override
     public void removeAllIconsForSlot(String slotName) {
+        removeAllIconsForSlot(slotName, /* fromNewPipeline */ false);
+    }
+
+    private void removeAllIconsForSlot(String slotName, boolean fromNewPipeline) {
+        // If the new pipeline is on for this icon, don't allow removal, since the new pipeline
+        // will never call this method
+        if (!fromNewPipeline && 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 d480fab..3c32131 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,6 +18,7 @@
 
 import static android.view.WindowInsets.Type.navigationBars;
 
+import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
 import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
@@ -36,7 +37,8 @@
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
 import android.view.WindowManagerGlobal;
-import android.window.OnBackInvokedCallback;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
 import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
@@ -58,10 +60,13 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.BouncerView;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.navigationbar.TaskbarDelegate;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
@@ -75,7 +80,6 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.unfold.FoldAodAnimationController;
@@ -126,7 +130,6 @@
     private final ConfigurationController mConfigurationController;
     private final NavigationModeController mNavigationModeController;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
-    private final KeyguardBouncer.Factory mKeyguardBouncerFactory;
     private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
     private final DreamOverlayStateController mDreamOverlayStateController;
     @Nullable
@@ -134,6 +137,7 @@
     private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController;
     private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
     private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final BouncerView mPrimaryBouncerView;
     private final Lazy<ShadeController> mShadeController;
 
@@ -182,8 +186,7 @@
                         isVisible && mDreamOverlayStateController.isOverlayActive());
 
                 if (!isVisible) {
-                    mCentralSurfaces.setPrimaryBouncerHiddenFraction(
-                            KeyguardBouncer.EXPANSION_HIDDEN);
+                    mCentralSurfaces.setPrimaryBouncerHiddenFraction(EXPANSION_HIDDEN);
                 }
 
                 /* Register predictive back callback when keyguard becomes visible, and unregister
@@ -196,11 +199,38 @@
             }
     };
 
-    private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
-        if (DEBUG) {
-            Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()");
+    private final OnBackAnimationCallback mOnBackInvokedCallback = new OnBackAnimationCallback() {
+        @Override
+        public void onBackInvoked() {
+            if (DEBUG) {
+                Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()");
+            }
+            onBackPressed();
+            if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+                mPrimaryBouncerView.getDelegate().getBackCallback().onBackInvoked();
+            }
         }
-        onBackPressed();
+
+        @Override
+        public void onBackProgressed(BackEvent event) {
+            if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+                mPrimaryBouncerView.getDelegate().getBackCallback().onBackProgressed(event);
+            }
+        }
+
+        @Override
+        public void onBackCancelled() {
+            if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+                mPrimaryBouncerView.getDelegate().getBackCallback().onBackCancelled();
+            }
+        }
+
+        @Override
+        public void onBackStarted(BackEvent event) {
+            if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+                mPrimaryBouncerView.getDelegate().getBackCallback().onBackStarted(event);
+            }
+        }
     };
     private boolean mIsBackCallbackRegistered = false;
 
@@ -226,7 +256,6 @@
 
     private View mNotificationContainer;
 
-    @Nullable protected KeyguardBouncer mPrimaryBouncer;
     protected boolean mRemoteInputActive;
     private boolean mGlobalActionsVisible = false;
     private boolean mLastGlobalActionsVisible = false;
@@ -251,8 +280,8 @@
     private boolean mLastScreenOffAnimationPlaying;
     private float mQsExpansion;
     final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
-    private boolean mIsModernBouncerEnabled;
-    private boolean mIsUnoccludeTransitionFlagEnabled;
+    private boolean mIsModernAlternateBouncerEnabled;
+    private boolean mIsBackAnimationEnabled;
 
     private OnDismissAction mAfterKeyguardGoneAction;
     private Runnable mKeyguardGoneCancelAction;
@@ -269,8 +298,9 @@
     private final LatencyTracker mLatencyTracker;
     private final KeyguardSecurityModel mKeyguardSecurityModel;
     @Nullable private KeyguardBypassController mBypassController;
-    @Nullable private AlternateBouncer mAlternateBouncer;
+    @Nullable private OccludingAppBiometricUI mOccludingAppBiometricUI;
 
+    @Nullable private TaskbarDelegate mTaskbarDelegate;
     private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
             new KeyguardUpdateMonitorCallback() {
         @Override
@@ -297,7 +327,6 @@
             NotificationShadeWindowController notificationShadeWindowController,
             KeyguardStateController keyguardStateController,
             NotificationMediaManager notificationMediaManager,
-            KeyguardBouncer.Factory keyguardBouncerFactory,
             KeyguardMessageAreaController.Factory keyguardMessageAreaFactory,
             Optional<SysUIUnfoldComponent> sysUIUnfoldComponent,
             Lazy<ShadeController> shadeController,
@@ -306,7 +335,8 @@
             FeatureFlags featureFlags,
             PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
             PrimaryBouncerInteractor primaryBouncerInteractor,
-            BouncerView primaryBouncerView) {
+            BouncerView primaryBouncerView,
+            AlternateBouncerInteractor alternateBouncerInteractor) {
         mContext = context;
         mViewMediatorCallback = callback;
         mLockPatternUtils = lockPatternUtils;
@@ -319,7 +349,6 @@
         mKeyguardUpdateManager = keyguardUpdateMonitor;
         mStatusBarStateController = sysuiStatusBarStateController;
         mDockManager = dockManager;
-        mKeyguardBouncerFactory = keyguardBouncerFactory;
         mKeyguardMessageAreaFactory = keyguardMessageAreaFactory;
         mShadeController = shadeController;
         mLatencyTracker = latencyTracker;
@@ -329,8 +358,10 @@
         mPrimaryBouncerView = primaryBouncerView;
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
-        mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
-        mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+        mIsModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER);
+        mAlternateBouncerInteractor = alternateBouncerInteractor;
+        mIsBackAnimationEnabled =
+                featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM);
     }
 
     @Override
@@ -344,11 +375,7 @@
         mBiometricUnlockController = biometricUnlockController;
 
         ViewGroup container = mCentralSurfaces.getBouncerContainer();
-        if (mIsModernBouncerEnabled) {
-            mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
-        } else {
-            mPrimaryBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
-        }
+        mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
         mNotificationPanelViewController = notificationPanelViewController;
         if (shadeExpansionStateManager != null) {
             shadeExpansionStateManager.addExpansionListener(this);
@@ -363,23 +390,51 @@
     }
 
     /**
-     * Sets the given alt auth interceptor to null if it's the current auth interceptor. Else,
-     * does nothing.
+     * Sets the given legacy alternate bouncer to null if it's the current alternate bouncer. Else,
+     * does nothing. Only used if modern alternate bouncer is NOT enabled.
      */
-    public void removeAlternateAuthInterceptor(@NonNull AlternateBouncer authInterceptor) {
-        if (Objects.equals(mAlternateBouncer, authInterceptor)) {
-            mAlternateBouncer = null;
-            hideAlternateBouncer(true);
+    public void removeLegacyAlternateBouncer(
+            @NonNull LegacyAlternateBouncer alternateBouncerLegacy) {
+        if (!mIsModernAlternateBouncerEnabled) {
+            if (Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(),
+                    alternateBouncerLegacy)) {
+                mAlternateBouncerInteractor.setLegacyAlternateBouncer(null);
+                hideAlternateBouncer(true);
+            }
         }
     }
 
     /**
-     * Sets a new alt auth interceptor.
+     * Sets a new legacy alternate bouncer. Only used if mdoern alternate bouncer is NOT enable.
      */
-    public void setAlternateBouncer(@NonNull AlternateBouncer authInterceptor) {
-        if (!Objects.equals(mAlternateBouncer, authInterceptor)) {
-            mAlternateBouncer = authInterceptor;
-            hideAlternateBouncer(false);
+    public void setLegacyAlternateBouncer(@NonNull LegacyAlternateBouncer alternateBouncerLegacy) {
+        if (!mIsModernAlternateBouncerEnabled) {
+            if (!Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(),
+                    alternateBouncerLegacy)) {
+                mAlternateBouncerInteractor.setLegacyAlternateBouncer(alternateBouncerLegacy);
+                hideAlternateBouncer(false);
+            }
+        }
+
+    }
+
+
+    /**
+     * Sets the given OccludingAppBiometricUI to null if it's the current auth interceptor. Else,
+     * does nothing.
+     */
+    public void removeOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) {
+        if (Objects.equals(mOccludingAppBiometricUI, biometricUI)) {
+            mOccludingAppBiometricUI = null;
+        }
+    }
+
+    /**
+     * Sets a new OccludingAppBiometricUI.
+     */
+    public void setOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) {
+        if (!Objects.equals(mOccludingAppBiometricUI, biometricUI)) {
+            mOccludingAppBiometricUI = biometricUI;
         }
     }
 
@@ -438,6 +493,11 @@
         }
     }
 
+    private boolean shouldPlayBackAnimation() {
+        // Suppress back animation when bouncer shouldn't be dismissed on back invocation.
+        return !needsFullscreenBouncer() && mIsBackAnimationEnabled;
+    }
+
     @Override
     public void onDensityOrFontScaleChanged() {
         hideBouncer(true /* destroyView */);
@@ -451,7 +511,7 @@
                         || mNotificationPanelViewController.isExpanding());
 
         final boolean isUserTrackingStarted =
-                event.getFraction() != KeyguardBouncer.EXPANSION_HIDDEN && event.getTracking();
+                event.getFraction() != EXPANSION_HIDDEN && event.getTracking();
 
         return mKeyguardStateController.isShowing()
                 && !primaryBouncerIsOrWillBeShowing()
@@ -482,11 +542,7 @@
          * show if any subsequent events are to be handled.
          */
         if (beginShowingBouncer(event)) {
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
-            } else {
-                mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
-            }
+            mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
         }
 
         if (!primaryBouncerIsOrWillBeShowing()) {
@@ -494,17 +550,9 @@
         }
 
         if (mKeyguardStateController.isShowing()) {
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.setExpansion(fraction);
-            } else {
-                mPrimaryBouncerInteractor.setPanelExpansion(fraction);
-            }
+            mPrimaryBouncerInteractor.setPanelExpansion(fraction);
         } else {
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
-            } else {
-                mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
-            }
+            mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN);
         }
     }
 
@@ -516,6 +564,10 @@
         updateStates();
     }
 
+    public void setTaskbarDelegate(TaskbarDelegate taskbarDelegate) {
+        mTaskbarDelegate = taskbarDelegate;
+    }
+
     /**
      * Show the keyguard.  Will handle creating and attaching to the view manager
      * lazily.
@@ -534,24 +586,17 @@
 
     /**
      * Shows the notification keyguard or the bouncer depending on
-     * {@link KeyguardBouncer#needsFullscreenBouncer()}.
+     * {@link #needsFullscreenBouncer()}.
      */
     protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
         if (needsFullscreenBouncer() && !mDozing) {
             // The keyguard might be showing (already). So we need to hide it.
             mCentralSurfaces.hideKeyguard();
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.show(true /* resetSecuritySelection */);
-            } else {
-                mPrimaryBouncerInteractor.show(true);
-            }
+            mPrimaryBouncerInteractor.show(true);
         } else {
             mCentralSurfaces.showKeyguard();
             if (hideBouncerWhenShowing) {
                 hideBouncer(false /* destroyView */);
-                if (mPrimaryBouncer != null) {
-                    mPrimaryBouncer.prepare();
-                }
             }
         }
         updateStates();
@@ -566,18 +611,11 @@
      *                 {@see KeyguardBouncer#show(boolean, boolean)}
      */
     public void showBouncer(boolean scrimmed) {
-        if (canShowAlternateBouncer()) {
-            updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer());
-            return;
+        if (!mAlternateBouncerInteractor.show()) {
+            showPrimaryBouncer(scrimmed);
+        } else {
+            updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
         }
-
-        showPrimaryBouncer(scrimmed);
-    }
-
-    /** Whether we can show the alternate bouncer instead of the primary bouncer. */
-    public boolean canShowAlternateBouncer() {
-        return mAlternateBouncer != null
-                && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true);
     }
 
     /**
@@ -585,11 +623,7 @@
      */
     @VisibleForTesting
     void hideBouncer(boolean destroyView) {
-        if (mPrimaryBouncer != null) {
-            mPrimaryBouncer.hide(destroyView);
-        } else {
-            mPrimaryBouncerInteractor.hide();
-        }
+        mPrimaryBouncerInteractor.hide();
         if (mKeyguardStateController.isShowing()) {
             // If we were showing the bouncer and then aborting, we need to also clear out any
             // potential actions unless we actually unlocked.
@@ -608,11 +642,7 @@
         hideAlternateBouncer(false);
 
         if (mKeyguardStateController.isShowing()  && !isBouncerShowing()) {
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.show(false /* resetSecuritySelection */, scrimmed);
-            } else {
-                mPrimaryBouncerInteractor.show(scrimmed);
-            }
+            mPrimaryBouncerInteractor.show(scrimmed);
         }
         updateStates();
     }
@@ -641,44 +671,30 @@
                 mKeyguardGoneCancelAction = cancelAction;
                 mDismissActionWillAnimateOnKeyguard = r != null && r.willRunAnimationOnKeyguard();
 
-                // If there is an an alternate auth interceptor (like the UDFPS), show that one
+                // If there is an alternate auth interceptor (like the UDFPS), show that one
                 // instead of the bouncer.
-                if (canShowAlternateBouncer()) {
+                if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) {
                     if (!afterKeyguardGone) {
-                        if (mPrimaryBouncer != null) {
-                            mPrimaryBouncer.setDismissAction(mAfterKeyguardGoneAction,
-                                    mKeyguardGoneCancelAction);
-                        } else {
-                            mPrimaryBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
-                                    mKeyguardGoneCancelAction);
-                        }
+                        mPrimaryBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
+                                mKeyguardGoneCancelAction);
                         mAfterKeyguardGoneAction = null;
                         mKeyguardGoneCancelAction = null;
                     }
 
-                    updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer());
+                    updateAlternateBouncerShowing(mAlternateBouncerInteractor.show());
                     return;
                 }
 
                 if (afterKeyguardGone) {
                     // we'll handle the dismiss action after keyguard is gone, so just show the
                     // bouncer
-                    if (mPrimaryBouncer != null) {
-                        mPrimaryBouncer.show(false /* resetSecuritySelection */);
-                    } else {
-                        mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
-                    }
+                    mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
                 } else {
                     // after authentication success, run dismiss action with the option to defer
                     // hiding the keyguard based on the return value of the OnDismissAction
-                    if (mPrimaryBouncer != null) {
-                        mPrimaryBouncer.showWithDismissAction(mAfterKeyguardGoneAction,
-                                mKeyguardGoneCancelAction);
-                    } else {
-                        mPrimaryBouncerInteractor.setDismissAction(
-                                mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
-                        mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
-                    }
+                    mPrimaryBouncerInteractor.setDismissAction(
+                            mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
+                    mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
                     // bouncer will handle the dismiss action, so we no longer need to track it here
                     mAfterKeyguardGoneAction = null;
                     mKeyguardGoneCancelAction = null;
@@ -725,10 +741,7 @@
 
     @Override
     public void hideAlternateBouncer(boolean forceUpdateScrim) {
-        final boolean updateScrim = (mAlternateBouncer != null
-                && mAlternateBouncer.hideAlternateBouncer())
-                || forceUpdateScrim;
-        updateAlternateBouncerShowing(updateScrim);
+        updateAlternateBouncerShowing(mAlternateBouncerInteractor.hide() || forceUpdateScrim);
     }
 
     private void updateAlternateBouncerShowing(boolean updateScrim) {
@@ -738,13 +751,13 @@
             return;
         }
 
-        final boolean isShowingAlternateBouncer = isShowingAlternateBouncer();
+        final boolean isShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState();
         if (mKeyguardMessageAreaController != null) {
             mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer);
             mKeyguardMessageAreaController.setMessage("");
         }
         mBypassController.setAltBouncerShowing(isShowingAlternateBouncer);
-        mKeyguardUpdateManager.setUdfpsBouncerShowing(isShowingAlternateBouncer);
+        mKeyguardUpdateManager.setAlternateBouncerShowing(isShowingAlternateBouncer);
 
         if (updateScrim) {
             mCentralSurfaces.updateScrimController();
@@ -781,11 +794,7 @@
 
     @Override
     public void onFinishedGoingToSleep() {
-        if (mPrimaryBouncer != null) {
-            mPrimaryBouncer.onScreenTurnedOff();
-        } else {
-            mPrimaryBouncerInteractor.onScreenTurnedOff();
-        }
+        mPrimaryBouncerInteractor.hide();
     }
 
     @Override
@@ -869,21 +878,12 @@
             // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
             reset(isOccluding /* hideBouncerWhenShowing*/);
         }
-        if (!mIsUnoccludeTransitionFlagEnabled) {
-            if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
-                mCentralSurfaces.animateKeyguardUnoccluding();
-            }
-        }
     }
 
     @Override
     public void startPreHideAnimation(Runnable finishRunnable) {
         if (primaryBouncerIsShowing()) {
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.startPreHideAnimation(finishRunnable);
-            } else {
-                mPrimaryBouncerInteractor.startDisappearAnimation(finishRunnable);
-            }
+            mPrimaryBouncerInteractor.startDisappearAnimation(finishRunnable);
             mNotificationPanelViewController.startBouncerPreHideAnimation();
 
             // We update the state (which will show the keyguard) only if an animation will run on
@@ -991,17 +991,7 @@
     }
 
     public void onThemeChanged() {
-        if (mIsModernBouncerEnabled) {
-            updateResources();
-            return;
-        }
-        boolean wasShowing = primaryBouncerIsShowing();
-        boolean wasScrimmed = primaryBouncerIsScrimmed();
-
-        hideBouncer(true /* destroyView */);
-        mPrimaryBouncer.prepare();
-
-        if (wasShowing) showPrimaryBouncer(wasScrimmed);
+        updateResources();
     }
 
     public void onKeyguardFadedAway() {
@@ -1046,10 +1036,6 @@
      * WARNING: This method might cause Binder calls.
      */
     public boolean isSecure() {
-        if (mPrimaryBouncer != null) {
-            return mPrimaryBouncer.isSecure();
-        }
-
         return mKeyguardSecurityModel.getSecurityMode(
                 KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None;
     }
@@ -1087,7 +1073,7 @@
             if (hideImmediately) {
                 mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
             } else {
-                mNotificationPanelViewController.expandWithoutQs();
+                mNotificationPanelViewController.expandShadeToNotifications();
             }
         }
         return;
@@ -1095,7 +1081,7 @@
 
     @Override
     public boolean isBouncerShowing() {
-        return primaryBouncerIsShowing() || isShowingAlternateBouncer();
+        return primaryBouncerIsShowing() || mAlternateBouncerInteractor.isVisibleState();
     }
 
     @Override
@@ -1104,10 +1090,8 @@
     }
 
     public boolean isFullscreenBouncer() {
-        if (mPrimaryBouncerView.getDelegate() != null) {
-            return mPrimaryBouncerView.getDelegate().isFullScreenBouncer();
-        }
-        return mPrimaryBouncer != null && mPrimaryBouncer.isFullscreenBouncer();
+        return mPrimaryBouncerView.getDelegate() != null
+                && mPrimaryBouncerView.getDelegate().isFullScreenBouncer();
     }
 
     /**
@@ -1163,17 +1147,9 @@
                 != (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
                 || mFirstUpdate) {
             if (primaryBouncerDismissible || !showing || remoteInputActive) {
-                if (mPrimaryBouncer != null) {
-                    mPrimaryBouncer.setBackButtonEnabled(true);
-                } else {
-                    mPrimaryBouncerInteractor.setBackButtonEnabled(true);
-                }
+                mPrimaryBouncerInteractor.setBackButtonEnabled(true);
             } else {
-                if (mPrimaryBouncer != null) {
-                    mPrimaryBouncer.setBackButtonEnabled(false);
-                } else {
-                    mPrimaryBouncerInteractor.setBackButtonEnabled(false);
-                }
+                mPrimaryBouncerInteractor.setBackButtonEnabled(false);
             }
         }
 
@@ -1217,7 +1193,8 @@
      * Updates the visibility of the nav bar window (which will cause insets changes).
      */
     protected void updateNavigationBarVisibility(boolean navBarVisible) {
-        if (mCentralSurfaces.getNavigationBarView() != null) {
+        if (mCentralSurfaces.getNavigationBarView() != null
+                || (mTaskbarDelegate != null && mTaskbarDelegate.isInitialized())) {
             if (navBarVisible) {
                 long delay = getNavBarShowDelay();
                 if (delay == 0) {
@@ -1267,27 +1244,21 @@
     }
 
     public boolean shouldDismissOnMenuPressed() {
-        if (mPrimaryBouncerView.getDelegate() != null) {
-            return mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
-        }
-        return mPrimaryBouncer != null && mPrimaryBouncer.shouldDismissOnMenuPressed();
+        return mPrimaryBouncerView.getDelegate() != null
+                && mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
     }
 
     public boolean interceptMediaKey(KeyEvent event) {
-        if (mPrimaryBouncerView.getDelegate() != null) {
-            return mPrimaryBouncerView.getDelegate().interceptMediaKey(event);
-        }
-        return mPrimaryBouncer != null && mPrimaryBouncer.interceptMediaKey(event);
+        return mPrimaryBouncerView.getDelegate() != null
+                && mPrimaryBouncerView.getDelegate().interceptMediaKey(event);
     }
 
     /**
      * @return true if the pre IME back event should be handled
      */
     public boolean dispatchBackKeyEventPreIme() {
-        if (mPrimaryBouncerView.getDelegate() != null) {
-            return mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme();
-        }
-        return mPrimaryBouncer != null && mPrimaryBouncer.dispatchBackKeyEventPreIme();
+        return mPrimaryBouncerView.getDelegate() != null
+                && mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme();
     }
 
     public void readyForKeyguardDone() {
@@ -1333,13 +1304,9 @@
      * fingerprint.
      */
     public void notifyKeyguardAuthenticated(boolean strongAuth) {
-        if (mPrimaryBouncer != null) {
-            mPrimaryBouncer.notifyKeyguardAuthenticated(strongAuth);
-        } else {
-            mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
-        }
+        mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
 
-        if (mAlternateBouncer != null && isShowingAlternateBouncer()) {
+        if (mAlternateBouncerInteractor.isVisibleState()) {
             hideAlternateBouncer(false);
             executeAfterKeyguardGoneAction();
         }
@@ -1347,16 +1314,12 @@
 
     /** Display security message to relevant KeyguardMessageArea. */
     public void setKeyguardMessage(String message, ColorStateList colorState) {
-        if (isShowingAlternateBouncer()) {
+        if (mAlternateBouncerInteractor.isVisibleState()) {
             if (mKeyguardMessageAreaController != null) {
                 mKeyguardMessageAreaController.setMessage(message);
             }
         } else {
-            if (mPrimaryBouncer != null) {
-                mPrimaryBouncer.showMessage(message, colorState);
-            } else {
-                mPrimaryBouncerInteractor.showMessage(message, colorState);
-            }
+            mPrimaryBouncerInteractor.showMessage(message, colorState);
         }
     }
 
@@ -1412,15 +1375,12 @@
      * configuration.
      */
     public void updateResources() {
-        if (mPrimaryBouncer != null) {
-            mPrimaryBouncer.updateResources();
-        } else {
-            mPrimaryBouncerInteractor.updateResources();
-        }
+        mPrimaryBouncerInteractor.updateResources();
     }
 
     public void dump(PrintWriter pw) {
         pw.println("StatusBarKeyguardViewManager:");
+        pw.println("  mIsModernAlternateBouncerEnabled: " + mIsModernAlternateBouncerEnabled);
         pw.println("  mRemoteInputActive: " + mRemoteInputActive);
         pw.println("  mDozing: " + mDozing);
         pw.println("  mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction);
@@ -1433,14 +1393,9 @@
             pw.println("      " + callback);
         }
 
-        if (mPrimaryBouncer != null) {
-            pw.println("PrimaryBouncer:");
-            mPrimaryBouncer.dump(pw);
-        }
-
-        if (mAlternateBouncer != null) {
-            pw.println("AlternateBouncer:");
-            mAlternateBouncer.dump(pw);
+        if (mOccludingAppBiometricUI != null) {
+            pw.println("mOccludingAppBiometricUI:");
+            mOccludingAppBiometricUI.dump(pw);
         }
     }
 
@@ -1487,19 +1442,17 @@
         }
     }
 
-    @Nullable
-    public KeyguardBouncer getPrimaryBouncer() {
-        return mPrimaryBouncer;
-    }
-
-    public boolean isShowingAlternateBouncer() {
-        return mAlternateBouncer != null && mAlternateBouncer.isShowingAlternateBouncer();
-    }
-
     /**
-     * Forward touches to callbacks.
+     * For any touches on the NPVC, show the primary bouncer if the alternate bouncer is currently
+     * showing.
      */
     public void onTouch(MotionEvent event) {
+        if (mAlternateBouncerInteractor.isVisibleState()
+                && mAlternateBouncerInteractor.hasAlternateBouncerShownWithMinTime()) {
+            showPrimaryBouncer(true);
+        }
+
+        // Forward NPVC touches to callbacks in case they want to respond to touches
         for (KeyguardViewManagerCallback callback: mCallbacks) {
             callback.onTouch(event);
         }
@@ -1507,11 +1460,7 @@
 
     /** Update keyguard position based on a tapped X coordinate. */
     public void updateKeyguardPosition(float x) {
-        if (mPrimaryBouncer != null) {
-            mPrimaryBouncer.updateKeyguardPosition(x);
-        } else {
-            mPrimaryBouncerInteractor.setKeyguardPosition(x);
-        }
+        mPrimaryBouncerInteractor.setKeyguardPosition(x);
     }
 
     private static class DismissWithActionRequest {
@@ -1542,8 +1491,8 @@
      */
     public void requestFp(boolean request, int udfpsColor) {
         mKeyguardUpdateManager.requestFingerprintAuthOnOccludingApp(request);
-        if (mAlternateBouncer != null) {
-            mAlternateBouncer.requestUdfps(request, udfpsColor);
+        if (mOccludingAppBiometricUI != null) {
+            mOccludingAppBiometricUI.requestUdfps(request, udfpsColor);
         }
     }
 
@@ -1551,56 +1500,35 @@
      * Returns if bouncer expansion is between 0 and 1 non-inclusive.
      */
     public boolean isPrimaryBouncerInTransit() {
-        if (mPrimaryBouncer != null) {
-            return mPrimaryBouncer.inTransit();
-        } else {
-            return mPrimaryBouncerInteractor.isInTransit();
-        }
+        return mPrimaryBouncerInteractor.isInTransit();
     }
 
     /**
      * Returns if bouncer is showing
      */
     public boolean primaryBouncerIsShowing() {
-        if (mPrimaryBouncer != null) {
-            return mPrimaryBouncer.isShowing();
-        } else {
-            return mPrimaryBouncerInteractor.isFullyShowing();
-        }
+        return mPrimaryBouncerInteractor.isFullyShowing();
     }
 
     /**
      * Returns if bouncer is scrimmed
      */
     public boolean primaryBouncerIsScrimmed() {
-        if (mPrimaryBouncer != null) {
-            return mPrimaryBouncer.isScrimmed();
-        } else {
-            return mPrimaryBouncerInteractor.isScrimmed();
-        }
+        return mPrimaryBouncerInteractor.isScrimmed();
     }
 
     /**
      * Returns if bouncer is animating away
      */
     public boolean bouncerIsAnimatingAway() {
-        if (mPrimaryBouncer != null) {
-            return mPrimaryBouncer.isAnimatingAway();
-        } else {
-            return mPrimaryBouncerInteractor.isAnimatingAway();
-        }
-
+        return mPrimaryBouncerInteractor.isAnimatingAway();
     }
 
     /**
      * Returns if bouncer will dismiss with action
      */
     public boolean primaryBouncerWillDismissWithAction() {
-        if (mPrimaryBouncer != null) {
-            return mPrimaryBouncer.willDismissWithAction();
-        } else {
-            return mPrimaryBouncerInteractor.willDismissWithAction();
-        }
+        return mPrimaryBouncerInteractor.willDismissWithAction();
     }
 
     /**
@@ -1614,10 +1542,9 @@
     }
 
     /**
-     * Delegate used to send show and hide events to an alternate authentication method instead of
-     * the regular pin/pattern/password bouncer.
+     * @Deprecated Delegate used to send show and hide events to an alternate bouncer.
      */
-    public interface AlternateBouncer {
+    public interface LegacyAlternateBouncer {
         /**
          * Show alternate authentication bouncer.
          * @return whether alternate auth method was newly shown
@@ -1634,7 +1561,13 @@
          * @return true if the alternate auth bouncer is showing
          */
         boolean isShowingAlternateBouncer();
+    }
 
+    /**
+     * Delegate used to send show and hide events to an alternate authentication method instead of
+     * the regular pin/pattern/password bouncer.
+     */
+    public interface OccludingAppBiometricUI {
         /**
          * Use when an app occluding the keyguard would like to give the user ability to
          * unlock the device using udfps.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index be6e0cc..078a00d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -53,6 +53,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationClickNotifier;
@@ -118,6 +119,7 @@
     private final NotificationPanelViewController mNotificationPanel;
     private final ActivityLaunchAnimator mActivityLaunchAnimator;
     private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
+    private final UserTracker mUserTracker;
     private final OnUserInteractionCallback mOnUserInteractionCallback;
 
     private boolean mIsCollapsingToShowActivityOverLockscreen;
@@ -153,7 +155,8 @@
             ActivityLaunchAnimator activityLaunchAnimator,
             NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
             LaunchFullScreenIntentProvider launchFullScreenIntentProvider,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            UserTracker userTracker) {
         mContext = context;
         mMainThreadHandler = mainThreadHandler;
         mUiBgExecutor = uiBgExecutor;
@@ -184,6 +187,7 @@
         mNotificationPanel = panel;
         mActivityLaunchAnimator = activityLaunchAnimator;
         mNotificationAnimationProvider = notificationAnimationProvider;
+        mUserTracker = userTracker;
 
         launchFullScreenIntentProvider.registerListener(entry -> launchFullScreenIntent(entry));
     }
@@ -518,7 +522,7 @@
                             intent.getPackage(),
                             (adapter) -> tsb.startActivities(
                                     getActivityOptions(mCentralSurfaces.getDisplayId(), adapter),
-                                    UserHandle.CURRENT));
+                                    mUserTracker.getUserHandle()));
                 });
                 return true;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index da1c361..4eed487 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -40,6 +40,7 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
+import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -102,6 +103,7 @@
     private final IStatusBarService mBarService;
     private final DynamicPrivacyController mDynamicPrivacyController;
     private final NotificationListContainer mNotifListContainer;
+    private final QuickSettingsController mQsController;
 
     protected boolean mVrMode;
 
@@ -109,6 +111,7 @@
     StatusBarNotificationPresenter(
             Context context,
             NotificationPanelViewController panel,
+            QuickSettingsController quickSettingsController,
             HeadsUpManagerPhone headsUp,
             NotificationShadeWindowView statusBarWindow,
             ActivityStarter activityStarter,
@@ -136,6 +139,7 @@
         mActivityStarter = activityStarter;
         mKeyguardStateController = keyguardStateController;
         mNotificationPanel = panel;
+        mQsController = quickSettingsController;
         mHeadsUpManager = headsUp;
         mDynamicPrivacyController = dynamicPrivacyController;
         mKeyguardIndicationController = keyguardIndicationController;
@@ -191,7 +195,7 @@
     private void maybeClosePanelForShadeEmptied() {
         if (CLOSE_PANEL_WHEN_EMPTIED
                 && !mNotificationPanel.isTracking()
-                && !mNotificationPanel.isQsExpanded()
+                && !mQsController.getExpanded()
                 && mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED
                 && !isCollapsing()) {
             mStatusBarStateController.setState(StatusBarState.KEYGUARD);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
index ae48c2d3..50cce45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
@@ -17,5 +17,5 @@
 
 public interface StatusBarWindowCallback {
     void onStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean bouncerShowing,
-            boolean isDozing, boolean panelExpanded);
+            boolean isDozing, boolean panelExpanded, boolean isDreaming);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
index 6cd8c78..9e6bb20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.view.InsetsFlags
 import android.view.InsetsVisibilities
+import android.view.ViewDebug
 import android.view.WindowInsetsController.Appearance
 import android.view.WindowInsetsController.Behavior
 import com.android.internal.statusbar.LetterboxDetails
@@ -148,4 +150,20 @@
 ) {
     val letterboxesArray = letterboxes.toTypedArray()
     val appearanceRegionsArray = appearanceRegions.toTypedArray()
+    override fun toString(): String {
+        val appearanceToString =
+                ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
+        return """SystemBarAttributesParams(
+            displayId=$displayId,
+            appearance=$appearanceToString,
+            appearanceRegions=$appearanceRegions,
+            navbarColorManagedByIme=$navbarColorManagedByIme,
+            behavior=$behavior,
+            requestedVisibilities=$requestedVisibilities,
+            packageName='$packageName',
+            letterboxes=$letterboxes,
+            letterboxesArray=${letterboxesArray.contentToString()},
+            appearanceRegionsArray=${appearanceRegionsArray.contentToString()}
+            )""".trimMargin()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 608bfa6..924ae4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -45,8 +45,11 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.util.DialogKt;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -68,6 +71,7 @@
     private static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true;
 
     private final Context mContext;
+    private final FeatureFlags mFeatureFlags;
     @Nullable private final DismissReceiver mDismissReceiver;
     private final Handler mHandler = new Handler();
     private final SystemUIDialogManager mDialogManager;
@@ -96,16 +100,23 @@
         // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
         // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
         // the content and attach listeners.
-        this(context, theme, dismissOnDeviceLock, Dependency.get(SystemUIDialogManager.class),
-                Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class),
+        this(context, theme, dismissOnDeviceLock,
+                Dependency.get(FeatureFlags.class),
+                Dependency.get(SystemUIDialogManager.class),
+                Dependency.get(SysUiState.class),
+                Dependency.get(BroadcastDispatcher.class),
                 Dependency.get(DialogLaunchAnimator.class));
     }
 
     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock,
-            SystemUIDialogManager dialogManager, SysUiState sysUiState,
-            BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) {
+            FeatureFlags featureFlags,
+            SystemUIDialogManager dialogManager,
+            SysUiState sysUiState,
+            BroadcastDispatcher broadcastDispatcher,
+            DialogLaunchAnimator dialogLaunchAnimator) {
         super(context, theme);
         mContext = context;
+        mFeatureFlags = featureFlags;
 
         applyFlags(this);
         WindowManager.LayoutParams attrs = getWindow().getAttributes();
@@ -130,6 +141,12 @@
         for (int i = 0; i < mOnCreateRunnables.size(); i++) {
             mOnCreateRunnables.get(i).run();
         }
+        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM)) {
+            DialogKt.registerAnimationOnBackInvoked(
+                    /* dialog = */ this,
+                    /* targetView = */ getWindow().getDecorView()
+            );
+        }
     }
 
     private void updateWindowSize() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
index 64b04e9..8e59a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
@@ -22,10 +22,11 @@
 
 import com.android.keyguard.LockIconViewController;
 import com.android.systemui.biometrics.AuthRippleController;
-import com.android.systemui.shade.LargeScreenShadeHeaderController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
+import com.android.systemui.shade.QuickSettingsController;
+import com.android.systemui.shade.ShadeHeaderController;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.core.StatusBarInitializer;
@@ -113,6 +114,9 @@
      */
     NotificationPanelViewController getNotificationPanelViewController();
 
+    /** Creates a QuickSettingsController. */
+    QuickSettingsController getQuickSettingsController();
+
     /**
      * Creates a LockIconViewController. Must be init after creation.
      */
@@ -134,9 +138,9 @@
     CentralSurfacesCommandQueueCallbacks getCentralSurfacesCommandQueueCallbacks();
 
     /**
-     * Creates a {@link LargeScreenShadeHeaderController}.
+     * Creates a {@link ShadeHeaderController}.
      */
-    LargeScreenShadeHeaderController getLargeScreenShadeHeaderController();
+    ShadeHeaderController getLargeScreenShadeHeaderController();
 
     /**
      * Creates a new {@link CollapsedStatusBarFragment} each time it's called. See
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 206c0aa..0929233 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -20,9 +20,11 @@
 import android.content.ContentResolver;
 import android.os.Handler;
 import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewStub;
 
+import androidx.constraintlayout.motion.widget.MotionLayout;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.LockIconView;
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterView;
@@ -31,7 +33,6 @@
 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.statusbar.StatusBarStateController;
 import com.android.systemui.privacy.OngoingPrivacyChip;
 import com.android.systemui.settings.UserTracker;
@@ -46,7 +47,6 @@
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.OperatorNameViewController;
-import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.settings.SecureSettings;
@@ -84,9 +85,7 @@
 @Module(subcomponents = StatusBarFragmentComponent.class)
 public abstract class StatusBarViewModule {
 
-    public static final String LARGE_SCREEN_SHADE_HEADER = "large_screen_shade_header";
-    private static final String SPLIT_SHADE_BATTERY_VIEW = "split_shade_battery_view";
-    public static final String LARGE_SCREEN_BATTERY_CONTROLLER = "split_shade_battery_controller";
+    public static final String SHADE_HEADER = "large_screen_shade_header";
     public static final String STATUS_BAR_FRAGMENT = "status_bar_fragment";
 
     /** */
@@ -170,17 +169,15 @@
 
     /** */
     @Provides
-    @Named(LARGE_SCREEN_SHADE_HEADER)
+    @Named(SHADE_HEADER)
     @CentralSurfacesComponent.CentralSurfacesScope
-    public static View getLargeScreenShadeHeaderBarView(
+    public static MotionLayout getLargeScreenShadeHeaderBarView(
             NotificationShadeWindowView notificationShadeWindowView,
             FeatureFlags featureFlags) {
         ViewStub stub = notificationShadeWindowView.findViewById(R.id.qs_header_stub);
-        int layoutId = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)
-                ? R.layout.combined_qs_header
-                : R.layout.large_screen_shade_header;
+        int layoutId = R.layout.combined_qs_header;
         stub.setLayoutResource(layoutId);
-        View v = stub.inflate();
+        MotionLayout v = (MotionLayout) stub.inflate();
         return v;
     }
 
@@ -196,7 +193,7 @@
     @Provides
     @CentralSurfacesComponent.CentralSurfacesScope
     public static OngoingPrivacyChip getSplitShadeOngoingPrivacyChip(
-            @Named(LARGE_SCREEN_SHADE_HEADER) View header) {
+            @Named(SHADE_HEADER) MotionLayout header) {
         return header.findViewById(R.id.privacy_chip);
     }
 
@@ -204,23 +201,23 @@
     @Provides
     @CentralSurfacesComponent.CentralSurfacesScope
     static StatusIconContainer providesStatusIconContainer(
-            @Named(LARGE_SCREEN_SHADE_HEADER) View header) {
+            @Named(SHADE_HEADER) MotionLayout header) {
         return header.findViewById(R.id.statusIcons);
     }
 
     /** */
     @Provides
     @CentralSurfacesComponent.CentralSurfacesScope
-    @Named(SPLIT_SHADE_BATTERY_VIEW)
-    static BatteryMeterView getBatteryMeterView(@Named(LARGE_SCREEN_SHADE_HEADER) View view) {
+    @Named(SHADE_HEADER)
+    static BatteryMeterView getBatteryMeterView(@Named(SHADE_HEADER) MotionLayout view) {
         return view.findViewById(R.id.batteryRemainingIcon);
     }
 
     @Provides
     @CentralSurfacesComponent.CentralSurfacesScope
-    @Named(LARGE_SCREEN_BATTERY_CONTROLLER)
+    @Named(SHADE_HEADER)
     static BatteryMeterViewController getBatteryMeterViewController(
-            @Named(SPLIT_SHADE_BATTERY_VIEW) BatteryMeterView batteryMeterView,
+            @Named(SHADE_HEADER) BatteryMeterView batteryMeterView,
             UserTracker userTracker,
             ConfigurationController configurationController,
             TunerService tunerService,
@@ -293,7 +290,6 @@
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             KeyguardStateController keyguardStateController,
             NotificationPanelViewController notificationPanelViewController,
-            NetworkController networkController,
             StatusBarStateController statusBarStateController,
             CommandQueue commandQueue,
             CarrierConfigTracker carrierConfigTracker,
@@ -301,7 +297,9 @@
             OperatorNameViewController.Factory operatorNameViewControllerFactory,
             SecureSettings secureSettings,
             @Main Executor mainExecutor,
-            DumpManager dumpManager
+            DumpManager dumpManager,
+            StatusBarWindowStateController statusBarWindowStateController,
+            KeyguardUpdateMonitor keyguardUpdateMonitor
     ) {
         return new CollapsedStatusBarFragment(statusBarFragmentComponentFactory,
                 ongoingCallController,
@@ -315,7 +313,6 @@
                 statusBarHideIconsForBouncerManager,
                 keyguardStateController,
                 notificationPanelViewController,
-                networkController,
                 statusBarStateController,
                 commandQueue,
                 carrierConfigTracker,
@@ -323,7 +320,9 @@
                 operatorNameViewControllerFactory,
                 secureSettings,
                 mainExecutor,
-                dumpManager);
+                dumpManager,
+                statusBarWindowStateController,
+                keyguardUpdateMonitor);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 9f3fd72..2fe7145 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -23,8 +23,6 @@
 import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE;
 import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT;
 
-import android.animation.Animator;
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.Fragment;
@@ -44,7 +42,9 @@
 import android.widget.LinearLayout;
 
 import androidx.annotation.VisibleForTesting;
+import androidx.core.animation.Animator;
 
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -59,9 +59,6 @@
 import com.android.systemui.statusbar.OperatorNameView;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.connectivity.IconState;
-import com.android.systemui.statusbar.connectivity.NetworkController;
-import com.android.systemui.statusbar.connectivity.SignalCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -75,8 +72,9 @@
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
-import com.android.systemui.statusbar.policy.EncryptionHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.CarrierConfigTracker.CarrierConfigChangedListener;
 import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
@@ -110,7 +108,6 @@
     private final StatusBarStateController mStatusBarStateController;
     private final KeyguardStateController mKeyguardStateController;
     private final NotificationPanelViewController mNotificationPanelViewController;
-    private final NetworkController mNetworkController;
     private LinearLayout mEndSideContent;
     private View mClockView;
     private View mOngoingCallChip;
@@ -135,17 +132,12 @@
     private final SecureSettings mSecureSettings;
     private final Executor mMainExecutor;
     private final DumpManager mDumpManager;
+    private final StatusBarWindowStateController mStatusBarWindowStateController;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
     private List<String> mBlockedIcons = new ArrayList<>();
     private Map<Startable, Startable.State> mStartableStates = new ArrayMap<>();
 
-    private SignalCallback mSignalCallback = new SignalCallback() {
-        @Override
-        public void setIsAirplaneMode(@NonNull IconState icon) {
-            mCommandQueue.recomputeDisableFlags(getContext().getDisplayId(), true /* animate */);
-        }
-    };
-
     private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() {
         @Override
         public void onOngoingCallStateChanged(boolean animate) {
@@ -177,6 +169,22 @@
                 }
             };
 
+    /**
+     * Whether we've launched the secure camera over the lockscreen, but haven't yet received a
+     * status bar window state change afterward.
+     *
+     * We wait for this state change (which will tell us whether to show/hide the status bar icons)
+     * so that there is no flickering/jump cutting during the camera launch.
+     */
+    private boolean mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
+    /**
+     * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
+     * a new status bar window state.
+     */
+    private final StatusBarWindowStateListener mStatusBarWindowStateListener = state ->
+            mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
     @SuppressLint("ValidFragment")
     public CollapsedStatusBarFragment(
             StatusBarFragmentComponent.Factory statusBarFragmentComponentFactory,
@@ -191,7 +199,6 @@
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             KeyguardStateController keyguardStateController,
             NotificationPanelViewController notificationPanelViewController,
-            NetworkController networkController,
             StatusBarStateController statusBarStateController,
             CommandQueue commandQueue,
             CarrierConfigTracker carrierConfigTracker,
@@ -199,7 +206,9 @@
             OperatorNameViewController.Factory operatorNameViewControllerFactory,
             SecureSettings secureSettings,
             @Main Executor mainExecutor,
-            DumpManager dumpManager
+            DumpManager dumpManager,
+            StatusBarWindowStateController statusBarWindowStateController,
+            KeyguardUpdateMonitor keyguardUpdateMonitor
     ) {
         mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
         mOngoingCallController = ongoingCallController;
@@ -213,7 +222,6 @@
         mDarkIconManagerFactory = darkIconManagerFactory;
         mKeyguardStateController = keyguardStateController;
         mNotificationPanelViewController = notificationPanelViewController;
-        mNetworkController = networkController;
         mStatusBarStateController = statusBarStateController;
         mCommandQueue = commandQueue;
         mCarrierConfigTracker = carrierConfigTracker;
@@ -222,6 +230,20 @@
         mSecureSettings = secureSettings;
         mMainExecutor = mainExecutor;
         mDumpManager = dumpManager;
+        mStatusBarWindowStateController = statusBarWindowStateController;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
     }
 
     @Override
@@ -261,7 +283,6 @@
         mOngoingCallChip = mStatusBar.findViewById(R.id.ongoing_call_chip);
         showEndSideContent(false);
         showClock(false);
-        initEmergencyCryptkeeperText();
         initOperatorName();
         initNotificationIconArea();
         mSystemEventAnimator =
@@ -270,6 +291,11 @@
         mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
     }
 
+    @Override
+    public void onCameraLaunchGestureDetected(int source) {
+        mWaitingForWindowStateChangeAfterCameraLaunch = true;
+    }
+
     @VisibleForTesting
     void updateBlockedIcons() {
         mBlockedIcons.clear();
@@ -320,7 +346,7 @@
         mAnimationScheduler.addCallback(this);
 
         mSecureSettings.registerContentObserverForUser(
-                Settings.Secure.getUriFor(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON),
+                Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
                 false,
                 mVolumeSettingObserver,
                 UserHandle.USER_ALL);
@@ -340,9 +366,6 @@
     public void onDestroyView() {
         super.onDestroyView();
         mStatusBarIconController.removeIconGroup(mDarkIconManager);
-        if (mNetworkController.hasEmergencyCryptKeeperText()) {
-            mNetworkController.removeCallback(mSignalCallback);
-        }
         mCarrierConfigTracker.removeCallback(mCarrierConfigCallback);
         mCarrierConfigTracker.removeDataSubscriptionChangedListener(mDefaultDataListener);
 
@@ -444,15 +467,6 @@
             state |= DISABLE_CLOCK;
         }
 
-        if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
-            if (mNetworkController.hasEmergencyCryptKeeperText()) {
-                state |= DISABLE_NOTIFICATION_ICONS;
-            }
-            if (!mNetworkController.isRadioOn()) {
-                state |= DISABLE_SYSTEM_INFO;
-            }
-        }
-
         if (mOngoingCallController.hasOngoingCall()) {
             state &= ~DISABLE_ONGOING_CALL_CHIP;
         } else {
@@ -494,6 +508,27 @@
                 && mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
             return true;
         }
+
+        // When launching the camera over the lockscreen, the icons become visible momentarily
+        // before animating out, since we're not yet aware that the launching camera activity is
+        // fullscreen. Even once the activity finishes launching, it takes a short time before WM
+        // decides that the top app wants to hide the icons and tells us to hide them. To ensure
+        // that this high-visibility animation is smooth, keep the icons hidden during a camera
+        // launch until we receive a window state change which indicates that the activity is done
+        // launching and WM has decided to show/hide the icons. For extra safety (to ensure the
+        // icons don't remain hidden somehow) we double check that the camera is still showing, the
+        // status bar window isn't hidden, and we're still occluded as well, though these checks
+        // are typically unnecessary.
+        final boolean hideIconsForSecureCamera =
+                (mWaitingForWindowStateChangeAfterCameraLaunch ||
+                        !mStatusBarWindowStateController.windowIsShowing()) &&
+                        mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard() &&
+                        mKeyguardStateController.isOccluded();
+
+        if (hideIconsForSecureCamera) {
+            return true;
+        }
+
         return mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer();
     }
 
@@ -620,19 +655,6 @@
         }
     }
 
-    private void initEmergencyCryptkeeperText() {
-        View emergencyViewStub = mStatusBar.findViewById(R.id.emergency_cryptkeeper_text);
-        if (mNetworkController.hasEmergencyCryptKeeperText()) {
-            if (emergencyViewStub != null) {
-                ((ViewStub) emergencyViewStub).inflate();
-            }
-            mNetworkController.addCallback(mSignalCallback);
-        } else if (emergencyViewStub != null) {
-            ViewGroup parent = (ViewGroup) emergencyViewStub.getParent();
-            parent.removeView(emergencyViewStub);
-        }
-    }
-
     private void initOperatorName() {
         int subId = SubscriptionManager.getDefaultDataSubscriptionId();
         if (mCarrierConfigTracker.getShowOperatorNameInStatusBarConfig(subId)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
index fe69f75..c04ea36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.phone.fragment
 
-import android.animation.Animator
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorSet
+import androidx.core.animation.ValueAnimator
 import android.content.res.Resources
 import android.view.View
 import com.android.systemui.R
@@ -46,15 +46,19 @@
             R.dimen.ongoing_appops_chip_animation_out_status_bar_translation_x)
 
     override fun onSystemEventAnimationBegin(): Animator {
-        val moveOut = ValueAnimator.ofFloat(0f, 1f).setDuration(23.frames)
-        moveOut.interpolator = STATUS_BAR_X_MOVE_OUT
-        moveOut.addUpdateListener { animation: ValueAnimator ->
-            animatedView.translationX = -(translationXIn * animation.animatedValue as Float)
+        val moveOut = ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = 23.frames
+            interpolator = STATUS_BAR_X_MOVE_OUT
+            addUpdateListener {
+                animatedView.translationX = -(translationXIn * animatedValue as Float)
+            }
         }
-        val alphaOut = ValueAnimator.ofFloat(1f, 0f).setDuration(8.frames)
-        alphaOut.interpolator = null
-        alphaOut.addUpdateListener { animation: ValueAnimator ->
-            animatedView.alpha = animation.animatedValue as Float
+        val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = 8.frames
+            interpolator = null
+            addUpdateListener {
+                animatedView.alpha = animatedValue as Float
+            }
         }
 
         val animSet = AnimatorSet()
@@ -64,17 +68,21 @@
 
     override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
         animatedView.translationX = translationXOut.toFloat()
-        val moveIn = ValueAnimator.ofFloat(1f, 0f).setDuration(28.frames)
-        moveIn.startDelay = 2.frames
-        moveIn.interpolator = STATUS_BAR_X_MOVE_IN
-        moveIn.addUpdateListener { animation: ValueAnimator ->
-            animatedView.translationX = translationXOut * animation.animatedValue as Float
+        val moveIn = ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = 23.frames
+            startDelay = 7.frames
+            interpolator = STATUS_BAR_X_MOVE_IN
+            addUpdateListener {
+                animatedView.translationX = translationXOut * animatedValue as Float
+            }
         }
-        val alphaIn = ValueAnimator.ofFloat(0f, 1f).setDuration(10.frames)
-        alphaIn.startDelay = 4.frames
-        alphaIn.interpolator = null
-        alphaIn.addUpdateListener { animation: ValueAnimator ->
-            animatedView.alpha = animation.animatedValue as Float
+        val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = 5.frames
+            startDelay = 11.frames
+            interpolator = null
+            addUpdateListener {
+                animatedView.alpha = animatedValue as Float
+            }
         }
 
         val animatorSet = AnimatorSet()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
index 2c8677d..270c592 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
@@ -19,14 +19,14 @@
 import android.content.Context
 import android.util.AttributeSet
 import android.widget.ImageView
-import android.widget.LinearLayout
 import android.widget.TextView
 import com.android.systemui.R
+import com.android.systemui.animation.view.LaunchableLinearLayout
 
 class StatusBarUserSwitcherContainer(
     context: Context?,
     attrs: AttributeSet?
-) : LinearLayout(context, attrs) {
+) : LaunchableLinearLayout(context, attrs) {
     lateinit var text: TextView
         private set
     lateinit var avatar: ImageView
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/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
index 4a5342e..5d5d562 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
@@ -18,9 +18,10 @@
 
 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.airplane.domain.interactor.AirplaneModeInteractor
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.dagger.AirplaneTableLog
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -46,7 +47,7 @@
 @Inject
 constructor(
     interactor: AirplaneModeInteractor,
-    logger: ConnectivityPipelineLogger,
+    @AirplaneTableLog logger: TableLogBuffer,
     @Application private val scope: CoroutineScope,
 ) : AirplaneModeViewModel {
     override val isAirplaneModeIconVisible: StateFlow<Boolean> =
@@ -56,6 +57,11 @@
                 isAirplaneMode && !isAirplaneIconForceHidden
             }
             .distinctUntilChanged()
-            .logOutputChange(logger, "isAirplaneModeIconVisible")
+            .logDiffsForTable(
+                logger,
+                columnPrefix = "",
+                columnName = "isAirplaneModeIconVisible",
+                initialValue = false,
+            )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileInputLog.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileInputLog.kt
index 67733e9..d1aa79e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileInputLog.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.statusbar.pipeline.dagger
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+import javax.inject.Qualifier
+
+/** Logs for inputs into the mobile pipeline. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class MobileInputLog
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/SharedConnectivityInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/SharedConnectivityInputLog.kt
new file mode 100644
index 0000000..5face22
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/SharedConnectivityInputLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 connectivity-related inputs that are shared across wifi, mobile, etc. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class SharedConnectivityInputLog
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..4464751 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
@@ -19,12 +19,15 @@
 import android.net.wifi.WifiManager
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 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 +85,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
@@ -101,6 +109,13 @@
 
         @Provides
         @SysUISingleton
+        @WifiInputLog
+        fun provideWifiInputLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("WifiInputLog", 50)
+        }
+
+        @Provides
+        @SysUISingleton
         @WifiTableLog
         fun provideWifiTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
             return factory.create("WifiTableLog", 100)
@@ -112,5 +127,26 @@
         fun provideAirplaneTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
             return factory.create("AirplaneTableLog", 30)
         }
+
+        @Provides
+        @SysUISingleton
+        @SharedConnectivityInputLog
+        fun provideSharedConnectivityTableLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("SharedConnectivityInputLog", 30)
+        }
+
+        @Provides
+        @SysUISingleton
+        @MobileSummaryLog
+        fun provideMobileSummaryLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("MobileSummaryLog", 100)
+        }
+
+        @Provides
+        @SysUISingleton
+        @MobileInputLog
+        fun provideMobileInputLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("MobileInputLog", 100)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiInputLog.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiInputLog.kt
index 67733e9..6db6944 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiInputLog.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.statusbar.pipeline.dagger
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+import javax.inject.Qualifier
+
+/** Wifi logs for inputs into the wifi pipeline. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class WifiInputLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
index 5479b92..85729c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
@@ -20,16 +20,21 @@
 import android.telephony.TelephonyManager.DATA_CONNECTING
 import android.telephony.TelephonyManager.DATA_DISCONNECTED
 import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS
+import android.telephony.TelephonyManager.DATA_SUSPENDED
 import android.telephony.TelephonyManager.DATA_UNKNOWN
 import android.telephony.TelephonyManager.DataState
 
 /** Internal enum representation of the telephony data connection states */
-enum class DataConnectionState(@DataState val dataState: Int) {
-    Connected(DATA_CONNECTED),
-    Connecting(DATA_CONNECTING),
-    Disconnected(DATA_DISCONNECTED),
-    Disconnecting(DATA_DISCONNECTING),
-    Unknown(DATA_UNKNOWN),
+enum class DataConnectionState {
+    Connected,
+    Connecting,
+    Disconnected,
+    Disconnecting,
+    Suspended,
+    HandoverInProgress,
+    Unknown,
+    Invalid,
 }
 
 fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
@@ -38,6 +43,8 @@
         DATA_CONNECTING -> DataConnectionState.Connecting
         DATA_DISCONNECTED -> DataConnectionState.Disconnected
         DATA_DISCONNECTING -> DataConnectionState.Disconnecting
+        DATA_SUSPENDED -> DataConnectionState.Suspended
+        DATA_HANDOVER_IN_PROGRESS -> DataConnectionState.HandoverInProgress
         DATA_UNKNOWN -> DataConnectionState.Unknown
-        else -> throw IllegalArgumentException("unknown data state received $this")
+        else -> DataConnectionState.Invalid
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index 012b9ec..ed7f60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -26,6 +26,7 @@
 import android.telephony.TelephonyCallback.SignalStrengthsListener
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.log.table.Diffable
 import com.android.systemui.log.table.TableRowLogger
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
@@ -94,7 +95,7 @@
 ) : Diffable<MobileConnectionModel> {
     override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) {
         if (prevVal.dataConnectionState != dataConnectionState) {
-            row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+            row.logChange(COL_CONNECTION_STATE, dataConnectionState.name)
         }
 
         if (prevVal.isEmergencyOnly != isEmergencyOnly) {
@@ -125,8 +126,12 @@
             row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
         }
 
-        if (prevVal.dataActivityDirection != dataActivityDirection) {
-            row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+        if (prevVal.dataActivityDirection.hasActivityIn != dataActivityDirection.hasActivityIn) {
+            row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn)
+        }
+
+        if (prevVal.dataActivityDirection.hasActivityOut != dataActivityDirection.hasActivityOut) {
+            row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut)
         }
 
         if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) {
@@ -139,7 +144,7 @@
     }
 
     override fun logFull(row: TableRowLogger) {
-        row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+        row.logChange(COL_CONNECTION_STATE, dataConnectionState.name)
         row.logChange(COL_EMERGENCY, isEmergencyOnly)
         row.logChange(COL_ROAMING, isRoaming)
         row.logChange(COL_OPERATOR, operatorAlphaShort)
@@ -147,11 +152,13 @@
         row.logChange(COL_IS_GSM, isGsm)
         row.logChange(COL_CDMA_LEVEL, cdmaLevel)
         row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
-        row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+        row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn)
+        row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut)
         row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
         row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
     }
 
+    @VisibleForTesting
     companion object {
         const val COL_EMERGENCY = "EmergencyOnly"
         const val COL_ROAMING = "Roaming"
@@ -161,7 +168,8 @@
         const val COL_CDMA_LEVEL = "CdmaLevel"
         const val COL_PRIMARY_LEVEL = "PrimaryLevel"
         const val COL_CONNECTION_STATE = "ConnectionState"
-        const val COL_ACTIVITY_DIRECTION = "DataActivity"
+        const val COL_ACTIVITY_DIRECTION_IN = "DataActivity.In"
+        const val COL_ACTIVITY_DIRECTION_OUT = "DataActivity.Out"
         const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive"
         const val COL_RESOLVED_NETWORK_TYPE = "NetworkType"
     }
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/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
index 5960387..5562e73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.model
 
 import android.telephony.Annotation.NetworkType
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 
 /**
@@ -38,4 +40,12 @@
     data class OverrideNetworkType(
         override val lookupKey: String,
     ) : ResolvedNetworkType
+
+    /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */
+    object CarrierMergedNetworkType : ResolvedNetworkType {
+        // Effectively unused since [iconGroupOverride] is used instead.
+        override val lookupKey: String = "cwf"
+
+        val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
index 2f34516..16c4027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.model
 
+import android.os.ParcelUuid
+
 /**
  * SystemUI representation of [SubscriptionInfo]. Currently we only use two fields on the
  * subscriptions themselves: subscriptionId and isOpportunistic. Any new fields that we need can be
@@ -29,4 +31,7 @@
      * filtering in certain cases. See [MobileIconsInteractor] for the filtering logic
      */
     val isOpportunistic: Boolean = false,
+
+    /** Subscriptions in the same group may be filtered or treated as a single subscription */
+    val groupUuid: ParcelUuid? = 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..bb3b9b2
--- /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.mobile.shared.MobileInputLogger
+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: MobileInputLogger,
+    @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/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index d04996b..6187f64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
 /**
@@ -50,7 +49,7 @@
      * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
      * listener + model.
      */
-    val connectionInfo: Flow<MobileConnectionModel>
+    val connectionInfo: StateFlow<MobileConnectionModel>
 
     /** The total number of levels. Used with [SignalDrawable]. */
     val numberOfLevels: StateFlow<Int>
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 97b4c2c..be30ea4 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,8 +16,8 @@
 
 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
 import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.MobileMappings.Config
@@ -34,8 +34,23 @@
     /** Observable list of current mobile subscriptions */
     val subscriptions: StateFlow<List<SubscriptionModel>>
 
-    /** Observable for the subscriptionId of the current mobile data connection */
-    val activeMobileDataSubscriptionId: StateFlow<Int>
+    /**
+     * Observable for the subscriptionId of the current mobile data connection. Null if we don't
+     * have a valid subscription id
+     */
+    val activeMobileDataSubscriptionId: StateFlow<Int?>
+
+    /** Repo that tracks the current [activeMobileDataSubscriptionId] */
+    val activeMobileDataRepository: StateFlow<MobileConnectionRepository?>
+
+    /**
+     * Observable event for when the active data sim switches but the group stays the same. E.g.,
+     * CBRS switching would trigger this
+     */
+    val activeSubChangedInGroupEvent: Flow<Unit>
+
+    /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
+    val defaultDataSubId: StateFlow<Int>
 
     /** The current connectivity status for the default mobile network connection */
     val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
@@ -43,9 +58,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 0c8593d6..d54531a 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
@@ -47,7 +47,6 @@
  * interface in its own repository, completely separate from the real version, while still using all
  * of the prod implementations for the rest of the pipeline (interactors and onward). Looks
  * something like this:
- *
  * ```
  * RealRepository
  *                 │
@@ -115,7 +114,7 @@
             .flatMapLatest { it.subscriptions }
             .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.subscriptions.value)
 
-    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+    override val activeMobileDataSubscriptionId: StateFlow<Int?> =
         activeRepo
             .flatMapLatest { it.activeMobileDataSubscriptionId }
             .stateIn(
@@ -124,6 +123,18 @@
                 realRepository.activeMobileDataSubscriptionId.value
             )
 
+    override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
+        activeRepo
+            .flatMapLatest { it.activeMobileDataRepository }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realRepository.activeMobileDataRepository.value
+            )
+
+    override val activeSubChangedInGroupEvent: Flow<Unit> =
+        activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent }
+
     override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
         activeRepo
             .flatMapLatest { it.defaultDataSubRatConfig }
@@ -139,6 +150,11 @@
     override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
         activeRepo.flatMapLatest { it.defaultMobileIconGroup }
 
+    override val defaultDataSubId: StateFlow<Int> =
+        activeRepo
+            .flatMapLatest { it.defaultDataSubId }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
+
     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
         activeRepo
             .flatMapLatest { it.defaultMobileNetworkConnectivity }
@@ -148,9 +164,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 0e164e7..e924832 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
@@ -39,17 +39,23 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filterNotNull
 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
@@ -60,15 +66,19 @@
 class DemoMobileConnectionsRepository
 @Inject
 constructor(
-    private val dataSource: DemoModeMobileConnectionDataSource,
+    private val mobileDataSource: DemoModeMobileConnectionDataSource,
+    private val wifiDataSource: DemoModeWifiDataSource,
     @Application private val scope: CoroutineScope,
     context: Context,
     private val logFactory: TableLogBufferFactory,
 ) : MobileConnectionsRepository {
 
-    private var demoCommandJob: Job? = null
+    private var mobileDemoCommandJob: Job? = null
+    private var wifiDemoCommandJob: Job? = null
 
-    private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
+    private var carrierMergedSubId: Int? = null
+
+    private var connectionRepoCache = mutableMapOf<Int, CacheContainer>()
     private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>()
     val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
 
@@ -112,6 +122,18 @@
                 subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
             )
 
+    override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
+        activeMobileDataSubscriptionId
+            .map { getRepoForSubId(it) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                getRepoForSubId(activeMobileDataSubscriptionId.value)
+            )
+
+    // TODO(b/261029387): consider adding a demo command for this
+    override val activeSubChangedInGroupEvent: Flow<Unit> = flowOf()
+
     /** Demo mode doesn't currently support modifications to the mobile mappings */
     override val defaultDataSubRatConfig =
         MutableStateFlow(MobileMappings.Config.readConfig(context))
@@ -140,72 +162,133 @@
 
     private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key }
 
+    // TODO(b/261029387): add a command for this value
+    override val defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+
     // TODO(b/261029387): not yet supported
-    override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
+    override val defaultMobileNetworkConnectivity =
+        MutableStateFlow(MobileConnectivityModel(isConnected = true, isValidated = true))
 
     override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
-        return connectionRepoCache[subId]
-            ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it }
+        val current = connectionRepoCache[subId]?.repo
+        if (current != null) {
+            return current
+        }
+
+        val new = createDemoMobileConnectionRepo(subId)
+        connectionRepoCache[subId] = new
+        return new.repo
     }
 
-    private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository {
-        val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100)
+    private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer {
+        val tableLogBuffer =
+            logFactory.getOrCreate(
+                "DemoMobileConnectionLog [$subId]",
+                MOBILE_CONNECTION_BUFFER_SIZE,
+            )
 
-        return DemoMobileConnectionRepository(
-            subId,
-            tableLogBuffer,
-        )
+        val repo =
+            DemoMobileConnectionRepository(
+                subId,
+                tableLogBuffer,
+            )
+        return CacheContainer(repo, lastMobileState = null)
     }
 
-    override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
-
     fun startProcessingCommands() {
-        demoCommandJob =
+        mobileDemoCommandJob =
             scope.launch {
-                dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
+                mobileDataSource.mobileEvents.filterNotNull().collect { event ->
+                    processMobileEvent(event)
+                }
+            }
+        wifiDemoCommandJob =
+            scope.launch {
+                wifiDataSource.wifiEvents.filterNotNull().collect { event ->
+                    processWifiEvent(event)
+                }
             }
     }
 
     fun stopProcessingCommands() {
-        demoCommandJob?.cancel()
+        mobileDemoCommandJob?.cancel()
+        wifiDemoCommandJob?.cancel()
         _subscriptions.value = listOf()
         connectionRepoCache.clear()
         subscriptionInfoCache.clear()
     }
 
-    private fun processEvent(event: FakeNetworkEventModel) {
+    private fun processMobileEvent(event: FakeNetworkEventModel) {
         when (event) {
             is Mobile -> {
                 processEnabledMobileState(event)
             }
             is MobileDisabled -> {
-                processDisabledMobileState(event)
+                maybeRemoveSubscription(event.subId)
             }
         }
     }
 
+    private fun processWifiEvent(event: FakeWifiEventModel) {
+        when (event) {
+            is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged()
+            is FakeWifiEventModel.Wifi -> disableCarrierMerged()
+            is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
+        }
+    }
+
     private fun processEnabledMobileState(state: Mobile) {
         // get or create the connection repo, and set its values
         val subId = state.subId ?: DEFAULT_SUB_ID
         maybeCreateSubscription(subId)
 
         val connection = getRepoForSubId(subId)
+        connectionRepoCache[subId]?.lastMobileState = state
+
+        // TODO(b/261029387): until we have a command, use the most recent subId
+        defaultDataSubId.value = 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(state.name)
+        connection.networkName.value = NetworkNameModel.IntentDerived(state.name)
 
         connection.cdmaRoaming.value = state.roaming
         connection.connectionInfo.value = state.toMobileConnectionModel()
     }
 
-    private fun processDisabledMobileState(state: MobileDisabled) {
+    private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+        // The new carrier merged connection is for a different sub ID, so disable carrier merged
+        // for the current (now old) sub
+        if (carrierMergedSubId != event.subscriptionId) {
+            disableCarrierMerged()
+        }
+
+        // get or create the connection repo, and set its values
+        val subId = event.subscriptionId
+        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.IntentDerived(CARRIER_MERGED_NAME)
+        connection.numberOfLevels.value = event.numberOfLevels
+        connection.cdmaRoaming.value = false
+        connection.connectionInfo.value = event.toMobileConnectionModel()
+        Log.e("CCS", "output connection info = ${connection.connectionInfo.value}")
+    }
+
+    private fun maybeRemoveSubscription(subId: Int?) {
         if (_subscriptions.value.isEmpty()) {
             // Nothing to do here
             return
         }
 
-        val subId =
-            state.subId
+        val finalSubId =
+            subId
                 ?: run {
                     // For sake of usability, we can allow for no subId arg if there is only one
                     // subscription
@@ -223,7 +306,21 @@
                     _subscriptions.value[0].subscriptionId
                 }
 
-        removeSubscription(subId)
+        removeSubscription(finalSubId)
+    }
+
+    private fun disableCarrierMerged() {
+        val currentCarrierMergedSubId = carrierMergedSubId ?: return
+
+        // If this sub ID was previously not carrier merged, we should reset it to its previous
+        // connection.
+        val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState
+        if (lastMobileState != null) {
+            processEnabledMobileState(lastMobileState)
+        } else {
+            // Otherwise, just remove the subscription entirely
+            removeSubscription(currentCarrierMergedSubId)
+        }
     }
 
     private fun removeSubscription(subId: Int) {
@@ -251,6 +348,13 @@
         )
     }
 
+    private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel {
+        return createCarrierMergedConnectionModel(
+            this.level,
+            activity.toMobileDataActivityModel(),
+        )
+    }
+
     private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
         val key = mobileMappingsReverseLookup.value[this] ?: "dis"
         return DefaultNetworkType(key)
@@ -260,9 +364,17 @@
         private const val TAG = "DemoMobileConnectionsRepo"
 
         private const val DEFAULT_SUB_ID = 1
+
+        private const val CARRIER_MERGED_NAME = "Carrier Merged Network"
     }
 }
 
+class CacheContainer(
+    var repo: DemoMobileConnectionRepository,
+    /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */
+    var lastMobileState: Mobile?,
+)
+
 class DemoMobileConnectionRepository(
     override val subId: Int,
     override val tableLogBuffer: TableLogBuffer,
@@ -275,5 +387,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
new file mode 100644
index 0000000..8f6a87b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.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
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+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
+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.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is
+ * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually
+ * displayed as a mobile network triangle.
+ *
+ * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+ *
+ * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile
+ * connection.
+ */
+class CarrierMergedConnectionRepository(
+    override val subId: Int,
+    override val tableLogBuffer: TableLogBuffer,
+    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
+     * network.
+     */
+    private val network: Flow<WifiNetworkModel.CarrierMerged?> =
+        combine(
+            wifiRepository.isWifiEnabled,
+            wifiRepository.isWifiDefault,
+            wifiRepository.wifiNetwork,
+        ) { isEnabled, isDefault, network ->
+            when {
+                !isEnabled -> null
+                !isDefault -> null
+                network !is WifiNetworkModel.CarrierMerged -> null
+                network.subscriptionId != subId -> {
+                    Log.w(
+                        TAG,
+                        "Connection repo subId=$subId " +
+                            "does not equal wifi repo subId=${network.subscriptionId}; " +
+                            "not showing carrier merged"
+                    )
+                    null
+                }
+                else -> network
+            }
+        }
+
+    override val connectionInfo: StateFlow<MobileConnectionModel> =
+        combine(network, wifiRepository.wifiActivity) { network, activity ->
+                if (network == null) {
+                    MobileConnectionModel()
+                } else {
+                    createCarrierMergedConnectionModel(network.level, activity)
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel())
+
+    override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(ROAMING).asStateFlow()
+
+    override val networkName: StateFlow<NetworkNameModel> =
+        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
+            .map {
+                if (it is WifiNetworkModel.CarrierMerged) {
+                    it.numberOfLevels
+                } else {
+                    DEFAULT_NUM_LEVELS
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+
+    override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled
+
+    companion object {
+        /**
+         * Creates an instance of [MobileConnectionModel] that represents a carrier merged network
+         * with the given [level] and [activity].
+         */
+        fun createCarrierMergedConnectionModel(
+            level: Int,
+            activity: DataActivityModel,
+        ): MobileConnectionModel {
+            return MobileConnectionModel(
+                primaryLevel = level,
+                cdmaLevel = level,
+                dataActivityDirection = activity,
+                // Here and below: These values are always the same for every carrier-merged
+                // connection.
+                resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+                dataConnectionState = DataConnectionState.Connected,
+                isRoaming = ROAMING,
+                isEmergencyOnly = false,
+                operatorAlphaShort = null,
+                isInService = true,
+                isGsm = false,
+                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,
+        ): MobileConnectionRepository {
+            return CarrierMergedConnectionRepository(
+                subId,
+                mobileLogger,
+                telephonyManager.createForSubscriptionId(subId),
+                scope,
+                wifiRepository,
+            )
+        }
+    }
+}
+
+private const val TAG = "CarrierMergedConnectionRepository"
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
new file mode 100644
index 0000000..a39ea0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository that fully implements a mobile connection.
+ *
+ * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl]
+ * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository
+ * switches between the two types of connections based on whether the connection is currently
+ * carrier merged (see [setIsCarrierMerged]).
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class FullMobileConnectionRepository(
+    override val subId: Int,
+    startingIsCarrierMerged: Boolean,
+    override val tableLogBuffer: TableLogBuffer,
+    private val defaultNetworkName: NetworkNameModel,
+    private val networkNameSeparator: String,
+    @Application scope: CoroutineScope,
+    private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+    private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+) : MobileConnectionRepository {
+    /**
+     * Sets whether this connection is a typical mobile connection or a carrier merged connection.
+     */
+    fun setIsCarrierMerged(isCarrierMerged: Boolean) {
+        _isCarrierMerged.value = isCarrierMerged
+    }
+
+    /**
+     * Returns true if this repo is currently for a carrier merged connection and false otherwise.
+     */
+    @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value
+
+    private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged)
+    private val isCarrierMerged: StateFlow<Boolean> =
+        _isCarrierMerged
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = "isCarrierMerged",
+                initialValue = startingIsCarrierMerged,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged)
+
+    private val mobileRepo: MobileConnectionRepository by lazy {
+        mobileRepoFactory.build(
+            subId,
+            tableLogBuffer,
+            defaultNetworkName,
+            networkNameSeparator,
+        )
+    }
+
+    private val carrierMergedRepo: MobileConnectionRepository by lazy {
+        carrierMergedRepoFactory.build(subId, tableLogBuffer)
+    }
+
+    @VisibleForTesting
+    internal val activeRepo: StateFlow<MobileConnectionRepository> = run {
+        val initial =
+            if (startingIsCarrierMerged) {
+                carrierMergedRepo
+            } else {
+                mobileRepo
+            }
+
+        this.isCarrierMerged
+            .mapLatest { isCarrierMerged ->
+                if (isCarrierMerged) {
+                    carrierMergedRepo
+                } else {
+                    mobileRepo
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+    }
+
+    override val cdmaRoaming =
+        activeRepo
+            .flatMapLatest { it.cdmaRoaming }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value)
+
+    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 =
+        activeRepo
+            .flatMapLatest { it.numberOfLevels }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value)
+
+    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
+    @Inject
+    constructor(
+        @Application private val scope: CoroutineScope,
+        private val logFactory: TableLogBufferFactory,
+        private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+        private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+    ) {
+        fun build(
+            subId: Int,
+            startingIsCarrierMerged: Boolean,
+            defaultNetworkName: NetworkNameModel,
+            networkNameSeparator: String,
+        ): FullMobileConnectionRepository {
+            val mobileLogger =
+                logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
+
+            return FullMobileConnectionRepository(
+                subId,
+                startingIsCarrierMerged,
+                mobileLogger,
+                defaultNetworkName,
+                networkNameSeparator,
+                scope,
+                mobileRepoFactory,
+                carrierMergedRepoFactory,
+            )
+        }
+
+        companion object {
+            /** The buffer size to use for logging. */
+            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]"
+        }
+    }
+}
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 0fa0fea..96b96f1 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,21 +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.TableLogBufferFactory
-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.shared.MobileInputLogger
 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
@@ -61,15 +58,21 @@
 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
 
+/**
+ * A repository implementation for a typical mobile connection (as opposed to a carrier merged
+ * connection -- see [CarrierMergedConnectionRepository]).
+ */
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 class MobileConnectionRepositoryImpl(
@@ -78,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,
+    logger: MobileInputLogger,
+    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."
             )
         }
@@ -98,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 :
@@ -111,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(
@@ -153,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
@@ -256,69 +268,49 @@
 
     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(
         private val broadcastDispatcher: BroadcastDispatcher,
         private val context: Context,
         private val telephonyManager: TelephonyManager,
-        private val logger: ConnectivityPipelineLogger,
-        private val globalSettings: GlobalSettings,
+        private val logger: MobileInputLogger,
+        private val carrierConfigRepository: CarrierConfigRepository,
         private val mobileMappingsProxy: MobileMappingsProxy,
-        private val logFactory: TableLogBufferFactory,
         @Background private val bgDispatcher: CoroutineDispatcher,
         @Application private val scope: CoroutineScope,
     ) {
         fun build(
             subId: Int,
+            mobileLogger: TableLogBuffer,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
-            globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): MobileConnectionRepository {
-            val mobileLogger = logFactory.create(tableBufferLogName(subId), 100)
-
             return MobileConnectionRepositoryImpl(
                 context,
                 subId,
                 defaultNetworkName,
                 networkNameSeparator,
                 telephonyManager.createForSubscriptionId(subId),
-                globalSettings,
+                carrierConfigRepository.getOrCreateConfigForSubId(subId),
                 broadcastDispatcher,
-                globalMobileDataSettingChangedEvent,
                 mobileMappingsProxy,
                 bgDispatcher,
                 logger,
@@ -327,8 +319,18 @@
             )
         }
     }
+}
 
-    companion object {
-        fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
-    }
+/**
+ * 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 c88c700..b3d5b1e 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
@@ -35,6 +33,7 @@
 import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
 import android.telephony.TelephonyManager
 import androidx.annotation.VisibleForTesting
+import com.android.internal.telephony.PhoneConstants
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings.Config
 import com.android.systemui.R
@@ -43,15 +42,18 @@
 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
-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.shared.MobileInputLogger
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
-import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.util.kotlin.pairwise
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -59,11 +61,14 @@
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
@@ -78,16 +83,21 @@
     private val connectivityManager: ConnectivityManager,
     private val subscriptionManager: SubscriptionManager,
     private val telephonyManager: TelephonyManager,
-    private val logger: ConnectivityPipelineLogger,
+    private val logger: MobileInputLogger,
+    @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,
-    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+    // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi
+    // repository is an input to the mobile repository.
+    // See [CarrierMergedConnectionRepository] for details.
+    wifiRepository: WifiRepository,
+    private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory,
 ) : MobileConnectionsRepository {
-    private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+    private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> =
+        mutableMapOf()
 
     private val defaultNetworkName =
         NetworkNameModel.Default(
@@ -97,39 +107,67 @@
     private val networkNameSeparator: String =
         context.getString(R.string.status_bar_network_name_separator)
 
+    private val carrierMergedSubId: StateFlow<Int?> =
+        wifiRepository.wifiNetwork
+            .mapLatest {
+                if (it is WifiNetworkModel.CarrierMerged) {
+                    it.subscriptionId
+                } else {
+                    null
+                }
+            }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "carrierMergedSubId",
+                initialValue = null,
+            )
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
+
+    private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
+        val callback =
+            object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                override fun onSubscriptionsChanged() {
+                    trySend(Unit)
+                }
+            }
+
+        subscriptionManager.addOnSubscriptionsChangedListener(
+            bgDispatcher.asExecutor(),
+            callback,
+        )
+
+        awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+    }
+
     /**
      * State flow that emits the set of mobile data subscriptions, each represented by its own
-     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
-     * info object, but for now we keep track of the infos themselves.
+     * [SubscriptionModel].
      */
     override val subscriptions: StateFlow<List<SubscriptionModel>> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                        override fun onSubscriptionsChanged() {
-                            trySend(Unit)
-                        }
-                    }
-
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                    bgDispatcher.asExecutor(),
-                    callback,
-                )
-
-                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-            }
+        merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
             .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
-            .logInputChange(logger, "onSubscriptionsChanged")
-            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .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 */
-    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+    override val activeMobileDataSubscriptionId: StateFlow<Int?> =
         conflatedCallbackFlow {
                 val callback =
                     object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
                         override fun onActiveDataSubscriptionIdChanged(subId: Int) {
-                            trySend(subId)
+                            if (subId != INVALID_SUBSCRIPTION_ID) {
+                                trySend(subId)
+                            } else {
+                                trySend(null)
+                            }
                         }
                     }
 
@@ -137,24 +175,59 @@
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
             }
             .distinctUntilChanged()
-            .logInputChange(logger, "onActiveDataSubscriptionIdChanged")
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "activeSubId",
+                initialValue = INVALID_SUBSCRIPTION_ID,
+            )
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
 
-    private val defaultDataSubIdChangedEvent =
+    override val activeMobileDataRepository =
+        activeMobileDataSubscriptionId
+            .map { activeSubId ->
+                if (activeSubId == null) {
+                    null
+                } else {
+                    getOrCreateRepoForSubId(activeSubId)
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
+        MutableSharedFlow(extraBufferCapacity = 1)
+
+    override val defaultDataSubId: StateFlow<Int> =
         broadcastDispatcher
-            .broadcastFlow(IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED))
-            .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
+            .broadcastFlow(
+                IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+            ) { intent, _ ->
+                intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+            }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "defaultSubId",
+                initialValue = SubscriptionManager.getDefaultDataSubscriptionId(),
+            )
+            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                SubscriptionManager.getDefaultDataSubscriptionId()
+            )
 
     private val carrierConfigChangedEvent =
         broadcastDispatcher
             .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED))
-            .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED")
+            .onEach { logger.logActionCarrierConfigChanged() }
 
     override val defaultDataSubRatConfig: StateFlow<Config> =
-        merge(defaultDataSubIdChangedEvent, carrierConfigChangedEvent)
+        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
             .mapLatest { Config.readConfig(context) }
             .distinctUntilChanged()
-            .logInputChange(logger, "defaultDataSubRatConfig")
+            .onEach { logger.logDefaultDataSubRatConfig(it) }
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
@@ -165,47 +238,27 @@
         defaultDataSubRatConfig
             .map { mobileMappingsProxy.mapIconSets(it) }
             .distinctUntilChanged()
-            .logInputChange(logger, "defaultMobileIconMapping")
+            .onEach { logger.logDefaultMobileIconMapping(it) }
 
     override val defaultMobileIconGroup: Flow<MobileIconGroup> =
         defaultDataSubRatConfig
             .map { mobileMappingsProxy.getDefaultIcons(it) }
             .distinctUntilChanged()
-            .logInputChange(logger, "defaultMobileIconGroup")
+            .onEach { logger.logDefaultMobileIconGroup(it) }
 
-    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+    override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository {
         if (!isValidSubId(subId)) {
             throw IllegalArgumentException(
                 "subscriptionId $subId is not in the list of valid subscriptions"
             )
         }
 
-        return subIdRepositoryCache[subId]
-            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+        return getOrCreateRepoForSubId(subId)
     }
 
-    /**
-     * 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")
+    private fun getOrCreateRepoForSubId(subId: Int) =
+        subIdRepositoryCache[subId]
+            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
 
     @SuppressLint("MissingPermission")
     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
@@ -236,11 +289,77 @@
                 awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
             }
             .distinctUntilChanged()
-            .logInputChange(logger, "defaultMobileNetworkConnectivity")
+            .logDiffsForTable(
+                tableLogger,
+                columnPrefix = "$LOGGING_PREFIX.defaultConnection",
+                initialValue = MobileConnectivityModel(),
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
 
-    private fun isValidSubId(subId: Int): Boolean {
-        subscriptions.value.forEach {
+    /**
+     * Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data
+     * subscription Id changes but the subscription group remains the same. In these cases, we want
+     * to retain the previous subscription's validation status for up to 2s to avoid flickering the
+     * icon.
+     *
+     * TODO(b/265164432): we should probably expose all change events, not just same group
+     */
+    @SuppressLint("MissingPermission")
+    override val activeSubChangedInGroupEvent =
+        activeMobileDataSubscriptionId
+            .pairwise()
+            .mapNotNull { (prevVal: Int?, newVal: Int?) ->
+                if (prevVal == null || newVal == null) return@mapNotNull null
+
+                val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal)?.groupUuid
+                val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal)?.groupUuid
+
+                if (prevSub != null && prevSub == nextSub) Unit else null
+            }
+            .flowOn(bgDispatcher)
+
+    private fun isValidSubId(subId: Int): Boolean = checkSub(subId, subscriptions.value)
+
+    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+    private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
+        return fullMobileRepoFactory.build(
+            subId,
+            isCarrierMerged(subId),
+            defaultNetworkName,
+            networkNameSeparator,
+        )
+    }
+
+    private fun updateRepos(newInfos: List<SubscriptionModel>) {
+        dropUnusedReposFromCache(newInfos)
+        subIdRepositoryCache.forEach { (subId, repo) ->
+            repo.setIsCarrierMerged(isCarrierMerged(subId))
+        }
+    }
+
+    private fun isCarrierMerged(subId: Int): Boolean {
+        return subId == carrierMergedSubId.value
+    }
+
+    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
+        // Remove any connection repository from the cache that isn't in the new set of IDs. They
+        // will get garbage collected once their subscribers go away
+        subIdRepositoryCache =
+            subIdRepositoryCache.filter { checkSub(it.key, newInfos) }.toMutableMap()
+    }
+
+    /**
+     * True if the checked subId is in the list of current subs or the active mobile data subId
+     *
+     * @param checkedSubs the list to validate [subId] against. To invalidate the cache, pass in the
+     * new subscription list. Otherwise use [subscriptions.value] to validate a subId against the
+     * current known subscriptions
+     */
+    private fun checkSub(subId: Int, checkedSubs: List<SubscriptionModel>): Boolean {
+        if (activeMobileDataSubscriptionId.value == subId) return true
+
+        checkedSubs.forEach {
             if (it.subscriptionId == subId) {
                 return true
             }
@@ -249,28 +368,6 @@
         return false
     }
 
-    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
-
-    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
-        return mobileConnectionRepositoryFactory.build(
-            subId,
-            defaultNetworkName,
-            networkNameSeparator,
-            globalMobileDataSettingChangedEvent,
-        )
-    }
-
-    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
-        // Remove any connection repository from the cache that isn't in the new set of IDs. They
-        // will get garbage collected once their subscribers go away
-        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
-
-        subIdRepositoryCache =
-            subIdRepositoryCache
-                .filter { currentValidSubscriptionIds.contains(it.key) }
-                .toMutableMap()
-    }
-
     private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
         withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
 
@@ -278,5 +375,10 @@
         SubscriptionModel(
             subscriptionId = subscriptionId,
             isOpportunistic = isOpportunistic,
+            groupUuid = groupUuid,
         )
+
+    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 9427c6b..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
@@ -18,12 +18,14 @@
 
 import android.telephony.CarrierConfigManager
 import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons.NOT_DEFAULT_DATA
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+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.ResolvedNetworkType
 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.shared.data.model.DataActivityModel
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -31,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 {
@@ -41,12 +45,29 @@
     /** The current mobile data activity */
     val activity: Flow<DataActivityModel>
 
+    /**
+     * This bit is meant to be `true` if and only if the default network capabilities (see
+     * [android.net.ConnectivityManager.registerDefaultNetworkCallback]) result in a network that
+     * has the [android.net.NetworkCapabilities.TRANSPORT_CELLULAR] represented.
+     *
+     * Note that this differs from [isDataConnected], which is tracked by telephony and has to do
+     * with the state of using this mobile connection for data as opposed to just voice. It is
+     * possible for a mobile subscription to be connected but not be in a connected data state, and
+     * thus we wouldn't want to show the network type icon.
+     */
+    val isConnected: Flow<Boolean>
+
+    /**
+     * True when telephony tells us that the data state is CONNECTED. See
+     * [android.telephony.TelephonyCallback.DataConnectionStateListener] for more details. We
+     * consider this connection to be serving data, and thus want to show a network type icon, when
+     * data is connected. Other data connection states would typically cause us not to show the icon
+     */
+    val isDataConnected: StateFlow<Boolean>
+
     /** Only true if mobile is the default transport but is not validated, otherwise false */
     val isDefaultConnectionFailed: StateFlow<Boolean>
 
-    /** True when telephony tells us that the data state is CONNECTED */
-    val isDataConnected: StateFlow<Boolean>
-
     /** True if we consider this connection to be in service, i.e. can make calls */
     val isInService: StateFlow<Boolean>
 
@@ -90,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 */
@@ -100,9 +124,12 @@
     defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
     override val alwaysShowDataRatIcon: StateFlow<Boolean>,
     override val alwaysUseCdmaLevel: StateFlow<Boolean>,
+    defaultMobileConnectivity: StateFlow<MobileConnectivityModel>,
     defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
     defaultMobileIconGroup: StateFlow<MobileIconGroup>,
+    defaultDataSubId: StateFlow<Int>,
     override val isDefaultConnectionFailed: StateFlow<Boolean>,
+    override val isForceHidden: Flow<Boolean>,
     connectionRepository: MobileConnectionRepository,
 ) : MobileIconInteractor {
     private val connectionInfo = connectionRepository.connectionInfo
@@ -111,8 +138,19 @@
 
     override val activity = connectionInfo.mapLatest { it.dataActivityDirection }
 
+    override val isConnected: Flow<Boolean> = defaultMobileConnectivity.mapLatest { it.isConnected }
+
     override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
 
+    private val isDefault =
+        defaultDataSubId
+            .mapLatest { connectionRepository.subId == it }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                connectionRepository.subId == defaultDataSubId.value
+            )
+
     override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
 
     override val networkName =
@@ -120,7 +158,7 @@
                 if (
                     networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null
                 ) {
-                    NetworkNameModel.Derived(connection.operatorAlphaShort)
+                    NetworkNameModel.IntentDerived(connection.operatorAlphaShort)
                 } else {
                     networkName
                 }
@@ -137,8 +175,27 @@
                 connectionInfo,
                 defaultMobileIconMapping,
                 defaultMobileIconGroup,
-            ) { info, mapping, defaultGroup ->
-                mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+                isDefault,
+            ) { info, mapping, defaultGroup, isDefault ->
+                if (!isDefault) {
+                    return@combine NOT_DEFAULT_DATA
+                }
+
+                when (info.resolvedNetworkType) {
+                    is ResolvedNetworkType.CarrierMergedNetworkType ->
+                        info.resolvedNetworkType.iconGroupOverride
+                    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)
 
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 83da1dd..142c372 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
@@ -18,27 +18,37 @@
 
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionManager
-import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import com.android.settingslib.SignalIcon.MobileIconGroup
 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.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
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+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.stateIn
+import kotlinx.coroutines.flow.transformLatest
 
 /**
  * Business layer logic for the set of mobile subscription icons.
@@ -62,6 +72,17 @@
     /** True if the CDMA level should be preferred over the primary level. */
     val alwaysUseCdmaLevel: StateFlow<Boolean>
 
+    /** Tracks the subscriptionId set as the default for data connections */
+    val defaultDataSubId: StateFlow<Int>
+
+    /**
+     * The connectivity of the default mobile network. Note that this can differ from what is
+     * reported from [MobileConnectionsRepository] in some cases. E.g., when the active subscription
+     * changes but the groupUuid remains the same, we keep the old validation information for 2
+     * seconds to avoid icon flickering.
+     */
+    val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
+
     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
     val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
     /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
@@ -70,6 +91,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
@@ -85,25 +110,13 @@
 constructor(
     private val mobileConnectionsRepo: MobileConnectionsRepository,
     private val carrierConfigTracker: CarrierConfigTracker,
+    @MobileSummaryLog private val tableLogger: TableLogBuffer,
+    connectivityRepository: ConnectivityRepository,
     userSetupRepo: UserSetupRepository,
     @Application private val scope: CoroutineScope,
 ) : MobileIconsInteractor {
-    private val activeMobileDataSubscriptionId =
-        mobileConnectionsRepo.activeMobileDataSubscriptionId
-
-    private val activeMobileDataConnectionRepo: StateFlow<MobileConnectionRepository?> =
-        activeMobileDataSubscriptionId
-            .mapLatest { activeId ->
-                if (activeId == INVALID_SUBSCRIPTION_ID) {
-                    null
-                } else {
-                    mobileConnectionsRepo.getRepoForSubId(activeId)
-                }
-            }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
-
     override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
-        activeMobileDataConnectionRepo
+        mobileConnectionsRepo.activeMobileDataRepository
             .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
@@ -124,35 +137,105 @@
      * and by checking which subscription is opportunistic, or which one is active.
      */
     override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
-        combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
-            ->
-            // Based on the old logic,
-            if (unfilteredSubs.size != 2) {
-                return@combine unfilteredSubs
-            }
+        combine(
+                unfilteredSubscriptions,
+                mobileConnectionsRepo.activeMobileDataSubscriptionId,
+            ) { unfilteredSubs, activeId ->
+                // Based on the old logic,
+                if (unfilteredSubs.size != 2) {
+                    return@combine unfilteredSubs
+                }
 
-            val info1 = unfilteredSubs[0]
-            val info2 = unfilteredSubs[1]
-            // If both subscriptions are primary, show both
-            if (!info1.isOpportunistic && !info2.isOpportunistic) {
-                return@combine unfilteredSubs
-            }
+                val info1 = unfilteredSubs[0]
+                val info2 = unfilteredSubs[1]
 
-            // NOTE: at this point, we are now returning a single SubscriptionInfo
+                // Filtering only applies to subscriptions in the same group
+                if (info1.groupUuid == null || info1.groupUuid != info2.groupUuid) {
+                    return@combine unfilteredSubs
+                }
 
-            // If carrier required, always show the icon of the primary subscription.
-            // Otherwise, show whichever subscription is currently active for internet.
-            if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
-                // return the non-opportunistic info
-                return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1)
-            } else {
-                return@combine if (info1.subscriptionId == activeId) {
-                    listOf(info1)
+                // If both subscriptions are primary, show both
+                if (!info1.isOpportunistic && !info2.isOpportunistic) {
+                    return@combine unfilteredSubs
+                }
+
+                // NOTE: at this point, we are now returning a single SubscriptionInfo
+
+                // If carrier required, always show the icon of the primary subscription.
+                // Otherwise, show whichever subscription is currently active for internet.
+                if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
+                    // return the non-opportunistic info
+                    return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1)
                 } else {
-                    listOf(info2)
+                    return@combine if (info1.subscriptionId == activeId) {
+                        listOf(info1)
+                    } else {
+                        listOf(info2)
+                    }
                 }
             }
-        }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "filteredSubscriptions",
+                initialValue = listOf(),
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+    override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
+
+    /**
+     * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
+     * validated bit from the old active network (A) while data is changing to the new one (B).
+     *
+     * This condition only applies if
+     * 1. A and B are in the same subscription group (e.g. for CBRS data switching) and
+     * 2. A was validated before the switch
+     *
+     * The goal of this is to minimize the flickering in the UI of the cellular indicator
+     */
+    private val forcingCellularValidation =
+        mobileConnectionsRepo.activeSubChangedInGroupEvent
+            .filter { mobileConnectionsRepo.defaultMobileNetworkConnectivity.value.isValidated }
+            .transformLatest {
+                emit(true)
+                delay(2000)
+                emit(false)
+            }
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "forcingValidation",
+                initialValue = false,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+        combine(
+                mobileConnectionsRepo.defaultMobileNetworkConnectivity,
+                forcingCellularValidation,
+            ) { networkConnectivity, forceValidation ->
+                return@combine if (forceValidation) {
+                    MobileConnectivityModel(
+                        isValidated = true,
+                        isConnected = networkConnectivity.isConnected
+                    )
+                } else {
+                    networkConnectivity
+                }
+            }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                columnPrefix = "$LOGGING_PREFIX.defaultConnection",
+                initialValue = mobileConnectionsRepo.defaultMobileNetworkConnectivity.value,
+            )
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                mobileConnectionsRepo.defaultMobileNetworkConnectivity.value
+            )
 
     /**
      * Mapping from network type to [MobileIconGroup] using the config generated for the default
@@ -196,10 +279,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(
@@ -207,9 +301,16 @@
             activeDataConnectionHasDataEnabled,
             alwaysShowDataRatIcon,
             alwaysUseCdmaLevel,
+            defaultMobileNetworkConnectivity,
             defaultMobileIconMapping,
             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/shared/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLogger.kt
new file mode 100644
index 0000000..3cbd2b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLogger.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.shared
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.TelephonyDisplayInfo
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.pipeline.dagger.MobileInputLog
+import com.android.systemui.statusbar.pipeline.shared.LoggerHelper
+import javax.inject.Inject
+
+/** Logs for inputs into the mobile pipeline. */
+@SysUISingleton
+class MobileInputLogger
+@Inject
+constructor(
+    @MobileInputLog private val buffer: LogBuffer,
+) {
+    fun logOnCapabilitiesChanged(
+        network: Network,
+        networkCapabilities: NetworkCapabilities,
+        isDefaultNetworkCallback: Boolean,
+    ) {
+        LoggerHelper.logOnCapabilitiesChanged(
+            buffer,
+            TAG,
+            network,
+            networkCapabilities,
+            isDefaultNetworkCallback,
+        )
+    }
+
+    fun logOnLost(network: Network) {
+        LoggerHelper.logOnLost(buffer, TAG, network)
+    }
+
+    fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                bool1 = serviceState.isEmergencyOnly
+                bool2 = serviceState.roaming
+                str1 = serviceState.operatorAlphaShort
+            },
+            {
+                "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" +
+                    " operator=$str1"
+            }
+        )
+    }
+
+    fun logOnSignalStrengthsChanged(signalStrength: SignalStrength, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                str1 = signalStrength.toString()
+            },
+            { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }
+        )
+    }
+
+    fun logOnDataConnectionStateChanged(dataState: Int, networkType: Int, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                int2 = dataState
+                str1 = networkType.toString()
+            },
+            { "onDataConnectionStateChanged: subId=$int1 dataState=$int2 networkType=$str1" },
+        )
+    }
+
+    fun logOnDataActivity(direction: Int, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                int2 = direction
+            },
+            { "onDataActivity: subId=$int1 direction=$int2" },
+        )
+    }
+
+    fun logOnCarrierNetworkChange(active: Boolean, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                bool1 = active
+            },
+            { "onCarrierNetworkChange: subId=$int1 active=$bool1" },
+        )
+    }
+
+    fun logOnDisplayInfoChanged(displayInfo: TelephonyDisplayInfo, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                str1 = displayInfo.toString()
+            },
+            { "onDisplayInfoChanged: subId=$int1 displayInfo=$str1" },
+        )
+    }
+
+    fun logUiAdapterSubIdsUpdated(subs: List<Int>) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { str1 = subs.toString() },
+            { "Sub IDs in MobileUiAdapter updated internally: $str1" },
+        )
+    }
+
+    fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { str1 = subs.toString() },
+            { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" },
+        )
+    }
+
+    fun logCarrierConfigChanged(subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { int1 = subId },
+            { "onCarrierConfigChanged: subId=$int1" },
+        )
+    }
+
+    fun logOnDataEnabledChanged(enabled: Boolean, subId: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                bool1 = enabled
+            },
+            { "onDataEnabledChanged: subId=$int1 enabled=$bool1" },
+        )
+    }
+
+    fun logActionCarrierConfigChanged() {
+        buffer.log(TAG, LogLevel.INFO, {}, { "Intent received: ACTION_CARRIER_CONFIG_CHANGED" })
+    }
+
+    fun logDefaultDataSubRatConfig(config: MobileMappings.Config) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { str1 = config.toString() },
+            { "defaultDataSubRatConfig: $str1" }
+        )
+    }
+
+    fun logDefaultMobileIconMapping(mapping: Map<String, SignalIcon.MobileIconGroup>) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { str1 = mapping.toString() },
+            { "defaultMobileIconMapping: $str1" }
+        )
+    }
+
+    fun logDefaultMobileIconGroup(group: SignalIcon.MobileIconGroup) {
+        buffer.log(TAG, LogLevel.INFO, { str1 = group.name }, { "defaultMobileIconGroup: $str1" })
+    }
+}
+
+private const val TAG = "MobileInputLog"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 829a5ca..da63ab1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -22,7 +22,9 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,7 +32,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
@@ -51,13 +55,17 @@
     interactor: MobileIconsInteractor,
     private val iconController: StatusBarIconController,
     private val iconsViewModelFactory: MobileIconsViewModel.Factory,
+    private val logger: MobileInputLogger,
     @Application private val scope: CoroutineScope,
     private val statusBarPipelineFlags: StatusBarPipelineFlags,
 ) : CoreStartable {
     private val mobileSubIds: Flow<List<Int>> =
-        interactor.filteredSubscriptions.mapLatest { subscriptions ->
-            subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
-        }
+        interactor.filteredSubscriptions
+            .mapLatest { subscriptions ->
+                subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
+            }
+            .distinctUntilChanged()
+            .onEach { logger.logUiAdapterSubIdsUpdated(it) }
 
     /**
      * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is
@@ -72,6 +80,9 @@
     /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */
     val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState)
 
+    private var isCollecting: Boolean = false
+    private var lastValue: List<Int>? = null
+
     override fun start() {
         // Only notify the icon controller if we want to *render* the new icons.
         // Note that this flow may still run if
@@ -79,8 +90,18 @@
         // get the logging data without rendering.
         if (statusBarPipelineFlags.useNewMobileIcons()) {
             scope.launch {
-                mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) }
+                isCollecting = true
+                mobileSubIds.collectLatest {
+                    logger.logUiAdapterSubIdsSentToIconController(it)
+                    lastValue = it
+                    iconController.setNewMobileIconSubIds(it)
+                }
             }
         }
     }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("isCollecting=$isCollecting")
+        pw.println("Last values sent to icon controller: $lastValue")
+    }
 }
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 3e81c7c..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
@@ -29,6 +29,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.R
+import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.StatusBarIconView
@@ -90,10 +91,23 @@
                     }
                 }
 
+                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,
+                            )
+                    }
+                }
+
+                launch {
+                    viewModel.contentDescription.distinctUntilChanged().collect {
+                        ContentDescriptionViewBinder.bind(it, view)
                     }
                 }
 
@@ -141,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 a2117c7..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
@@ -16,14 +16,17 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
+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.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
@@ -35,14 +38,15 @@
 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 */
     val networkTypeIcon: Flow<Icon?>
@@ -70,7 +74,7 @@
 constructor(
     override val subscriptionId: Int,
     iconInteractor: MobileIconInteractor,
-    logger: ConnectivityPipelineLogger,
+    airplaneModeInteractor: AirplaneModeInteractor,
     constants: ConnectivityConstants,
     scope: CoroutineScope,
 ) : MobileIconViewModelCommon {
@@ -78,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,
@@ -87,51 +111,72 @@
                 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)
     }
 
-    override val networkTypeIcon: Flow<Icon?> =
+    override val contentDescription: Flow<ContentDescription> = run {
+        val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH_NONE)
         combine(
-                iconInteractor.networkTypeIconGroup,
+                iconInteractor.level,
+                iconInteractor.isInService,
+            ) { level, isInService ->
+                val resId =
+                    when {
+                        isInService -> PHONE_SIGNAL_STRENGTH[level]
+                        else -> PHONE_SIGNAL_STRENGTH_NONE
+                    }
+                ContentDescription.Resource(resId)
+            }
+            .distinctUntilChanged()
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+    }
+
+    private val showNetworkTypeIcon: Flow<Boolean> =
+        combine(
                 iconInteractor.isDataConnected,
                 iconInteractor.isDataEnabled,
                 iconInteractor.isDefaultConnectionFailed,
                 iconInteractor.alwaysShowDataRatIcon,
-            ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
+                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(
+                iconInteractor.networkTypeIconGroup,
+                showNetworkTypeIcon,
+            ) { networkTypeIconGroup, shouldShow ->
                 val desc =
                     if (networkTypeIconGroup.dataContentDescription != 0)
                         ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
                     else null
                 val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
                 return@combine when {
-                    alwaysShow -> icon
-                    !dataConnected -> null
-                    !dataEnabled -> null
-                    failedConnection -> null
+                    !shouldShow -> null
                     else -> icon
                 }
             }
             .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..8cb52af 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,13 +17,14 @@
 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
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.StateFlow
@@ -39,7 +40,7 @@
 constructor(
     val subscriptionIdsFlow: StateFlow<List<Int>>,
     private val interactor: MobileIconsInteractor,
-    private val logger: ConnectivityPipelineLogger,
+    private val airplaneModeInteractor: AirplaneModeInteractor,
     private val constants: ConnectivityConstants,
     @Application private val scope: CoroutineScope,
     private val statusBarPipelineFlags: StatusBarPipelineFlags,
@@ -56,7 +57,7 @@
                 ?: MobileIconViewModel(
                         subId,
                         interactor.createMobileConnectionInteractorForSubId(subId),
-                        logger,
+                        airplaneModeInteractor,
                         constants,
                         scope,
                     )
@@ -74,11 +75,12 @@
         subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }
     }
 
+    @SysUISingleton
     class Factory
     @Inject
     constructor(
         private val interactor: MobileIconsInteractor,
-        private val logger: ConnectivityPipelineLogger,
+        private val airplaneModeInteractor: AirplaneModeInteractor,
         private val constants: ConnectivityConstants,
         @Application private val scope: CoroutineScope,
         private val statusBarPipelineFlags: StatusBarPipelineFlags,
@@ -87,7 +89,7 @@
             return MobileIconsViewModel(
                 subscriptionIdsFlow,
                 interactor,
-                logger,
+                airplaneModeInteractor,
                 constants,
                 scope,
                 statusBarPipelineFlags,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
index 0c9b86c..0fe5329 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -40,7 +39,7 @@
     telephonyManager: TelephonyManager,
 ) : Dumpable {
     init {
-        dumpManager.registerNormalDumpable("${SB_LOGGING_TAG}Constants", this)
+        dumpManager.registerNormalDumpable("ConnectivityConstants", this)
     }
 
     /** True if this device has the capability for data connections and false otherwise. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt
new file mode 100644
index 0000000..95548b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.shared
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.pipeline.dagger.SharedConnectivityInputLog
+import javax.inject.Inject
+
+/** Logs for connectivity-related inputs that are shared across wifi, mobile, etc. */
+@SysUISingleton
+class ConnectivityInputLogger
+@Inject
+constructor(
+    @SharedConnectivityInputLog private val buffer: LogBuffer,
+) {
+    fun logTuningChanged(tuningList: String?) {
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = tuningList }, { "onTuningChanged: $str1" })
+    }
+}
+
+private const val TAG = "ConnectivityInputLogger"
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
deleted file mode 100644
index d3ff357..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.shared
-
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.TelephonyDisplayInfo
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.dagger.StatusBarConnectivityLog
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.onEach
-
-@SysUISingleton
-class ConnectivityPipelineLogger
-@Inject
-constructor(
-    @StatusBarConnectivityLog private val buffer: LogBuffer,
-) {
-    /**
-     * Logs a change in one of the **raw inputs** to the connectivity pipeline.
-     *
-     * Use this method for inputs that don't have any extra information besides their callback name.
-     */
-    fun logInputChange(callbackName: String) {
-        buffer.log(SB_LOGGING_TAG, LogLevel.INFO, { str1 = callbackName }, { "Input: $str1" })
-    }
-
-    /** Logs a change in one of the **raw inputs** to the connectivity pipeline. */
-    fun logInputChange(callbackName: String, changeInfo: String?) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                str1 = callbackName
-                str2 = changeInfo
-            },
-            { "Input: $str1: $str2" }
-        )
-    }
-
-    /** Logs a **data transformation** that we performed within the connectivity pipeline. */
-    fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) {
-        if (oldValue == newValue) {
-            buffer.log(
-                SB_LOGGING_TAG,
-                LogLevel.INFO,
-                {
-                    str1 = transformationName
-                    str2 = oldValue.toString()
-                },
-                { "Transform: $str1: $str2 (transformation didn't change it)" }
-            )
-        } else {
-            buffer.log(
-                SB_LOGGING_TAG,
-                LogLevel.INFO,
-                {
-                    str1 = transformationName
-                    str2 = oldValue.toString()
-                    str3 = newValue.toString()
-                },
-                { "Transform: $str1: $str2 -> $str3" }
-            )
-        }
-    }
-
-    /** Logs a change in one of the **outputs** to the connectivity pipeline. */
-    fun logOutputChange(outputParamName: String, changeInfo: String) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                str1 = outputParamName
-                str2 = changeInfo
-            },
-            { "Output: $str1: $str2" }
-        )
-    }
-
-    fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = network.getNetId()
-                str1 = networkCapabilities.toString()
-            },
-            { "onCapabilitiesChanged: net=$int1 capabilities=$str1" }
-        )
-    }
-
-    fun logOnLost(network: Network) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            { int1 = network.getNetId() },
-            { "onLost: net=$int1" }
-        )
-    }
-
-    fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = subId
-                bool1 = serviceState.isEmergencyOnly
-                bool2 = serviceState.roaming
-                str1 = serviceState.operatorAlphaShort
-            },
-            {
-                "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" +
-                    " operator=$str1"
-            }
-        )
-    }
-
-    fun logOnSignalStrengthsChanged(signalStrength: SignalStrength, subId: Int) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = subId
-                str1 = signalStrength.toString()
-            },
-            { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }
-        )
-    }
-
-    fun logOnDataConnectionStateChanged(dataState: Int, networkType: Int, subId: Int) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = subId
-                int2 = dataState
-                str1 = networkType.toString()
-            },
-            { "onDataConnectionStateChanged: subId=$int1 dataState=$int2 networkType=$str1" },
-        )
-    }
-
-    fun logOnDataActivity(direction: Int, subId: Int) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = subId
-                int2 = direction
-            },
-            { "onDataActivity: subId=$int1 direction=$int2" },
-        )
-    }
-
-    fun logOnCarrierNetworkChange(active: Boolean, subId: Int) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = subId
-                bool1 = active
-            },
-            { "onCarrierNetworkChange: subId=$int1 active=$bool1" },
-        )
-    }
-
-    fun logOnDisplayInfoChanged(displayInfo: TelephonyDisplayInfo, subId: Int) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            {
-                int1 = subId
-                str1 = displayInfo.toString()
-            },
-            { "onDisplayInfoChanged: subId=$int1 displayInfo=$str1" },
-        )
-    }
-
-    companion object {
-        const val SB_LOGGING_TAG = "SbConnectivity"
-
-        /** Log a change in one of the **inputs** to the connectivity pipeline. */
-        fun Flow<Unit>.logInputChange(
-            logger: ConnectivityPipelineLogger,
-            inputParamName: String,
-        ): Flow<Unit> {
-            return this.onEach { logger.logInputChange(inputParamName) }
-        }
-
-        /**
-         * Log a change in one of the **inputs** to the connectivity pipeline.
-         *
-         * @param prettyPrint an optional function to transform the value into a readable string.
-         * [toString] is used if no custom function is provided.
-         */
-        fun <T> Flow<T>.logInputChange(
-            logger: ConnectivityPipelineLogger,
-            inputParamName: String,
-            prettyPrint: (T) -> String = { it.toString() }
-        ): Flow<T> {
-            return this.onEach { logger.logInputChange(inputParamName, prettyPrint(it)) }
-        }
-
-        /**
-         * Log a change in one of the **outputs** to the connectivity pipeline.
-         *
-         * @param prettyPrint an optional function to transform the value into a readable string.
-         * [toString] is used if no custom function is provided.
-         */
-        fun <T> Flow<T>.logOutputChange(
-            logger: ConnectivityPipelineLogger,
-            outputParamName: String,
-            prettyPrint: (T) -> String = { it.toString() }
-        ): Flow<T> {
-            return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt
new file mode 100644
index 0000000..6f29e33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt
@@ -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.statusbar.pipeline.shared
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+
+/** Helper object for logs that are shared between wifi and mobile. */
+object LoggerHelper {
+    fun logOnCapabilitiesChanged(
+        buffer: LogBuffer,
+        tag: String,
+        network: Network,
+        networkCapabilities: NetworkCapabilities,
+        isDefaultNetworkCallback: Boolean,
+    ) {
+        buffer.log(
+            tag,
+            LogLevel.INFO,
+            {
+                bool1 = isDefaultNetworkCallback
+                int1 = network.getNetId()
+                str1 = networkCapabilities.toString()
+            },
+            { "onCapabilitiesChanged[default=$bool1]: net=$int1 capabilities=$str1" }
+        )
+    }
+
+    fun logOnLost(buffer: LogBuffer, tag: String, network: Network) {
+        buffer.log(tag, LogLevel.INFO, { int1 = network.getNetId() }, { "onLost: net=$int1" })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
index 45c6d46..5d9ba018 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
@@ -26,8 +26,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityInputLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
 import com.android.systemui.tuner.TunerService
@@ -45,59 +44,61 @@
  * types of connectivity (wifi, mobile, ethernet, etc.)
  */
 interface ConnectivityRepository {
-    /**
-     * Observable for the current set of connectivity icons that should be force-hidden.
-     */
+    /** Observable for the current set of connectivity icons that should be force-hidden. */
     val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>>
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
-class ConnectivityRepositoryImpl @Inject constructor(
+class ConnectivityRepositoryImpl
+@Inject
+constructor(
     private val connectivitySlots: ConnectivitySlots,
     context: Context,
     dumpManager: DumpManager,
-    logger: ConnectivityPipelineLogger,
+    logger: ConnectivityInputLogger,
     @Application scope: CoroutineScope,
     tunerService: TunerService,
 ) : ConnectivityRepository, Dumpable {
     init {
-        dumpManager.registerDumpable("${SB_LOGGING_TAG}Repository", this)
+        dumpManager.registerDumpable("ConnectivityRepository", this)
     }
 
     // The default set of hidden icons to use if we don't get any from [TunerService].
     private val defaultHiddenIcons: Set<ConnectivitySlot> =
-            context.resources.getStringArray(DEFAULT_HIDDEN_ICONS_RESOURCE)
-                .asList()
-                .toSlotSet(connectivitySlots)
+        context.resources
+            .getStringArray(DEFAULT_HIDDEN_ICONS_RESOURCE)
+            .asList()
+            .toSlotSet(connectivitySlots)
 
-    override val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>> = conflatedCallbackFlow {
-        val callback = object : TunerService.Tunable {
-            override fun onTuningChanged(key: String, newHideList: String?) {
-                if (key != HIDDEN_ICONS_TUNABLE_KEY) {
-                    return
-                }
-                logger.logInputChange("onTuningChanged", newHideList)
+    override val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TunerService.Tunable {
+                        override fun onTuningChanged(key: String, newHideList: String?) {
+                            if (key != HIDDEN_ICONS_TUNABLE_KEY) {
+                                return
+                            }
+                            logger.logTuningChanged(newHideList)
 
-                val outputList = newHideList?.split(",")?.toSlotSet(connectivitySlots)
-                    ?: defaultHiddenIcons
-                trySend(outputList)
+                            val outputList =
+                                newHideList?.split(",")?.toSlotSet(connectivitySlots)
+                                    ?: defaultHiddenIcons
+                            trySend(outputList)
+                        }
+                    }
+                tunerService.addTunable(callback, HIDDEN_ICONS_TUNABLE_KEY)
+
+                awaitClose { tunerService.removeTunable(callback) }
             }
-        }
-        tunerService.addTunable(callback, HIDDEN_ICONS_TUNABLE_KEY)
-
-        awaitClose { tunerService.removeTunable(callback) }
-    }
-        .stateIn(
-            scope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = defaultHiddenIcons
-        )
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = defaultHiddenIcons
+            )
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("defaultHiddenIcons=$defaultHiddenIcons")
-        }
+        pw.apply { println("defaultHiddenIcons=$defaultHiddenIcons") }
     }
 
     companion object {
@@ -111,8 +112,7 @@
         private fun List<String>.toSlotSet(
             connectivitySlots: ConnectivitySlots
         ): Set<ConnectivitySlot> {
-            return this
-                .filter { it.isNotBlank() }
+            return this.filter { it.isNotBlank() }
                 .mapNotNull { connectivitySlots.getSlotFromName(it) }
                 .toSet()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
index cc0ec54..b1e2812 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -77,6 +77,17 @@
         return binding.getShouldIconBeVisible()
     }
 
+    /** See [StatusBarIconView.getDrawingRect]. */
+    override fun getDrawingRect(outRect: Rect) {
+        super.getDrawingRect(outRect)
+        val translationX = translationX.toInt()
+        val translationY = translationY.toInt()
+        outRect.left += translationX
+        outRect.right += translationX
+        outRect.top += translationY
+        outRect.bottom += translationY
+    }
+
     /**
      * Initializes this view.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
deleted file mode 100644
index 4251d18..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.wifi.data.model
-
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.log.table.TableRowLogger
-import com.android.systemui.log.table.Diffable
-
-/** Provides information about the current wifi network. */
-sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
-
-    /**
-     * A model representing that we couldn't fetch any wifi information.
-     *
-     * This is only used with [DisabledWifiRepository], where [WifiManager] is null.
-     */
-    object Unavailable : WifiNetworkModel() {
-        override fun toString() = "WifiNetwork.Unavailable"
-        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
-            if (prevVal is Unavailable) {
-                return
-            }
-
-            logFull(row)
-        }
-
-        override fun logFull(row: TableRowLogger) {
-            row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
-            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
-            row.logChange(COL_VALIDATED, false)
-            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
-            row.logChange(COL_SSID, null)
-            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
-            row.logChange(COL_ONLINE_SIGN_UP, false)
-            row.logChange(COL_PASSPOINT_NAME, null)
-        }
-    }
-
-    /** A model representing that we have no active wifi network. */
-    object Inactive : WifiNetworkModel() {
-        override fun toString() = "WifiNetwork.Inactive"
-
-        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
-            if (prevVal is Inactive) {
-                return
-            }
-
-            if (prevVal is CarrierMerged) {
-                // The only difference between CarrierMerged and Inactive is the type
-                row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
-                return
-            }
-
-            // When changing from Active to Inactive, we need to log diffs to all the fields.
-            logFullNonActiveNetwork(TYPE_INACTIVE, row)
-        }
-
-        override fun logFull(row: TableRowLogger) {
-            logFullNonActiveNetwork(TYPE_INACTIVE, row)
-        }
-    }
-
-    /**
-     * A model representing that our wifi network is actually a carrier merged network, meaning it's
-     * treated as more of a mobile network.
-     *
-     * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
-     */
-    object CarrierMerged : WifiNetworkModel() {
-        override fun toString() = "WifiNetwork.CarrierMerged"
-
-        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
-            if (prevVal is CarrierMerged) {
-                return
-            }
-
-            if (prevVal is Inactive) {
-                // The only difference between CarrierMerged and Inactive is the type.
-                row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
-                return
-            }
-
-            // When changing from Active to CarrierMerged, we need to log diffs to all the fields.
-            logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row)
-        }
-    }
-
-    /** Provides information about an active wifi network. */
-    data class Active(
-        /**
-         * The [android.net.Network.netId] we received from
-         * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
-         *
-         * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
-         */
-        val networkId: Int,
-
-        /** See [android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED]. */
-        val isValidated: Boolean = false,
-
-        /**
-         * The wifi signal level, guaranteed to be 0 <= level <= 4.
-         */
-        val level: Int,
-
-        /** See [android.net.wifi.WifiInfo.ssid]. */
-        val ssid: String? = null,
-
-        /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
-        val isPasspointAccessPoint: Boolean = false,
-
-        /** See [android.net.wifi.WifiInfo.isOsuAp]. */
-        val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
-
-        /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
-        val passpointProviderFriendlyName: String? = null,
-    ) : WifiNetworkModel() {
-        init {
-            require(level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
-                "0 <= wifi level <= 4 required; level was $level"
-            }
-        }
-
-        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
-            if (prevVal !is Active) {
-                row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
-            }
-
-            if (prevVal !is Active || prevVal.networkId != networkId) {
-                row.logChange(COL_NETWORK_ID, networkId)
-            }
-            if (prevVal !is Active || prevVal.isValidated != isValidated) {
-                row.logChange(COL_VALIDATED, isValidated)
-            }
-            if (prevVal !is Active || prevVal.level != level) {
-                row.logChange(COL_LEVEL, level)
-            }
-            if (prevVal !is Active || prevVal.ssid != ssid) {
-                row.logChange(COL_SSID, ssid)
-            }
-
-            // TODO(b/238425913): The passpoint-related values are frequently never used, so it
-            //   would be great to not log them when they're not used.
-            if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
-                row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
-            }
-            if (prevVal !is Active ||
-                prevVal.isOnlineSignUpForPasspointAccessPoint !=
-                isOnlineSignUpForPasspointAccessPoint) {
-                row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
-            }
-            if (prevVal !is Active ||
-                prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
-                row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
-            }
-        }
-
-        override fun toString(): String {
-            // Only include the passpoint-related values in the string if we have them. (Most
-            // networks won't have them so they'll be mostly clutter.)
-            val passpointString =
-                if (isPasspointAccessPoint ||
-                    isOnlineSignUpForPasspointAccessPoint ||
-                    passpointProviderFriendlyName != null) {
-                    ", isPasspointAp=$isPasspointAccessPoint, " +
-                        "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " +
-                        "passpointName=$passpointProviderFriendlyName"
-            } else {
-                ""
-            }
-
-            return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " +
-                "level=$level, ssid=$ssid$passpointString)"
-        }
-
-        companion object {
-            @VisibleForTesting
-            internal const val MIN_VALID_LEVEL = 0
-            @VisibleForTesting
-            internal const val MAX_VALID_LEVEL = 4
-        }
-    }
-
-    internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) {
-        row.logChange(COL_NETWORK_TYPE, type)
-        row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
-        row.logChange(COL_VALIDATED, false)
-        row.logChange(COL_LEVEL, LEVEL_DEFAULT)
-        row.logChange(COL_SSID, null)
-        row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
-        row.logChange(COL_ONLINE_SIGN_UP, false)
-        row.logChange(COL_PASSPOINT_NAME, null)
-    }
-}
-
-const val TYPE_CARRIER_MERGED = "CarrierMerged"
-const val TYPE_UNAVAILABLE = "Unavailable"
-const val TYPE_INACTIVE = "Inactive"
-const val TYPE_ACTIVE = "Active"
-
-const val COL_NETWORK_TYPE = "type"
-const val COL_NETWORK_ID = "networkId"
-const val COL_VALIDATED = "isValidated"
-const val COL_LEVEL = "level"
-const val COL_SSID = "ssid"
-const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
-const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
-const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
-
-val LEVEL_DEFAULT: String? = null
-val NETWORK_ID_DEFAULT: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index ac4d55c..08c14e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 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.shared.model.WifiNetworkModel
 import kotlinx.coroutines.flow.StateFlow
 
 /** Provides data related to the wifi state. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
index 2cb81c8..e0e0ed7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
@@ -23,9 +23,9 @@
 import com.android.systemui.demomode.DemoMode
 import com.android.systemui.demomode.DemoModeController
 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.demo.DemoWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
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 c588945..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
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
 import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -43,16 +44,16 @@
 
     private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
         val wifi = getString("wifi") ?: return null
-        return if (wifi == "show") {
-            activeWifiEvent()
-        } else {
-            FakeWifiEventModel.WifiDisabled
+        return when (wifi) {
+            "show" -> activeWifiEvent()
+            "carriermerged" -> carrierMergedWifiEvent()
+            else -> FakeWifiEventModel.WifiDisabled
         }
     }
 
     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()
 
@@ -64,11 +65,24 @@
         )
     }
 
-    private fun String.toActivity(): Int =
+    private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged {
+        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, activity)
+    }
+
+    private fun String?.toActivity(): Int =
         when (this) {
             "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
             "in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN
             "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
             else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
         }
+
+    companion object {
+        const val DEFAULT_CARRIER_MERGED_SUB_ID = 10
+    }
 }
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 be3d7d4..a4fbc2c 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
@@ -19,9 +19,9 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
@@ -66,6 +66,7 @@
     private fun processEvent(event: FakeWifiEventModel) =
         when (event) {
             is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
+            is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
             is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
         }
 
@@ -79,12 +80,17 @@
     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
+        _wifiActivity.value = event.activity.toWifiDataActivityModel()
+        _wifiNetwork.value = event.toCarrierMergedModel()
+    }
+
     private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
         WifiNetworkModel.Active(
             networkId = DEMO_NET_ID,
@@ -99,6 +105,14 @@
             passpointProviderFriendlyName = null,
         )
 
+    private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel =
+        WifiNetworkModel.CarrierMerged(
+            networkId = DEMO_NET_ID,
+            subscriptionId = subscriptionId,
+            level = level,
+            numberOfLevels = numberOfLevels,
+        )
+
     companion object {
         private const val DEMO_NET_ID = 1234
     }
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 2353fb8..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,10 +26,17 @@
 sealed interface FakeWifiEventModel {
     data class Wifi(
         val level: Int?,
-        val activity: Int?,
+        @Annotation.DataActivityType val activity: Int,
         val ssid: String?,
         val validated: Boolean?,
     ) : FakeWifiEventModel
 
+    data class CarrierMerged(
+        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/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
index 5d4a666..86a668a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
@@ -18,8 +18,8 @@
 
 import com.android.systemui.dagger.SysUISingleton
 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.RealWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index c47c20d..ee58160 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -29,6 +29,7 @@
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import com.android.settingslib.Utils
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -38,13 +39,12 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -57,6 +57,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
 /** Real implementation of [WifiRepository]. */
@@ -69,7 +70,7 @@
 constructor(
     broadcastDispatcher: BroadcastDispatcher,
     connectivityManager: ConnectivityManager,
-    logger: ConnectivityPipelineLogger,
+    logger: WifiInputLogger,
     @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
     @Main mainExecutor: Executor,
     @Application scope: CoroutineScope,
@@ -79,7 +80,7 @@
     private val wifiStateChangeEvents: Flow<Unit> =
         broadcastDispatcher
             .broadcastFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION))
-            .logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent")
+            .onEach { logger.logIntent("WIFI_STATE_CHANGED_ACTION") }
 
     private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> =
         MutableSharedFlow(extraBufferCapacity = 1)
@@ -94,7 +95,7 @@
             .logDiffsForTable(
                 wifiTableLogBuffer,
                 columnPrefix = "",
-                columnName = "isWifiEnabled",
+                columnName = "isEnabled",
                 initialValue = wifiManager.isWifiEnabled,
             )
             .stateIn(
@@ -113,13 +114,17 @@
                             network: Network,
                             networkCapabilities: NetworkCapabilities
                         ) {
+                            logger.logOnCapabilitiesChanged(
+                                network,
+                                networkCapabilities,
+                                isDefaultNetworkCallback = true,
+                            )
+
                             // This method will always be called immediately after the network
                             // becomes the default, in addition to any time the capabilities change
                             // while the network is the default.
-                            // If this network contains valid wifi info, then wifi is the default
-                            // network.
-                            val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
-                            trySend(wifiInfo != null)
+                            // If this network is a wifi network, then wifi is the default network.
+                            trySend(isWifiNetwork(networkCapabilities))
                         }
 
                         override fun onLost(network: Network) {
@@ -136,7 +141,7 @@
             .logDiffsForTable(
                 wifiTableLogBuffer,
                 columnPrefix = "",
-                columnName = "isWifiDefault",
+                columnName = "isDefault",
                 initialValue = false,
             )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
@@ -151,7 +156,11 @@
                             network: Network,
                             networkCapabilities: NetworkCapabilities
                         ) {
-                            logger.logOnCapabilitiesChanged(network, networkCapabilities)
+                            logger.logOnCapabilitiesChanged(
+                                network,
+                                networkCapabilities,
+                                isDefaultNetworkCallback = false,
+                            )
 
                             wifiNetworkChangeEvents.tryEmit(Unit)
 
@@ -164,11 +173,6 @@
                                         networkCapabilities,
                                         wifiManager,
                                     )
-                                logger.logTransformation(
-                                    WIFI_NETWORK_CALLBACK_NAME,
-                                    oldValue = currentWifi,
-                                    newValue = wifiNetworkModel,
-                                )
                                 currentWifi = wifiNetworkModel
                                 trySend(wifiNetworkModel)
                             }
@@ -185,11 +189,6 @@
                                     wifi.networkId == network.getNetId()
                             ) {
                                 val newNetworkModel = WifiNetworkModel.Inactive
-                                logger.logTransformation(
-                                    WIFI_NETWORK_CALLBACK_NAME,
-                                    oldValue = wifi,
-                                    newValue = newNetworkModel,
-                                )
                                 currentWifi = newNetworkModel
                                 trySend(newNetworkModel)
                             }
@@ -203,7 +202,7 @@
             .distinctUntilChanged()
             .logDiffsForTable(
                 wifiTableLogBuffer,
-                columnPrefix = "wifiNetwork",
+                columnPrefix = "",
                 initialValue = WIFI_NETWORK_DEFAULT,
             )
             // There will be multiple wifi icons in different places that will frequently
@@ -219,7 +218,7 @@
     override val wifiActivity: StateFlow<DataActivityModel> =
         conflatedCallbackFlow {
                 val callback = TrafficStateCallback { state ->
-                    logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
+                    logger.logActivity(prettyPrintActivity(state))
                     trySend(state.toWifiDataActivityModel())
                 }
                 wifiManager.registerTrafficStateCallback(mainExecutor, callback)
@@ -252,16 +251,30 @@
             networkCapabilities: NetworkCapabilities
         ): WifiInfo? {
             return when {
-                networkCapabilities.hasTransport(TRANSPORT_WIFI) ->
-                    networkCapabilities.transportInfo as WifiInfo?
                 networkCapabilities.hasTransport(TRANSPORT_CELLULAR) ->
                     // Sometimes, cellular networks can act as wifi networks (known as VCN --
                     // virtual carrier network). So, see if this cellular network has wifi info.
                     Utils.tryGetWifiInfoForVcn(networkCapabilities)
+                networkCapabilities.hasTransport(TRANSPORT_WIFI) ->
+                    if (networkCapabilities.transportInfo is WifiInfo) {
+                        networkCapabilities.transportInfo as WifiInfo
+                    } else {
+                        null
+                    }
                 else -> null
             }
         }
 
+        /** True if these capabilities represent a wifi network. */
+        private fun isWifiNetwork(networkCapabilities: NetworkCapabilities): Boolean {
+            return when {
+                networkCapabilities.hasTransport(TRANSPORT_WIFI) -> true
+                networkCapabilities.hasTransport(TRANSPORT_CELLULAR) ->
+                    Utils.tryGetWifiInfoForVcn(networkCapabilities) != null
+                else -> false
+            }
+        }
+
         private fun createWifiNetworkModel(
             wifiInfo: WifiInfo,
             network: Network,
@@ -269,7 +282,19 @@
             wifiManager: WifiManager,
         ): WifiNetworkModel {
             return if (wifiInfo.isCarrierMerged) {
-                WifiNetworkModel.CarrierMerged
+                if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) {
+                    WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON)
+                } else {
+                    WifiNetworkModel.CarrierMerged(
+                        networkId = network.getNetId(),
+                        subscriptionId = wifiInfo.subscriptionId,
+                        level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
+                        // The WiFi signal level returned by WifiManager#calculateSignalLevel start
+                        // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level
+                        // buckets count.
+                        numberOfLevels = wifiManager.maxSignalLevel + 1,
+                    )
+                }
             } else {
                 WifiNetworkModel.Active(
                     network.getNetId(),
@@ -301,7 +326,8 @@
                 .addTransportType(TRANSPORT_CELLULAR)
                 .build()
 
-        private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
+        private const val CARRIER_MERGED_INVALID_SUB_ID_REASON =
+            "Wifi network was carrier merged but had invalid sub ID"
     }
 
     @SysUISingleton
@@ -310,7 +336,7 @@
     constructor(
         private val broadcastDispatcher: BroadcastDispatcher,
         private val connectivityManager: ConnectivityManager,
-        private val logger: ConnectivityPipelineLogger,
+        private val logger: WifiInputLogger,
         @WifiTableLog private val wifiTableLogBuffer: TableLogBuffer,
         @Main private val mainExecutor: Executor,
         @Application private val scope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 980560a..96ab074 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -21,8 +21,8 @@
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
@@ -58,24 +58,29 @@
 }
 
 @SysUISingleton
-class WifiInteractorImpl @Inject constructor(
+class WifiInteractorImpl
+@Inject
+constructor(
     connectivityRepository: ConnectivityRepository,
     wifiRepository: WifiRepository,
 ) : WifiInteractor {
 
-    override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
-        when (info) {
-            is WifiNetworkModel.Unavailable -> null
-            is WifiNetworkModel.Inactive -> null
-            is WifiNetworkModel.CarrierMerged -> null
-            is WifiNetworkModel.Active -> when {
-                info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
-                    info.passpointProviderFriendlyName
-                info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
-                else -> null
+    override val ssid: Flow<String?> =
+        wifiRepository.wifiNetwork.map { info ->
+            when (info) {
+                is WifiNetworkModel.Unavailable -> null
+                is WifiNetworkModel.Invalid -> null
+                is WifiNetworkModel.Inactive -> null
+                is WifiNetworkModel.CarrierMerged -> null
+                is WifiNetworkModel.Active ->
+                    when {
+                        info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
+                            info.passpointProviderFriendlyName
+                        info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
+                        else -> null
+                    }
             }
         }
-    }
 
     override val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled
 
@@ -85,7 +90,6 @@
 
     override val activity: StateFlow<DataActivityModel> = wifiRepository.wifiActivity
 
-    override val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map {
-        it.contains(ConnectivitySlot.WIFI)
-    }
+    override val isForceHidden: Flow<Boolean> =
+        connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.WIFI) }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
index 4f7fe28..8bea772 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -30,12 +29,14 @@
  * logging purposes.
  */
 @SysUISingleton
-class WifiConstants @Inject constructor(
-        context: Context,
-        dumpManager: DumpManager,
+class WifiConstants
+@Inject
+constructor(
+    context: Context,
+    dumpManager: DumpManager,
 ) : Dumpable {
     init {
-        dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this)
+        dumpManager.registerNormalDumpable("WifiConstants", this)
     }
 
     /** True if we should always show the wifi icon when wifi is enabled and false otherwise. */
@@ -43,8 +44,6 @@
         context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled)
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled")
-        }
+        pw.apply { println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled") }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt
new file mode 100644
index 0000000..a32e475
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.wifi.shared
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.pipeline.dagger.WifiInputLog
+import com.android.systemui.statusbar.pipeline.shared.LoggerHelper
+import javax.inject.Inject
+
+/**
+ * Logger for all the wifi-related inputs (intents, callbacks, etc.) that the wifi repo receives.
+ */
+@SysUISingleton
+class WifiInputLogger
+@Inject
+constructor(
+    @WifiInputLog val buffer: LogBuffer,
+) {
+    fun logOnCapabilitiesChanged(
+        network: Network,
+        networkCapabilities: NetworkCapabilities,
+        isDefaultNetworkCallback: Boolean,
+    ) {
+        LoggerHelper.logOnCapabilitiesChanged(
+            buffer,
+            TAG,
+            network,
+            networkCapabilities,
+            isDefaultNetworkCallback,
+        )
+    }
+
+    fun logOnLost(network: Network) {
+        LoggerHelper.logOnLost(buffer, TAG, network)
+    }
+
+    fun logIntent(intentName: String) {
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = intentName }, { "Intent received: $str1" })
+    }
+
+    fun logActivity(activity: String) {
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = activity }, { "Activity: $str1" })
+    }
+}
+
+private const val TAG = "WifiInputLog"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt
new file mode 100644
index 0000000..0923d78
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.shared.model
+
+import android.telephony.SubscriptionManager
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+
+/** Provides information about the current wifi network. */
+sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
+
+    // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of
+    //   copy-pasting the column names for each sub-object.
+
+    /**
+     * A model representing that we couldn't fetch any wifi information.
+     *
+     * This is only used with [DisabledWifiRepository], where [WifiManager] is null.
+     */
+    object Unavailable : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Unavailable"
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal is Unavailable) {
+                return
+            }
+
+            logFull(row)
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
+            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+            row.logChange(COL_VALIDATED, false)
+            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
+
+    /** A model representing that the wifi information we received was invalid in some way. */
+    data class Invalid(
+        /** A description of why the wifi information was invalid. */
+        val invalidReason: String,
+    ) : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Invalid[$invalidReason]"
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal !is Invalid) {
+                logFull(row)
+                return
+            }
+
+            if (invalidReason != prevVal.invalidReason) {
+                row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+            }
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+            row.logChange(COL_VALIDATED, false)
+            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
+
+    /** A model representing that we have no active wifi network. */
+    object Inactive : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Inactive"
+
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal is Inactive) {
+                return
+            }
+
+            // When changing to Inactive, we need to log diffs to all the fields.
+            logFull(row)
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+            row.logChange(COL_VALIDATED, false)
+            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
+
+    /**
+     * A model representing that our wifi network is actually a carrier merged network, meaning it's
+     * treated as more of a mobile network.
+     *
+     * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+     */
+    data class CarrierMerged(
+        /**
+         * The [android.net.Network.netId] we received from
+         * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+         *
+         * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+         */
+        val networkId: Int,
+
+        /**
+         * The subscription ID that this connection represents.
+         *
+         * Comes from [android.net.wifi.WifiInfo.getSubscriptionId].
+         *
+         * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid,
+         * then this is *not* a carrier merged network).
+         */
+        val subscriptionId: Int,
+
+        /** The signal level, guaranteed to be 0 <= level <= numberOfLevels. */
+        val level: Int,
+
+        /** The maximum possible level. */
+        val numberOfLevels: Int = MobileConnectionRepository.DEFAULT_NUM_LEVELS,
+    ) : WifiNetworkModel() {
+        init {
+            require(level in MIN_VALID_LEVEL..numberOfLevels) {
+                "0 <= wifi level <= $numberOfLevels required; level was $level"
+            }
+            require(subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                "subscription ID cannot be invalid"
+            }
+        }
+
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal !is CarrierMerged) {
+                logFull(row)
+                return
+            }
+
+            if (prevVal.networkId != networkId) {
+                row.logChange(COL_NETWORK_ID, networkId)
+            }
+            if (prevVal.subscriptionId != subscriptionId) {
+                row.logChange(COL_SUB_ID, subscriptionId)
+            }
+            if (prevVal.level != level) {
+                row.logChange(COL_LEVEL, level)
+            }
+            if (prevVal.numberOfLevels != numberOfLevels) {
+                row.logChange(COL_NUM_LEVELS, numberOfLevels)
+            }
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
+            row.logChange(COL_NETWORK_ID, networkId)
+            row.logChange(COL_SUB_ID, subscriptionId)
+            row.logChange(COL_VALIDATED, true)
+            row.logChange(COL_LEVEL, level)
+            row.logChange(COL_NUM_LEVELS, numberOfLevels)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
+
+    /** Provides information about an active wifi network. */
+    data class Active(
+        /**
+         * The [android.net.Network.netId] we received from
+         * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+         *
+         * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+         */
+        val networkId: Int,
+
+        /** See [android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED]. */
+        val isValidated: Boolean = false,
+
+        /** The wifi signal level, guaranteed to be 0 <= level <= 4. */
+        val level: Int,
+
+        /** See [android.net.wifi.WifiInfo.ssid]. */
+        val ssid: String? = null,
+
+        /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
+        val isPasspointAccessPoint: Boolean = false,
+
+        /** See [android.net.wifi.WifiInfo.isOsuAp]. */
+        val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
+
+        /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
+        val passpointProviderFriendlyName: String? = null,
+    ) : WifiNetworkModel() {
+        init {
+            require(level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
+                "0 <= wifi level <= 4 required; level was $level"
+            }
+        }
+
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal !is Active) {
+                logFull(row)
+                return
+            }
+
+            if (prevVal.networkId != networkId) {
+                row.logChange(COL_NETWORK_ID, networkId)
+            }
+            if (prevVal.isValidated != isValidated) {
+                row.logChange(COL_VALIDATED, isValidated)
+            }
+            if (prevVal.level != level) {
+                row.logChange(COL_LEVEL, level)
+            }
+            if (prevVal.ssid != ssid) {
+                row.logChange(COL_SSID, ssid)
+            }
+
+            // TODO(b/238425913): The passpoint-related values are frequently never used, so it
+            //   would be great to not log them when they're not used.
+            if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
+                row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+            }
+            if (
+                prevVal.isOnlineSignUpForPasspointAccessPoint !=
+                    isOnlineSignUpForPasspointAccessPoint
+            ) {
+                row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+            }
+            if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
+                row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+            }
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+            row.logChange(COL_NETWORK_ID, networkId)
+            row.logChange(COL_SUB_ID, null)
+            row.logChange(COL_VALIDATED, isValidated)
+            row.logChange(COL_LEVEL, level)
+            row.logChange(COL_NUM_LEVELS, null)
+            row.logChange(COL_SSID, ssid)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+            row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+            row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+        }
+
+        override fun toString(): String {
+            // Only include the passpoint-related values in the string if we have them. (Most
+            // networks won't have them so they'll be mostly clutter.)
+            val passpointString =
+                if (
+                    isPasspointAccessPoint ||
+                        isOnlineSignUpForPasspointAccessPoint ||
+                        passpointProviderFriendlyName != null
+                ) {
+                    ", isPasspointAp=$isPasspointAccessPoint, " +
+                        "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " +
+                        "passpointName=$passpointProviderFriendlyName"
+                } else {
+                    ""
+                }
+
+            return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " +
+                "level=$level, ssid=$ssid$passpointString)"
+        }
+
+        companion object {
+            @VisibleForTesting internal const val MAX_VALID_LEVEL = 4
+        }
+    }
+
+    companion object {
+        @VisibleForTesting internal const val MIN_VALID_LEVEL = 0
+    }
+}
+
+const val TYPE_CARRIER_MERGED = "CarrierMerged"
+const val TYPE_UNAVAILABLE = "Unavailable"
+const val TYPE_INACTIVE = "Inactive"
+const val TYPE_ACTIVE = "Active"
+
+const val COL_NETWORK_TYPE = "type"
+const val COL_NETWORK_ID = "networkId"
+const val COL_SUB_ID = "subscriptionId"
+const val COL_VALIDATED = "isValidated"
+const val COL_LEVEL = "level"
+const val COL_NUM_LEVELS = "maxLevel"
+const val COL_SSID = "ssid"
+const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
+const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
+const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
+
+val LEVEL_DEFAULT: String? = null
+val NUM_LEVELS_DEFAULT: String? = null
+val NETWORK_ID_DEFAULT: String? = null
+val SUB_ID_DEFAULT: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
index e491d2b..094bcf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
@@ -53,4 +53,4 @@
     }
 }
 
-private const val COL_ICON = "wifiIcon"
+private const val COL_ICON = "icon"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 824b597..1057231 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -25,8 +25,6 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
-import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
@@ -34,13 +32,13 @@
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
+import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 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.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -55,15 +53,12 @@
 /**
  * Models the UI state for the status bar wifi icon.
  *
- * This class exposes three view models, one per status bar location:
- *  - [home]
- *  - [keyguard]
- *  - [qs]
- *  In order to get the UI state for the wifi icon, you must use one of those view models (whichever
- *  is correct for your location).
+ * This class exposes three view models, one per status bar location: [home], [keyguard], and [qs].
+ * In order to get the UI state for the wifi icon, you must use one of those view models (whichever
+ * is correct for your location).
  *
- * Internally, this class maintains the current state of the wifi icon and notifies those three
- * view models of any changes.
+ * Internally, this class maintains the current state of the wifi icon and notifies those three view
+ * models of any changes.
  */
 @SysUISingleton
 class WifiViewModel
@@ -72,7 +67,6 @@
     airplaneModeViewModel: AirplaneModeViewModel,
     connectivityConstants: ConnectivityConstants,
     private val context: Context,
-    logger: ConnectivityPipelineLogger,
     @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
     interactor: WifiInteractor,
     @Application private val scope: CoroutineScope,
@@ -83,13 +77,15 @@
     private fun WifiNetworkModel.icon(): WifiIcon {
         return when (this) {
             is WifiNetworkModel.Unavailable -> WifiIcon.Hidden
+            is WifiNetworkModel.Invalid -> WifiIcon.Hidden
             is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
-            is WifiNetworkModel.Inactive -> WifiIcon.Visible(
-                res = WIFI_NO_NETWORK,
-                ContentDescription.Loaded(
-                    "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
+            is WifiNetworkModel.Inactive ->
+                WifiIcon.Visible(
+                    res = WIFI_NO_NETWORK,
+                    ContentDescription.Loaded(
+                        "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
+                    )
                 )
-            )
             is WifiNetworkModel.Active -> {
                 val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
                 when {
@@ -113,25 +109,25 @@
     /** The wifi icon that should be displayed. */
     private val wifiIcon: StateFlow<WifiIcon> =
         combine(
-            interactor.isEnabled,
-            interactor.isDefault,
-            interactor.isForceHidden,
-            interactor.wifiNetwork,
-        ) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
-            if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
-                return@combine WifiIcon.Hidden
-            }
+                interactor.isEnabled,
+                interactor.isDefault,
+                interactor.isForceHidden,
+                interactor.wifiNetwork,
+            ) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
+                if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
+                    return@combine WifiIcon.Hidden
+                }
 
-            val icon = wifiNetwork.icon()
+                val icon = wifiNetwork.icon()
 
-            return@combine when {
-                isDefault -> icon
-                wifiConstants.alwaysShowIconIfEnabled -> icon
-                !connectivityConstants.hasDataCapabilities -> icon
-                wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
-                else -> WifiIcon.Hidden
+                return@combine when {
+                    isDefault -> icon
+                    wifiConstants.alwaysShowIconIfEnabled -> icon
+                    !connectivityConstants.hasDataCapabilities -> icon
+                    wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
+                    else -> WifiIcon.Hidden
+                }
             }
-        }
             .logDiffsForTable(
                 wifiTableLogBuffer,
                 columnPrefix = "",
@@ -144,36 +140,42 @@
             )
 
     /** The wifi activity status. Null if we shouldn't display the activity status. */
-    private val activity: Flow<DataActivityModel?> =
+    private val activity: Flow<DataActivityModel> = run {
+        val default = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
         if (!connectivityConstants.shouldShowActivityConfig) {
-            flowOf(null)
-        } else {
-            combine(interactor.activity, interactor.ssid) { activity, ssid ->
-                when (ssid) {
-                    null -> null
-                    else -> activity
+                flowOf(default)
+            } else {
+                combine(interactor.activity, interactor.ssid) { activity, ssid ->
+                    when (ssid) {
+                        null -> default
+                        else -> activity
+                    }
                 }
             }
-        }
-        .distinctUntilChanged()
-        .logOutputChange(logger, "activity")
-        .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTableLogBuffer,
+                columnPrefix = "VM.activity",
+                initialValue = default,
+            )
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = default)
+    }
 
     private val isActivityInViewVisible: Flow<Boolean> =
-         activity
-             .map { it?.hasActivityIn == true }
-             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+        activity
+            .map { it.hasActivityIn }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
     private val isActivityOutViewVisible: Flow<Boolean> =
-       activity
-           .map { it?.hasActivityOut == true }
-           .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+        activity
+            .map { it.hasActivityOut }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
     private val isActivityContainerVisible: Flow<Boolean> =
-         combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut ->
-                    activityIn || activityOut
-                }
-             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+        combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut ->
+                activityIn || activityOut
+            }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
     // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the
     //  airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 68d30d3..2b4f51c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -60,7 +60,7 @@
      * animation to and from the parent dialog.
      */
     @JvmOverloads
-    open fun onUserListItemClicked(
+    fun onUserListItemClicked(
         record: UserRecord,
         dialogShower: DialogShower? = null,
     ) {
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/EmergencyCryptkeeperText.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
deleted file mode 100644
index 21a8300..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.policy;
-
-import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.provider.Settings;
-import android.telephony.SubscriptionInfo;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.systemui.Dependency;
-
-import java.util.List;
-
-public class EmergencyCryptkeeperText extends TextView {
-
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
-        @Override
-        public void onPhoneStateChanged(int phoneState) {
-            update();
-        }
-
-        @Override
-        public void onRefreshCarrierInfo() {
-            update();
-        }
-    };
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
-                update();
-            }
-        }
-    };
-
-    public EmergencyCryptkeeperText(Context context, @Nullable AttributeSet attrs) {
-        super(context, attrs);
-        setVisibility(GONE);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
-        mKeyguardUpdateMonitor.registerCallback(mCallback);
-        getContext().registerReceiver(mReceiver,
-                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
-        update();
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        if (mKeyguardUpdateMonitor != null) {
-            mKeyguardUpdateMonitor.removeCallback(mCallback);
-        }
-        getContext().unregisterReceiver(mReceiver);
-    }
-
-    private boolean iccCardExist(int simState) {
-        return ((simState == TelephonyManager.SIM_STATE_PIN_REQUIRED)
-                || (simState == TelephonyManager.SIM_STATE_PUK_REQUIRED)
-                || (simState == TelephonyManager.SIM_STATE_NETWORK_LOCKED)
-                || (simState == TelephonyManager.SIM_STATE_READY)
-                || (simState == TelephonyManager.SIM_STATE_NOT_READY)
-                || (simState == TelephonyManager.SIM_STATE_PERM_DISABLED)
-                || (simState == TelephonyManager.SIM_STATE_CARD_IO_ERROR)
-                || (simState == TelephonyManager.SIM_STATE_CARD_RESTRICTED)
-                || (simState == TelephonyManager.SIM_STATE_LOADED));
-    }
-
-    public void update() {
-        boolean hasMobile = mContext.getSystemService(TelephonyManager.class).isDataCapable();
-        boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(),
-                Settings.Global.AIRPLANE_MODE_ON, 0) == 1);
-
-        if (!hasMobile || airplaneMode) {
-            setText(null);
-            setVisibility(GONE);
-            return;
-        }
-
-        boolean allSimsMissing = true;
-        CharSequence displayText = null;
-
-        List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getFilteredSubscriptionInfo();
-        final int N = subs.size();
-        for (int i = 0; i < N; i++) {
-            int subId = subs.get(i).getSubscriptionId();
-            int simState = mKeyguardUpdateMonitor.getSimState(subId);
-            CharSequence carrierName = subs.get(i).getCarrierName();
-            if (iccCardExist(simState) && !TextUtils.isEmpty(carrierName)) {
-                allSimsMissing = false;
-                displayText = carrierName;
-            }
-        }
-        if (allSimsMissing) {
-            if (N != 0) {
-                // Shows "Emergency calls only" on devices that are voice-capable.
-                // This depends on mPlmn containing the text "Emergency calls only" when the radio
-                // has some connectivity. Otherwise it should show "No service"
-                // Grab the first subscription, because they all should contain the emergency text,
-                // described above.
-                displayText = subs.get(0).getCarrierName();
-            } else {
-                // We don't have a SubscriptionInfo to get the emergency calls only from.
-                // Grab it from the old sticky broadcast if possible instead. We can use it
-                // here because no subscriptions are active, so we don't have
-                // to worry about MSIM clashing.
-                displayText = getContext().getText(
-                        com.android.internal.R.string.emergency_calls_only);
-                Intent i = getContext().registerReceiver(null,
-                        new IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED));
-                if (i != null) {
-                    displayText = i.getStringExtra(TelephonyManager.EXTRA_PLMN);
-                }
-            }
-        }
-
-        setText(displayText);
-        setVisibility(TextUtils.isEmpty(displayText) ? GONE : VISIBLE);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EncryptionHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EncryptionHelper.java
deleted file mode 100644
index 9c099f9..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EncryptionHelper.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.policy;
-
-import android.sysprop.VoldProperties;
-
-/**
- * Helper for determining whether the phone is decrypted yet.
- */
-public class EncryptionHelper {
-
-    public static final boolean IS_DATA_ENCRYPTED = isDataEncrypted();
-
-    private static boolean isDataEncrypted() {
-        String voldState = VoldProperties.decrypt().orElse("");
-        return "1".equals(voldState) || "trigger_restart_min_framework".equals(voldState);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
index 9946b4b..5dcafb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.policy;
 
 import android.annotation.WorkerThread;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCharacteristics;
@@ -255,7 +254,6 @@
                 setTorchMode(enabled);
                 mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
                 mSecureSettings.putInt(Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0);
-                mBroadcastSender.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED));
             }
         }
 
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/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index f63d652..c8ee647 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -160,7 +160,7 @@
         mStatusBarStateController = statusBarStateController;
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView,
                 keyguardStateController, dozeParameters,
-                screenOffAnimationController,  /* animateYPos= */ false);
+                screenOffAnimationController,  /* animateYPos= */ false, /* logBuffer= */ null);
         mUserSwitchDialogController = userSwitchDialogController;
         mUiEventLogger = uiEventLogger;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index 1ae1eae..8929e02 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -56,7 +56,7 @@
     /**
      * Whether the bouncer (PIN/password entry) is currently visible.
      */
-    boolean isBouncerShowing();
+    boolean isPrimaryBouncerShowing();
 
     /**
      * If swiping up will unlock without asking for a password.
@@ -196,7 +196,7 @@
     /** **/
     default void notifyKeyguardState(boolean showing, boolean occluded) {}
     /** **/
-    default void notifyBouncerShowing(boolean showing) {}
+    default void notifyPrimaryBouncerShowing(boolean showing) {}
 
     /**
      * Updates the keyguard state to reflect that it's in the process of being dismissed, either by
@@ -244,7 +244,7 @@
         /**
          * Called when the bouncer (PIN/password entry) is shown or hidden.
          */
-        default void onBouncerShowingChanged() {}
+        default void onPrimaryBouncerShowingChanged() {}
 
         /**
          * Triggered when the device was just unlocked and the lock screen is being dismissed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index cc6fdcc..805368c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -65,7 +65,7 @@
 
     private boolean mCanDismissLockScreen;
     private boolean mShowing;
-    private boolean mBouncerShowing;
+    private boolean mPrimaryBouncerShowing;
     private boolean mSecure;
     private boolean mOccluded;
 
@@ -157,8 +157,8 @@
     }
 
     @Override
-    public boolean isBouncerShowing() {
-        return mBouncerShowing;
+    public boolean isPrimaryBouncerShowing() {
+        return mPrimaryBouncerShowing;
     }
 
     @Override
@@ -339,11 +339,11 @@
     }
 
     @Override
-    public void notifyBouncerShowing(boolean showing) {
-        if (mBouncerShowing != showing) {
-            mBouncerShowing = showing;
+    public void notifyPrimaryBouncerShowing(boolean showing) {
+        if (mPrimaryBouncerShowing != showing) {
+            mPrimaryBouncerShowing = showing;
 
-            new ArrayList<>(mCallbacks).forEach(Callback::onBouncerShowingChanged);
+            new ArrayList<>(mCallbacks).forEach(Callback::onPrimaryBouncerShowingChanged);
         }
     }
 
@@ -438,6 +438,11 @@
         }
 
         @Override
+        public void onLockedOutStateChanged(BiometricSourceType biometricSourceType) {
+            update(false /* updateAlways */);
+        }
+
+        @Override
         public void onKeyguardVisibilityChanged(boolean visible) {
             update(false /* updateAlways */);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index c150654..e9f0dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -173,7 +173,7 @@
                 mUserSwitcherController, this);
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView,
                 keyguardStateController, dozeParameters,
-                screenOffAnimationController, /* animateYPos= */ false);
+                screenOffAnimationController, /* animateYPos= */ false, /* logBuffer= */ null);
         mBackground = new KeyguardUserSwitcherScrim(context);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
new file mode 100644
index 0000000..2a18b81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use mHost 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.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.AlarmTile
+import com.android.systemui.qs.tiles.CameraToggleTile
+import com.android.systemui.qs.tiles.DndTile
+import com.android.systemui.qs.tiles.FlashlightTile
+import com.android.systemui.qs.tiles.LocationTile
+import com.android.systemui.qs.tiles.MicrophoneToggleTile
+import com.android.systemui.qs.tiles.UiModeNightTile
+import com.android.systemui.qs.tiles.WorkModeTile
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface PolicyModule {
+
+    /** Inject DndTile into tileMap in QSModule */
+    @Binds @IntoMap @StringKey(DndTile.TILE_SPEC) fun bindDndTile(dndTile: DndTile): QSTileImpl<*>
+
+    /** Inject WorkModeTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(WorkModeTile.TILE_SPEC)
+    fun bindWorkModeTile(workModeTile: WorkModeTile): QSTileImpl<*>
+
+    /** Inject FlashlightTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(FlashlightTile.TILE_SPEC)
+    fun bindFlashlightTile(flashlightTile: FlashlightTile): QSTileImpl<*>
+
+    /** Inject LocationTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(LocationTile.TILE_SPEC)
+    fun bindLocationTile(locationTile: LocationTile): QSTileImpl<*>
+
+    /** Inject CameraToggleTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(CameraToggleTile.TILE_SPEC)
+    fun bindCameraToggleTile(cameraToggleTile: CameraToggleTile): QSTileImpl<*>
+
+    /** Inject MicrophoneToggleTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(MicrophoneToggleTile.TILE_SPEC)
+    fun bindMicrophoneToggleTile(microphoneToggleTile: MicrophoneToggleTile): QSTileImpl<*>
+
+    /** Inject AlarmTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(AlarmTile.TILE_SPEC)
+    fun bindAlarmTile(alarmTile: AlarmTile): QSTileImpl<*>
+
+    @Binds
+    @IntoMap
+    @StringKey(UiModeNightTile.TILE_SPEC)
+    fun bindUiModeNightTile(uiModeNightTile: UiModeNightTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index c9ed0cb..4866f73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -109,6 +109,8 @@
     private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
     private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
     private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
+    private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120;
+    private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180;
 
     public final Object mToken = new Object();
 
@@ -301,7 +303,8 @@
                     mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null
                             && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime());
                     if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
-                        mController.removeRemoteInput(mEntry, mToken);
+                        // Pass null to ensure all inputs are cleared for this entry b/227115380
+                        mController.removeRemoteInput(mEntry, null);
                     }
                 }
             }
@@ -421,7 +424,7 @@
     }
 
     @VisibleForTesting
-    void onDefocus(boolean animate, boolean logClose) {
+    void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) {
         mController.removeRemoteInput(mEntry, mToken);
         mEntry.remoteInputText = mEditText.getText();
 
@@ -431,18 +434,20 @@
             ViewGroup parent = (ViewGroup) getParent();
             if (animate && parent != null && mIsFocusAnimationFlagActive) {
 
-
                 ViewGroup grandParent = (ViewGroup) parent.getParent();
                 ViewGroupOverlay overlay = parent.getOverlay();
+                View actionsContainer = getActionsContainerLayout();
+                int actionsContainerHeight =
+                        actionsContainer != null ? actionsContainer.getHeight() : 0;
 
                 // After adding this RemoteInputView to the overlay of the parent (and thus removing
                 // it from the parent itself), the parent will shrink in height. This causes the
                 // overlay to be moved. To correct the position of the overlay we need to offset it.
-                int overlayOffsetY = getMaxSiblingHeight() - getHeight();
+                int overlayOffsetY = actionsContainerHeight - getHeight();
                 overlay.add(this);
                 if (grandParent != null) grandParent.setClipChildren(false);
 
-                Animator animator = getDefocusAnimator(overlayOffsetY);
+                Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY);
                 View self = this;
                 animator.addListener(new AnimatorListenerAdapter() {
                     @Override
@@ -454,8 +459,12 @@
                         if (mWrapper != null) {
                             mWrapper.setRemoteInputVisible(false);
                         }
+                        if (doAfterDefocus != null) {
+                            doAfterDefocus.run();
+                        }
                     }
                 });
+                if (actionsContainer != null) actionsContainer.setAlpha(0f);
                 animator.start();
 
             } else if (animate && mRevealParams != null && mRevealParams.radius > 0) {
@@ -474,6 +483,7 @@
                 reveal.start();
             } else {
                 setVisibility(GONE);
+                if (doAfterDefocus != null) doAfterDefocus.run();
                 if (mWrapper != null) {
                     mWrapper.setRemoteInputVisible(false);
                 }
@@ -596,10 +606,8 @@
 
     /**
      * Focuses the RemoteInputView and animates its appearance
-     *
-     * @param crossFadeView view that will be crossfaded during the appearance animation
      */
-    public void focusAnimated(View crossFadeView) {
+    public void focusAnimated() {
         if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE
                 && mRevealParams != null) {
             android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this);
@@ -609,7 +617,7 @@
         } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
             mIsAnimatingAppearance = true;
             setAlpha(0f);
-            Animator focusAnimator = getFocusAnimator(crossFadeView);
+            Animator focusAnimator = getFocusAnimator(getActionsContainerLayout());
             focusAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation, boolean isReverse) {
@@ -661,6 +669,23 @@
     }
 
     private void reset() {
+        if (mIsFocusAnimationFlagActive) {
+            mProgressBar.setVisibility(INVISIBLE);
+            mResetting = true;
+            mSending = false;
+            onDefocus(true /* animate */, false /* logClose */, () -> {
+                mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
+                mEditText.getText().clear();
+                mEditText.setEnabled(isAggregatedVisible());
+                mSendButton.setVisibility(VISIBLE);
+                mController.removeSpinning(mEntry.getKey(), mToken);
+                updateSendButton();
+                setAttachment(null);
+                mResetting = false;
+            });
+            return;
+        }
+
         mResetting = true;
         mSending = false;
         mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
@@ -671,7 +696,7 @@
         mProgressBar.setVisibility(INVISIBLE);
         mController.removeSpinning(mEntry.getKey(), mToken);
         updateSendButton();
-        onDefocus(false /* animate */, false /* logClose */);
+        onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */);
         setAttachment(null);
 
         mResetting = false;
@@ -825,23 +850,22 @@
     }
 
     /**
-     * @return max sibling height (0 in case of no siblings)
+     * @return action button container view (i.e. ViewGroup containing Reply button etc.)
      */
-    public int getMaxSiblingHeight() {
+    public View getActionsContainerLayout() {
         ViewGroup parentView = (ViewGroup) getParent();
-        int maxHeight = 0;
-        if (parentView == null) return 0;
-        for (int i = 0; i < parentView.getChildCount(); i++) {
-            View siblingView = parentView.getChildAt(i);
-            if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight());
-        }
-        return maxHeight;
+        if (parentView == null) return null;
+        return parentView.findViewById(com.android.internal.R.id.actions_container_layout);
     }
 
     /**
      * Creates an animator for the focus animation.
+     *
+     * @param fadeOutView View that will be faded out during the focus animation.
      */
-    private Animator getFocusAnimator(View crossFadeView) {
+    private Animator getFocusAnimator(@Nullable View fadeOutView) {
+        final AnimatorSet animatorSet = new AnimatorSet();
+
         final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
         alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
         alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
@@ -854,30 +878,36 @@
         scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
         scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
 
-        final Animator crossFadeViewAlphaAnimator =
-                ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f);
-        crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
-        crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
-        alphaAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation, boolean isReverse) {
-                crossFadeView.setAlpha(1f);
-            }
-        });
-
-        final AnimatorSet animatorSet = new AnimatorSet();
-        animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator);
+        if (fadeOutView == null) {
+            animatorSet.playTogether(alphaAnimator, scaleAnimator);
+        } else {
+            final Animator fadeOutViewAlphaAnimator =
+                    ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f);
+            fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+            fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+            animatorSet.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation, boolean isReverse) {
+                    fadeOutView.setAlpha(1f);
+                }
+            });
+            animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator);
+        }
         return animatorSet;
     }
 
     /**
      * Creates an animator for the defocus animation.
      *
-     * @param offsetY The RemoteInputView will be offset by offsetY during the animation
+     * @param fadeInView View that will be faded in during the defocus animation.
+     * @param offsetY    The RemoteInputView will be offset by offsetY during the animation
      */
-    private Animator getDefocusAnimator(int offsetY) {
+    private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) {
+        final AnimatorSet animatorSet = new AnimatorSet();
+
         final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
-        alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+        alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
+        alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY);
         alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
 
         ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
@@ -893,8 +923,17 @@
             }
         });
 
-        final AnimatorSet animatorSet = new AnimatorSet();
-        animatorSet.playTogether(alphaAnimator, scaleAnimator);
+        if (fadeInView == null) {
+            animatorSet.playTogether(alphaAnimator, scaleAnimator);
+        } else {
+            fadeInView.forceHasOverlappingRendering(false);
+            Animator fadeInViewAlphaAnimator =
+                    ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f);
+            fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
+            fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+            fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY);
+            animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator);
+        }
         return animatorSet;
     }
 
@@ -1011,7 +1050,8 @@
             if (isFocusable() && isEnabled()) {
                 setInnerFocusable(false);
                 if (mRemoteInputView != null) {
-                    mRemoteInputView.onDefocus(animate, true /* logClose */);
+                    mRemoteInputView
+                            .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */);
                 }
                 mShowImeOnInputConnection = false;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
index e0d780a..7a4e35f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
@@ -48,6 +48,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
@@ -73,6 +74,7 @@
     private boolean mIsAttached;
 
     private final ViewGroup mStatusBarWindowView;
+    private final FragmentService mFragmentService;
     // The container in which we should run launch animations started from the status bar and
     //   expanding into the opening window.
     private final ViewGroup mLaunchAnimationContainer;
@@ -86,6 +88,7 @@
             WindowManager windowManager,
             IWindowManager iWindowManager,
             StatusBarContentInsetsProvider contentInsetsProvider,
+            FragmentService fragmentService,
             @Main Resources resources,
             Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider) {
         mContext = context;
@@ -93,6 +96,7 @@
         mIWindowManager = iWindowManager;
         mContentInsetsProvider = contentInsetsProvider;
         mStatusBarWindowView = statusBarWindowView;
+        mFragmentService = fragmentService;
         mLaunchAnimationContainer = mStatusBarWindowView.findViewById(
                 R.id.status_bar_launch_animation_container);
         mLpChanged = new WindowManager.LayoutParams();
@@ -157,7 +161,7 @@
 
     /** Returns a fragment host manager for the status bar window view. */
     public FragmentHostManager getFragmentHostManager() {
-        return FragmentHostManager.get(mStatusBarWindowView);
+        return mFragmentService.getFragmentHostManager(mStatusBarWindowView);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index 60f6df6..8f424b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -61,6 +61,10 @@
         listeners.add(listener)
     }
 
+    fun removeListener(listener: StatusBarWindowStateListener) {
+        listeners.remove(listener)
+    }
+
     /** Returns true if the window is currently showing. */
     fun windowIsShowing() = windowState == WINDOW_STATE_SHOWING
 
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt
deleted file mode 100644
index 154c6e2..0000000
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.stylus
-
-import android.content.Context
-import android.hardware.BatteryState
-import android.hardware.input.InputManager
-import android.os.Handler
-import android.util.Log
-import android.view.InputDevice
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-/**
- * A listener that detects when a stylus has first been used, by detecting 1) the presence of an
- * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth
- * address.
- */
-@SysUISingleton
-class StylusFirstUsageListener
-@Inject
-constructor(
-    private val context: Context,
-    private val inputManager: InputManager,
-    private val stylusManager: StylusManager,
-    private val featureFlags: FeatureFlags,
-    @Background private val executor: Executor,
-    @Background private val handler: Handler,
-) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
-
-    // Set must be only accessed from the background handler, which is the same handler that
-    // runs the StylusManager callbacks.
-    private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf()
-    @VisibleForTesting var hasStarted = false
-
-    override fun start() {
-        if (true) return // TODO(b/261826950): remove on main
-        if (hasStarted) return
-        if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
-        if (inputManager.isStylusEverUsed(context)) return
-        if (!hostDeviceSupportsStylusInput()) return
-
-        hasStarted = true
-        inputManager.inputDeviceIds.forEach(this::onStylusAdded)
-        stylusManager.registerCallback(this)
-        stylusManager.startListener()
-    }
-
-    override fun onStylusAdded(deviceId: Int) {
-        if (!hasStarted) return
-
-        val device = inputManager.getInputDevice(deviceId) ?: return
-        if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return
-
-        try {
-            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
-            internalStylusDeviceIds += deviceId
-        } catch (e: SecurityException) {
-            Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.")
-        }
-    }
-
-    override fun onStylusRemoved(deviceId: Int) {
-        if (!hasStarted) return
-
-        if (!internalStylusDeviceIds.contains(deviceId)) return
-        try {
-            inputManager.removeInputDeviceBatteryListener(deviceId, this)
-            internalStylusDeviceIds.remove(deviceId)
-        } catch (e: SecurityException) {
-            Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
-        }
-    }
-
-    override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
-        if (!hasStarted) return
-
-        onRemoteDeviceFound()
-    }
-
-    override fun onBatteryStateChanged(
-        deviceId: Int,
-        eventTimeMillis: Long,
-        batteryState: BatteryState
-    ) {
-        if (!hasStarted) return
-
-        if (batteryState.isPresent) {
-            onRemoteDeviceFound()
-        }
-    }
-
-    private fun onRemoteDeviceFound() {
-        inputManager.setStylusEverUsed(context, true)
-        cleanupListeners()
-    }
-
-    private fun cleanupListeners() {
-        stylusManager.unregisterCallback(this)
-        handler.post {
-            internalStylusDeviceIds.forEach {
-                inputManager.removeInputDeviceBatteryListener(it, this)
-            }
-        }
-    }
-
-    private fun hostDeviceSupportsStylusInput(): Boolean {
-        return inputManager.inputDeviceIds
-            .asSequence()
-            .mapNotNull { inputManager.getInputDevice(it) }
-            .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
-    }
-
-    companion object {
-        private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 302d6a9..235495cf 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -18,6 +18,8 @@
 
 import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.hardware.BatteryState
 import android.hardware.input.InputManager
 import android.os.Handler
 import android.util.ArrayMap
@@ -25,6 +27,8 @@
 import android.view.InputDevice
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -37,25 +41,37 @@
 class StylusManager
 @Inject
 constructor(
+    private val context: Context,
     private val inputManager: InputManager,
     private val bluetoothAdapter: BluetoothAdapter?,
     @Background private val handler: Handler,
     @Background private val executor: Executor,
-) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {
+    private val featureFlags: FeatureFlags,
+) :
+    InputManager.InputDeviceListener,
+    InputManager.InputDeviceBatteryListener,
+    BluetoothAdapter.OnMetadataChangedListener {
 
     private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
     private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> =
         CopyOnWriteArrayList()
     // This map should only be accessed on the handler
     private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()
+    // This variable should only be accessed on the handler
+    private var hasStarted: Boolean = false
 
     /**
      * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
      * at time of starting.
      */
     fun startListener() {
-        addExistingStylusToMap()
-        inputManager.registerInputDeviceListener(this, handler)
+        handler.post {
+            if (hasStarted) return@post
+            hasStarted = true
+            addExistingStylusToMap()
+
+            inputManager.registerInputDeviceListener(this, handler)
+        }
     }
 
     /** Registers a StylusCallback to listen to stylus events. */
@@ -77,21 +93,30 @@
     }
 
     override fun onInputDeviceAdded(deviceId: Int) {
+        if (!hasStarted) return
+
         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
 
+        if (!device.isExternal) {
+            registerBatteryListener(deviceId)
+        }
+
         // TODO(b/257936830): get address once input api available
         val btAddress: String? = null
         inputDeviceAddressMap[deviceId] = btAddress
         executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
 
         if (btAddress != null) {
+            onStylusUsed()
             onStylusBluetoothConnected(btAddress)
             executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
         }
     }
 
     override fun onInputDeviceChanged(deviceId: Int) {
+        if (!hasStarted) return
+
         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
 
@@ -112,7 +137,10 @@
     }
 
     override fun onInputDeviceRemoved(deviceId: Int) {
+        if (!hasStarted) return
+
         if (!inputDeviceAddressMap.contains(deviceId)) return
+        unregisterBatteryListener(deviceId)
 
         val btAddress: String? = inputDeviceAddressMap[deviceId]
         inputDeviceAddressMap.remove(deviceId)
@@ -124,13 +152,14 @@
     }
 
     override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
-        handler.post executeMetadataChanged@{
-            if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null)
-                return@executeMetadataChanged
+        handler.post {
+            if (!hasStarted) return@post
+
+            if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post
 
             val inputDeviceId: Int =
                 inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
-                    ?: return@executeMetadataChanged
+                    ?: return@post
 
             val isCharging = String(value) == "true"
 
@@ -140,6 +169,24 @@
         }
     }
 
+    override fun onBatteryStateChanged(
+        deviceId: Int,
+        eventTimeMillis: Long,
+        batteryState: BatteryState
+    ) {
+        handler.post {
+            if (!hasStarted) return@post
+
+            if (batteryState.isPresent) {
+                onStylusUsed()
+            }
+
+            executeStylusBatteryCallbacks { cb ->
+                cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState)
+            }
+        }
+    }
+
     private fun onStylusBluetoothConnected(btAddress: String) {
         val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
         try {
@@ -158,6 +205,21 @@
         }
     }
 
+    /**
+     * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a
+     * physical stylus device has never been used. This method is run when 1) a USI stylus battery
+     * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a
+     * physical stylus device has actually been used.
+     */
+    private fun onStylusUsed() {
+        if (true) return // TODO(b/261826950): remove on main
+        if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
+        if (inputManager.isStylusEverUsed(context)) return
+
+        inputManager.setStylusEverUsed(context, true)
+        executeStylusCallbacks { cb -> cb.onStylusFirstUsed() }
+    }
+
     private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
         stylusCallbacks.forEach(run)
     }
@@ -166,31 +228,69 @@
         stylusBatteryCallbacks.forEach(run)
     }
 
+    private fun registerBatteryListener(deviceId: Int) {
+        try {
+            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
+        } catch (e: SecurityException) {
+            Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
+        }
+    }
+
+    private fun unregisterBatteryListener(deviceId: Int) {
+        // If deviceId wasn't registered, the result is a no-op, so an "is registered"
+        // check is not needed.
+        try {
+            inputManager.removeInputDeviceBatteryListener(deviceId, this)
+        } catch (e: SecurityException) {
+            Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
+        }
+    }
+
     private fun addExistingStylusToMap() {
         for (deviceId: Int in inputManager.inputDeviceIds) {
             val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
             if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
                 // TODO(b/257936830): get address once input api available
                 inputDeviceAddressMap[deviceId] = null
+
+                if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
+                    // For most devices, an active (non-bluetooth) stylus is represented by an
+                    // internal InputDevice. This InputDevice will be present in InputManager
+                    // before CoreStartables run, and will not be removed.
+                    // In many cases, it reports the battery level of the stylus.
+                    registerBatteryListener(deviceId)
+                }
             }
         }
     }
 
-    /** Callback interface to receive events from the StylusManager. */
+    /**
+     * Callback interface to receive events from the StylusManager. All callbacks are run on the
+     * same background handler.
+     */
     interface StylusCallback {
         fun onStylusAdded(deviceId: Int) {}
         fun onStylusRemoved(deviceId: Int) {}
         fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {}
         fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
+        fun onStylusFirstUsed() {}
     }
 
-    /** Callback interface to receive stylus battery events from the StylusManager. */
+    /**
+     * Callback interface to receive stylus battery events from the StylusManager. All callbacks are
+     * runs on the same background handler.
+     */
     interface StylusBatteryCallback {
         fun onStylusBluetoothChargingStateChanged(
             inputDeviceId: Int,
             btDevice: BluetoothDevice,
             isCharging: Boolean
         ) {}
+        fun onStylusUsiBatteryStateChanged(
+            deviceId: Int,
+            eventTimeMillis: Long,
+            batteryState: BatteryState,
+        ) {}
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
index 11233dd..dde2a80 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
@@ -18,14 +18,11 @@
 
 import android.hardware.BatteryState
 import android.hardware.input.InputManager
-import android.util.Log
 import android.view.InputDevice
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import java.util.concurrent.Executor
 import javax.inject.Inject
 
 /**
@@ -40,14 +37,16 @@
     private val inputManager: InputManager,
     private val stylusUsiPowerUi: StylusUsiPowerUI,
     private val featureFlags: FeatureFlags,
-    @Background private val executor: Executor,
-) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
+) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback {
 
     override fun onStylusAdded(deviceId: Int) {
+        // On some devices, the addition of a new internal stylus indicates the use of a
+        // USI stylus with a different vendor/product ID. We would therefore like to reset
+        // the battery notification suppression, in case the user has dismissed a low battery
+        // notification of the previous stylus.
         val device = inputManager.getInputDevice(deviceId) ?: return
-
         if (!device.isExternal) {
-            registerBatteryListener(deviceId)
+            stylusUsiPowerUi.updateSuppression(false)
         }
     }
 
@@ -59,57 +58,30 @@
         stylusUsiPowerUi.refresh()
     }
 
-    override fun onStylusRemoved(deviceId: Int) {
-        val device = inputManager.getInputDevice(deviceId) ?: return
-
-        if (!device.isExternal) {
-            unregisterBatteryListener(deviceId)
-        }
-    }
-
-    override fun onBatteryStateChanged(
+    override fun onStylusUsiBatteryStateChanged(
         deviceId: Int,
         eventTimeMillis: Long,
         batteryState: BatteryState
     ) {
-        if (batteryState.isPresent) {
-            stylusUsiPowerUi.updateBatteryState(batteryState)
-        }
-    }
-
-    private fun registerBatteryListener(deviceId: Int) {
-        try {
-            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
-        } catch (e: SecurityException) {
-            Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
-        }
-    }
-
-    private fun unregisterBatteryListener(deviceId: Int) {
-        try {
-            inputManager.removeInputDeviceBatteryListener(deviceId, this)
-        } catch (e: SecurityException) {
-            Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.")
+        if (batteryState.isPresent && batteryState.capacity > 0f) {
+            stylusUsiPowerUi.updateBatteryState(deviceId, batteryState)
         }
     }
 
     override fun start() {
         if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return
-        addBatteryListenerForInternalStyluses()
+        if (!hostDeviceSupportsStylusInput()) return
 
+        stylusUsiPowerUi.init()
         stylusManager.registerCallback(this)
         stylusManager.startListener()
     }
 
-    private fun addBatteryListenerForInternalStyluses() {
-        // For most devices, an active stylus is represented by an internal InputDevice.
-        // This InputDevice will be present in InputManager before CoreStartables run,
-        // and will not be removed. In many cases, it reports the battery level of the stylus.
-        inputManager.inputDeviceIds
+    private fun hostDeviceSupportsStylusInput(): Boolean {
+        return inputManager.inputDeviceIds
             .asSequence()
             .mapNotNull { inputManager.getInputDevice(it) }
-            .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) }
-            .forEach { onStylusAdded(it.id) }
+            .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
index 70a5b36..8d5e01c 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
@@ -18,17 +18,21 @@
 
 import android.Manifest
 import android.app.PendingIntent
+import android.content.ActivityNotFoundException
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.hardware.BatteryState
 import android.hardware.input.InputManager
+import android.os.Bundle
 import android.os.Handler
 import android.os.UserHandle
+import android.util.Log
 import android.view.InputDevice
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -53,6 +57,7 @@
     // These values must only be accessed on the handler.
     private var batteryCapacity = 1.0f
     private var suppressed = false
+    private var inputDeviceId: Int? = null
 
     fun init() {
         val filter =
@@ -87,10 +92,12 @@
         }
     }
 
-    fun updateBatteryState(batteryState: BatteryState) {
+    fun updateBatteryState(deviceId: Int, batteryState: BatteryState) {
         handler.post updateBattery@{
-            if (batteryState.capacity == batteryCapacity) return@updateBattery
+            if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f)
+                return@updateBattery
 
+            inputDeviceId = deviceId
             batteryCapacity = batteryState.capacity
             refresh()
         }
@@ -123,13 +130,13 @@
                 .setSmallIcon(R.drawable.ic_power_low)
                 .setDeleteIntent(getPendingBroadcast(ACTION_DISMISSED_LOW_BATTERY))
                 .setContentIntent(getPendingBroadcast(ACTION_CLICKED_LOW_BATTERY))
-                .setContentTitle(context.getString(R.string.stylus_battery_low))
-                .setContentText(
+                .setContentTitle(
                     context.getString(
-                        R.string.battery_low_percent_format,
+                        R.string.stylus_battery_low_percentage,
                         NumberFormat.getPercentInstance().format(batteryCapacity)
                     )
                 )
+                .setContentText(context.getString(R.string.stylus_battery_low_subtitle))
                 .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                 .setLocalOnly(true)
                 .setAutoCancel(true)
@@ -150,23 +157,41 @@
     }
 
     private fun getPendingBroadcast(action: String): PendingIntent? {
-        return PendingIntent.getBroadcastAsUser(
+        return PendingIntent.getBroadcast(
             context,
             0,
-            Intent(action),
+            Intent(action).setPackage(context.packageName),
             PendingIntent.FLAG_IMMUTABLE,
-            UserHandle.CURRENT
         )
     }
 
-    private val receiver: BroadcastReceiver =
+    @VisibleForTesting
+    internal val receiver: BroadcastReceiver =
         object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 when (intent.action) {
                     ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true)
                     ACTION_CLICKED_LOW_BATTERY -> {
                         updateSuppression(true)
-                        // TODO(b/261584943): open USI device details page
+                        if (inputDeviceId == null) return
+
+                        val args = Bundle()
+                        args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!)
+                        try {
+                            context.startActivity(
+                                Intent(ACTION_STYLUS_USI_DETAILS)
+                                    .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args)
+                                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                            )
+                        } catch (e: ActivityNotFoundException) {
+                            // In the rare scenario where the Settings app manifest doesn't contain
+                            // the USI details activity, ignore the intent.
+                            Log.e(
+                                StylusUsiPowerUI::class.java.simpleName,
+                                "Cannot open USI details page."
+                            )
+                        }
                     }
                 }
             }
@@ -177,9 +202,13 @@
         // https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41
         private const val LOW_BATTERY_THRESHOLD = 0.16f
 
-        private val USI_NOTIFICATION_ID = R.string.stylus_battery_low
+        private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage
 
-        private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
-        private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+        @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
+        @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+        @VisibleForTesting
+        const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS"
+        @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id"
+        @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt
new file mode 100644
index 0000000..e092f01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.telephony.ui.activity
+
+import android.app.ActivityOptions
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.UserHandle
+import android.util.Log
+import android.view.WindowManager
+import com.android.internal.app.AlertActivity
+import com.android.systemui.R
+
+/** Dialog shown to the user to switch to managed profile for making a call using work SIM. */
+class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.OnClickListener {
+    private lateinit var phoneNumber: Uri
+    private var managedProfileUserId = UserHandle.USER_NULL
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        window.addSystemFlags(
+            WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
+        )
+        super.onCreate(savedInstanceState)
+
+        phoneNumber = intent.getData()
+        managedProfileUserId =
+            intent.getIntExtra(
+                "android.telecom.extra.MANAGED_PROFILE_USER_ID",
+                UserHandle.USER_NULL
+            )
+
+        mAlertParams.apply {
+            mTitle = getString(R.string.call_from_work_profile_title)
+            mMessage = getString(R.string.call_from_work_profile_text)
+            mPositiveButtonText = getString(R.string.call_from_work_profile_action)
+            mNegativeButtonText = getString(R.string.call_from_work_profile_close)
+            mPositiveButtonListener = this@SwitchToManagedProfileForCallActivity
+            mNegativeButtonListener = this@SwitchToManagedProfileForCallActivity
+        }
+        setupAlert()
+    }
+
+    override fun onClick(dialog: DialogInterface?, which: Int) {
+        if (which == BUTTON_POSITIVE) {
+            switchToManagedProfile()
+        }
+        finish()
+    }
+
+    private fun switchToManagedProfile() {
+        try {
+            applicationContext.startActivityAsUser(
+                Intent(Intent.ACTION_DIAL, phoneNumber),
+                ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(),
+                UserHandle.of(managedProfileUserId)
+            )
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to launch activity", e)
+        }
+    }
+
+    companion object {
+        private const val TAG = "SwitchToManagedProfileForCallActivity"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index ad48e21..1065d33 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -31,6 +31,7 @@
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
 import androidx.annotation.CallSuper
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Main
@@ -108,9 +109,10 @@
      * Whenever the current view disappears, the next-priority view will be displayed if it's still
      * valid.
      */
+    @VisibleForTesting
     internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
 
-    private fun getCurrentDisplayInfo(): DisplayInfo? {
+    internal fun getCurrentDisplayInfo(): DisplayInfo? {
         return activeViews.getOrNull(0)
     }
 
@@ -119,15 +121,26 @@
         dumpManager.registerNormalDumpable(this)
     }
 
+    private val listeners: MutableSet<Listener> = mutableSetOf()
+
+    /** Registers a listener. */
+    fun registerListener(listener: Listener) {
+        listeners.add(listener)
+    }
+
+    /** Unregisters a listener. */
+    fun unregisterListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
     /**
      * Displays the view with the provided [newInfo].
      *
      * This method handles inflating and attaching the view, then delegates to [updateView] to
      * display the correct information in the view.
-     * @param onViewTimeout a runnable that runs after the view timeout.
      */
     @Synchronized
-    fun displayView(newInfo: T, onViewTimeout: Runnable? = null) {
+    fun displayView(newInfo: T) {
         val timeout = accessibilityManager.getRecommendedTimeoutMillis(
             newInfo.timeoutMs,
             // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
@@ -146,14 +159,13 @@
             logger.logViewUpdate(newInfo)
             currentDisplayInfo.info = newInfo
             currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
-            updateTimeout(currentDisplayInfo, timeout, onViewTimeout)
+            updateTimeout(currentDisplayInfo, timeout)
             updateView(newInfo, view)
             return
         }
 
         val newDisplayInfo = DisplayInfo(
             info = newInfo,
-            onViewTimeout = onViewTimeout,
             timeExpirationMillis = timeExpirationMillis,
             // Null values will be updated to non-null if/when this view actually gets displayed
             view = null,
@@ -196,7 +208,7 @@
     private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
         logger.logViewAddition(newDisplayInfo.info)
         createAndAcquireWakeLock(newDisplayInfo)
-        updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout)
+        updateTimeout(newDisplayInfo, timeout)
         inflateAndUpdateView(newDisplayInfo)
     }
 
@@ -227,19 +239,16 @@
     /**
      * Creates a runnable that will remove [displayInfo] in [timeout] ms from now.
      *
-     * @param onViewTimeout an optional runnable that will be run if the view times out.
      * @return a runnable that, when run, will *cancel* the view's timeout.
      */
-    private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) {
+    private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int) {
         val cancelViewTimeout = mainExecutor.executeDelayed(
             {
                 removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT)
-                onViewTimeout?.run()
             },
             timeout.toLong()
         )
 
-        displayInfo.onViewTimeout = onViewTimeout
         // Cancel old view timeout and re-set it.
         displayInfo.cancelViewTimeout?.run()
         displayInfo.cancelViewTimeout = cancelViewTimeout
@@ -317,6 +326,9 @@
         // event comes in while this view is animating out, we still display the new view
         // appropriately.
         activeViews.remove(displayInfo)
+        listeners.forEach {
+            it.onInfoPermanentlyRemoved(id, removalReason)
+        }
 
         // No need to time the view out since it's already gone
         displayInfo.cancelViewTimeout?.run()
@@ -380,6 +392,9 @@
         invalidViews.forEach {
             activeViews.remove(it)
             logger.logViewExpiration(it.info)
+            listeners.forEach { listener ->
+                listener.onInfoPermanentlyRemoved(it.info.id, REMOVAL_REASON_TIME_EXPIRED)
+            }
         }
     }
 
@@ -436,6 +451,15 @@
         onAnimationEnd.run()
     }
 
+    /** A listener interface to be notified of various view events. */
+    fun interface Listener {
+        /**
+         * Called whenever a [DisplayInfo] with the given [id] has been removed and will never be
+         * displayed again (unless another call to [updateView] is made).
+         */
+        fun onInfoPermanentlyRemoved(id: String, reason: String)
+    }
+
     /** A container for all the display-related state objects. */
     inner class DisplayInfo(
         /**
@@ -461,11 +485,6 @@
         var wakeLock: WakeLock?,
 
         /**
-         * See [displayView].
-         */
-        var onViewTimeout: Runnable?,
-
-        /**
          * A runnable that, when run, will cancel this view's timeout.
          *
          * Null if this info isn't currently being displayed.
@@ -475,6 +494,7 @@
 }
 
 private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
+private const val REMOVAL_REASON_TIME_EXPIRED = "TIMEOUT_EXPIRED_BEFORE_REDISPLAY"
 private const val MIN_REQUIRED_TIME_FOR_REDISPLAY = 1000
 
 private data class IconInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index ec6965a..899b0c2 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -80,6 +80,26 @@
         )
     }
 
+    /** Logs that there was a failure to animate the view in. */
+    fun logAnimateInFailure() {
+        buffer.log(
+            tag,
+            LogLevel.WARNING,
+            {},
+            { "View's appearance animation failed. Forcing view display manually." },
+        )
+    }
+
+    /** Logs that there was a failure to animate the view out. */
+    fun logAnimateOutFailure() {
+        buffer.log(
+            tag,
+            LogLevel.WARNING,
+            {},
+            { "View's disappearance animation failed." },
+        )
+    }
+
     fun logViewHidden(info: T) {
         buffer.log(
             tag,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
new file mode 100644
index 0000000..01a81de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.children
+import javax.inject.Inject
+
+/**
+ * A class controlling chipbar animations. Typically delegates to [ViewHierarchyAnimator].
+ *
+ * Used so that animations can be mocked out in tests.
+ */
+@SysUISingleton
+open class ChipbarAnimator @Inject constructor() {
+    /**
+     * Animates [innerView] and its children into view.
+     *
+     * @return true if the animation was successfully started and false if the animation can't be
+     * run for any reason.
+     *
+     * See [ViewHierarchyAnimator.animateAddition].
+     */
+    open fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+        return ViewHierarchyAnimator.animateAddition(
+            innerView,
+            ViewHierarchyAnimator.Hotspot.TOP,
+            Interpolators.EMPHASIZED_DECELERATE,
+            duration = ANIMATION_IN_DURATION,
+            includeMargins = true,
+            includeFadeIn = true,
+            onAnimationEnd = onAnimationEnd,
+        )
+    }
+
+    /**
+     * Animates [innerView] and its children out of view.
+     *
+     * @return true if the animation was successfully started and false if the animation can't be
+     * run for any reason.
+     *
+     * See [ViewHierarchyAnimator.animateRemoval].
+     */
+    open fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+        return ViewHierarchyAnimator.animateRemoval(
+            innerView,
+            ViewHierarchyAnimator.Hotspot.TOP,
+            Interpolators.EMPHASIZED_ACCELERATE,
+            ANIMATION_OUT_DURATION,
+            includeMargins = true,
+            onAnimationEnd,
+        )
+    }
+
+    /** Force shows this view and all child views. Should be used in case [animateViewIn] fails. */
+    fun forceDisplayView(innerView: View) {
+        innerView.alpha = 1f
+        if (innerView is ViewGroup) {
+            innerView.children.forEach { forceDisplayView(it) }
+        }
+    }
+}
+
+private const val ANIMATION_IN_DURATION = 500L
+private const val ANIMATION_OUT_DURATION = 250L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 04b1a50..a20a5b2 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.temporarydisplay.chipbar
 
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Rect
 import android.os.PowerManager
@@ -27,13 +29,14 @@
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
 import android.widget.TextView
 import androidx.annotation.IdRes
+import androidx.annotation.VisibleForTesting
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
-import com.android.systemui.animation.ViewHierarchyAnimator
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -78,8 +81,10 @@
     configurationController: ConfigurationController,
     dumpManager: DumpManager,
     powerManager: PowerManager,
+    private val chipbarAnimator: ChipbarAnimator,
     private val falsingManager: FalsingManager,
     private val falsingCollector: FalsingCollector,
+    private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
     private val viewUtil: ViewUtil,
     private val vibratorHelper: VibratorHelper,
     wakeLockBuilder: WakeLock.Builder,
@@ -101,10 +106,21 @@
 
     private lateinit var parent: ChipbarRootView
 
+    /** The current loading information, or null we're not currently loading. */
+    @VisibleForTesting
+    internal var loadingDetails: LoadingDetails? = null
+        private set(value) {
+            // Always cancel the old one before updating
+            field?.animator?.cancel()
+            field = value
+        }
+
     override val windowLayoutParams =
         commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) }
 
     override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) {
+        updateGestureListening()
+
         logger.logViewUpdate(
             newInfo.windowTitle,
             newInfo.text.loadText(context),
@@ -141,8 +157,22 @@
 
         // ---- End item ----
         // Loading
-        currentView.requireViewById<View>(R.id.loading).visibility =
-            (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue()
+        val isLoading = newInfo.endItem == ChipbarEndItem.Loading
+        val loadingView = currentView.requireViewById<ImageView>(R.id.loading)
+        loadingView.visibility = isLoading.visibleIfTrue()
+
+        if (isLoading) {
+            val currentLoadingDetails = loadingDetails
+            // Since there can be multiple chipbars, we need to check if the loading view is the
+            // same and possibly re-start the loading animation on the new view.
+            if (currentLoadingDetails == null || currentLoadingDetails.loadingView != loadingView) {
+                val newDetails = createLoadingDetails(loadingView)
+                newDetails.animator.start()
+                loadingDetails = newDetails
+            }
+        } else {
+            loadingDetails = null
+        }
 
         // Error
         currentView.requireViewById<View>(R.id.error).visibility =
@@ -203,35 +233,76 @@
     }
 
     override fun animateViewIn(view: ViewGroup) {
-        ViewHierarchyAnimator.animateAddition(
-            view.getInnerView(),
-            ViewHierarchyAnimator.Hotspot.TOP,
-            Interpolators.EMPHASIZED_DECELERATE,
-            duration = ANIMATION_IN_DURATION,
-            includeMargins = true,
-            includeFadeIn = true,
-            // We can only request focus once the animation finishes.
-            onAnimationEnd = {
-                maybeGetAccessibilityFocus(view.getTag(INFO_TAG) as ChipbarInfo?, view)
-            },
-        )
+        // We can only request focus once the animation finishes.
+        val onAnimationEnd = Runnable {
+            maybeGetAccessibilityFocus(view.getTag(INFO_TAG) as ChipbarInfo?, view)
+        }
+        val animatedIn = chipbarAnimator.animateViewIn(view.getInnerView(), onAnimationEnd)
+
+        // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run and the
+        // views would remain un-displayed. So, just force-set/run those items immediately.
+        if (!animatedIn) {
+            logger.logAnimateInFailure()
+            chipbarAnimator.forceDisplayView(view.getInnerView())
+            onAnimationEnd.run()
+        }
     }
 
     override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         val innerView = view.getInnerView()
         innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
-        ViewHierarchyAnimator.animateRemoval(
-            innerView,
-            ViewHierarchyAnimator.Hotspot.TOP,
-            Interpolators.EMPHASIZED_ACCELERATE,
-            ANIMATION_OUT_DURATION,
-            includeMargins = true,
-            onAnimationEnd,
-        )
+
+        val fullEndRunnable = Runnable {
+            loadingDetails = null
+            onAnimationEnd.run()
+        }
+        val removed = chipbarAnimator.animateViewOut(innerView, fullEndRunnable)
+        // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
+        // run it immediately.
+        if (!removed) {
+            logger.logAnimateOutFailure()
+            fullEndRunnable.run()
+        }
+
+        updateGestureListening()
+    }
+
+    private fun updateGestureListening() {
+        if (swipeChipbarAwayGestureHandler == null) {
+            return
+        }
+
+        val currentDisplayInfo = getCurrentDisplayInfo()
+        if (currentDisplayInfo != null && currentDisplayInfo.info.allowSwipeToDismiss) {
+            swipeChipbarAwayGestureHandler.setViewFetcher { currentDisplayInfo.view }
+            swipeChipbarAwayGestureHandler.addOnGestureDetectedCallback(TAG) {
+                onSwipeUpGestureDetected()
+            }
+        } else {
+            swipeChipbarAwayGestureHandler.resetViewFetcher()
+            swipeChipbarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
+        }
+    }
+
+    private fun onSwipeUpGestureDetected() {
+        val currentDisplayInfo = getCurrentDisplayInfo()
+        if (currentDisplayInfo == null) {
+            logger.logSwipeGestureError(id = null, errorMsg = "No info is being displayed")
+            return
+        }
+        if (!currentDisplayInfo.info.allowSwipeToDismiss) {
+            logger.logSwipeGestureError(
+                id = currentDisplayInfo.info.id,
+                errorMsg = "This view prohibits swipe-to-dismiss",
+            )
+            return
+        }
+        removeView(currentDisplayInfo.info.id, SWIPE_UP_GESTURE_REASON)
+        updateGestureListening()
     }
 
     private fun ViewGroup.getInnerView(): ViewGroup {
-        return requireViewById(R.id.chipbar_inner)
+        return this.requireViewById(R.id.chipbar_inner)
     }
 
     override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -245,8 +316,28 @@
             View.GONE
         }
     }
+
+    private fun createLoadingDetails(loadingView: View): LoadingDetails {
+        // Ideally, we would use a <ProgressBar> view, which would automatically handle the loading
+        // spinner rotation for us. However, due to b/243983980, the ProgressBar animation
+        // unexpectedly pauses when SysUI starts another window. ObjectAnimator is a workaround that
+        // won't pause.
+        val animator =
+            ObjectAnimator.ofFloat(loadingView, View.ROTATION, 0f, 360f).apply {
+                duration = LOADING_ANIMATION_DURATION_MS
+                repeatCount = ValueAnimator.INFINITE
+                interpolator = Interpolators.LINEAR
+            }
+        return LoadingDetails(loadingView, animator)
+    }
+
+    internal data class LoadingDetails(
+        val loadingView: View,
+        val animator: ObjectAnimator,
+    )
 }
 
-private const val ANIMATION_IN_DURATION = 500L
-private const val ANIMATION_OUT_DURATION = 250L
 @IdRes private val INFO_TAG = R.id.tag_chipbar_info
+private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
+private const val TAG = "ChipbarCoordinator"
+private const val LOADING_ANIMATION_DURATION_MS = 1000L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index dd4bd26..fe46318 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -33,12 +33,14 @@
  * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR
  * locales; on the left in RTL locales).
  * @property vibrationEffect an optional vibration effect when the chipbar is displayed
+ * @property allowSwipeToDismiss true if users are allowed to swipe up to dismiss this chipbar.
  */
 data class ChipbarInfo(
     val startIcon: TintedIcon,
     val text: Text,
     val endItem: ChipbarEndItem?,
     val vibrationEffect: VibrationEffect? = null,
+    val allowSwipeToDismiss: Boolean = false,
     override val windowTitle: String,
     override val wakeReason: String,
     override val timeoutMs: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
index fcfbe0a..f239428 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
@@ -46,4 +46,16 @@
             { "Chipbar updated. window=$str1 text=$str2 endItem=$str3" }
         )
     }
+
+    fun logSwipeGestureError(id: String?, errorMsg: String) {
+        buffer.log(
+            tag,
+            LogLevel.WARNING,
+            {
+                str1 = id
+                str2 = errorMsg
+            },
+            { "Chipbar swipe gesture detected for incorrect state. id=$str1 error=$str2" }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
new file mode 100644
index 0000000..9dbc4b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.content.Context
+import android.view.MotionEvent
+import android.view.View
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.statusbar.gesture.SwipeUpGestureHandler
+import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
+import com.android.systemui.util.boundsOnScreen
+
+/**
+ * A class to detect when a user has swiped the chipbar away.
+ *
+ * Effectively [SysUISingleton]. But, this shouldn't be created if the gesture isn't enabled. See
+ * [TemporaryDisplayModule.provideSwipeChipbarAwayGestureHandler].
+ */
+class SwipeChipbarAwayGestureHandler(
+    context: Context,
+    displayTracker: DisplayTracker,
+    logger: SwipeUpGestureLogger,
+) : SwipeUpGestureHandler(context, displayTracker, logger, loggerTag = LOGGER_TAG) {
+
+    private var viewFetcher: () -> View? = { null }
+
+    override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
+        val view = viewFetcher.invoke() ?: return false
+        // Since chipbar is in its own window, we need to use [boundsOnScreen] to get an accurate
+        // bottom. ([view.bottom] would be relative to its window, which would be too small.)
+        val viewBottom = view.boundsOnScreen.bottom
+        // Allow the gesture to start a bit below the chipbar
+        return ev.y <= 1.5 * viewBottom
+    }
+
+    /**
+     * Sets a fetcher that returns the current chipbar view. The fetcher will be invoked whenever a
+     * gesture starts to determine if the gesture is near the chipbar.
+     */
+    fun setViewFetcher(fetcher: () -> View?) {
+        viewFetcher = fetcher
+    }
+
+    /** Removes the current view fetcher. */
+    fun resetViewFetcher() {
+        viewFetcher = { null }
+    }
+}
+
+private const val LOGGER_TAG = "SwipeChipbarAway"
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
index cf0a183..b1be404 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
@@ -16,22 +16,40 @@
 
 package com.android.systemui.temporarydisplay.dagger
 
+import android.content.Context
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
+import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
 import dagger.Module
 import dagger.Provides
 
 @Module
 interface TemporaryDisplayModule {
-    @Module
     companion object {
-        @JvmStatic
         @Provides
         @SysUISingleton
         @ChipbarLog
         fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer {
             return factory.create("ChipbarLog", 40)
         }
+
+        @Provides
+        @SysUISingleton
+        fun provideSwipeChipbarAwayGestureHandler(
+            mediaTttFlags: MediaTttFlags,
+            context: Context,
+            displayTracker: DisplayTracker,
+            logger: SwipeUpGestureLogger,
+        ): SwipeChipbarAwayGestureHandler? {
+            return if (mediaTttFlags.isMediaTttDismissGestureEnabled()) {
+                SwipeChipbarAwayGestureHandler(context, displayTracker, logger)
+            } else {
+                null
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 3ecb15b..2b7ea2a 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -26,7 +26,6 @@
 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_SOURCE;
 import static com.android.systemui.theme.ThemeOverlayApplier.TIMESTAMP_FIELD;
 
-import android.annotation.Nullable;
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
 import android.app.WallpaperManager.OnColorsChangedListener;
@@ -70,6 +69,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.monet.Style;
+import com.android.systemui.monet.TonalPalette;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
@@ -357,15 +357,24 @@
     };
 
     @Inject
-    public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher,
-            @Background Handler bgHandler, @Main Executor mainExecutor,
-            @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier,
-            SecureSettings secureSettings, WallpaperManager wallpaperManager,
-            UserManager userManager, DeviceProvisionedController deviceProvisionedController,
-            UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
-            @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
+    public ThemeOverlayController(
+            Context context,
+            BroadcastDispatcher broadcastDispatcher,
+            @Background Handler bgHandler,
+            @Main Executor mainExecutor,
+            @Background Executor bgExecutor,
+            ThemeOverlayApplier themeOverlayApplier,
+            SecureSettings secureSettings,
+            WallpaperManager wallpaperManager,
+            UserManager userManager,
+            DeviceProvisionedController deviceProvisionedController,
+            UserTracker userTracker,
+            DumpManager dumpManager,
+            FeatureFlags featureFlags,
+            @Main Resources resources,
+            WakefulnessLifecycle wakefulnessLifecycle) {
         mContext = context;
-        mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES);
+        mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME);
         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
         mDeviceProvisionedController = deviceProvisionedController;
         mBroadcastDispatcher = broadcastDispatcher;
@@ -391,7 +400,7 @@
         mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, mMainExecutor,
                 UserHandle.ALL);
         mSecureSettings.registerContentObserverForUser(
-                Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES),
+                Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
                 false,
                 new ContentObserver(mBgHandler) {
                     @Override
@@ -513,39 +522,42 @@
     /**
      * Given a color candidate, return an overlay definition.
      */
-    protected @Nullable FabricatedOverlay getOverlay(int color, int type, Style style) {
+    protected FabricatedOverlay getOverlay(int color, int type, Style style) {
         boolean nightMode = (mResources.getConfiguration().uiMode
                 & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
 
         mColorScheme = new ColorScheme(color, nightMode, style);
-        List<Integer> colorShades = type == ACCENT
-                ? mColorScheme.getAllAccentColors() : mColorScheme.getAllNeutralColors();
         String name = type == ACCENT ? "accent" : "neutral";
-        int paletteSize = mColorScheme.getAccent1().size();
+
         FabricatedOverlay.Builder overlay =
                 new FabricatedOverlay.Builder("com.android.systemui", name, "android");
-        for (int i = 0; i < colorShades.size(); i++) {
-            int luminosity = i % paletteSize;
-            int paletteIndex = i / paletteSize + 1;
-            String resourceName;
-            switch (luminosity) {
-                case 0:
-                    resourceName = "android:color/system_" + name + paletteIndex + "_10";
-                    break;
-                case 1:
-                    resourceName = "android:color/system_" + name + paletteIndex + "_50";
-                    break;
-                default:
-                    int l = luminosity - 1;
-                    resourceName = "android:color/system_" + name + paletteIndex + "_" + l + "00";
-            }
-            overlay.setResourceValue(resourceName, TypedValue.TYPE_INT_COLOR_ARGB8,
-                    ColorUtils.setAlphaComponent(colorShades.get(i), 0xFF));
+
+        if (type == ACCENT) {
+            assignTonalPaletteToOverlay("accent1", overlay, mColorScheme.getAccent1());
+            assignTonalPaletteToOverlay("accent2", overlay, mColorScheme.getAccent2());
+            assignTonalPaletteToOverlay("accent3", overlay, mColorScheme.getAccent3());
+        } else {
+            assignTonalPaletteToOverlay("neutral1", overlay, mColorScheme.getNeutral1());
+            assignTonalPaletteToOverlay("neutral2", overlay, mColorScheme.getNeutral2());
         }
 
         return overlay.build();
     }
 
+    private void assignTonalPaletteToOverlay(String name,
+            FabricatedOverlay.Builder overlay, TonalPalette tonalPalette) {
+
+        String resourcePrefix = "android:color/system_" + name;
+        int colorDataType = TypedValue.TYPE_INT_COLOR_ARGB8;
+
+        tonalPalette.getAllShadesMapped().forEach((key, value) -> {
+            String resourceName = resourcePrefix + "_" + key;
+            int colorValue = ColorUtils.setAlphaComponent(value, 0xFF);
+            overlay.setResourceValue(resourceName, colorDataType,
+                    colorValue);
+        });
+    }
+
     /**
      * Checks if the color scheme in mColorScheme matches the current system palettes.
      * @param managedProfiles List of managed profiles for this user.
@@ -557,15 +569,15 @@
             Resources res = userHandle.isSystem()
                     ? mResources : mContext.createContextAsUser(userHandle, 0).getResources();
             if (!(res.getColor(android.R.color.system_accent1_500, mContext.getTheme())
-                    == mColorScheme.getAccent1().get(6)
+                    == mColorScheme.getAccent1().getS500()
                     && res.getColor(android.R.color.system_accent2_500, mContext.getTheme())
-                    == mColorScheme.getAccent2().get(6)
+                    == mColorScheme.getAccent2().getS500()
                     && res.getColor(android.R.color.system_accent3_500, mContext.getTheme())
-                    == mColorScheme.getAccent3().get(6)
+                    == mColorScheme.getAccent3().getS500()
                     && res.getColor(android.R.color.system_neutral1_500, mContext.getTheme())
-                    == mColorScheme.getNeutral1().get(6)
+                    == mColorScheme.getNeutral1().getS500()
                     && res.getColor(android.R.color.system_neutral2_500, mContext.getTheme())
-                    == mColorScheme.getNeutral2().get(6))) {
+                    == mColorScheme.getNeutral2().getS500())) {
                 return false;
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
index 79811c5..2475890 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
@@ -28,8 +28,9 @@
 import androidx.preference.PreferenceScreen;
 
 import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
 
 import java.util.Objects;
 
@@ -74,7 +75,7 @@
 
         RadioFragment f = new RadioFragment();
         f.setPreference(this);
-        FragmentHostManager.get(v).getFragmentManager()
+        Dependency.get(FragmentService.class).getFragmentHostManager(v).getFragmentManager()
                 .beginTransaction()
                 .add(android.R.id.content, f)
                 .commit();
@@ -86,8 +87,10 @@
             Bundle savedInstanceState) {
         super.onDialogStateRestored(fragment, dialog, savedInstanceState);
         View view = dialog.findViewById(R.id.content);
-        RadioFragment radioFragment = (RadioFragment) FragmentHostManager.get(view)
-                .getFragmentManager().findFragmentById(R.id.content);
+        RadioFragment radioFragment = (RadioFragment) Dependency.get(FragmentService.class)
+                .getFragmentHostManager(view)
+                .getFragmentManager()
+                .findFragmentById(R.id.content);
         if (radioFragment != null) {
             radioFragment.setPreference(this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index b23d870..8cfe2ea 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -42,7 +42,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoModeController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -68,7 +68,7 @@
     // Things that use the tunable infrastructure but are now real user settings and
     // shouldn't be reset with tuner settings.
     private static final String[] RESET_EXCEPTION_LIST = new String[] {
-            QSTileHost.TILES_SETTING,
+            QSHost.TILES_SETTING,
             Settings.Secure.DOZE_ALWAYS_ON,
             Settings.Secure.MEDIA_CONTROLS_RESUME,
             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 82200c6..360fc90 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.events.StatusBarEventsModule;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.phone.DozeServiceHost;
@@ -93,6 +94,7 @@
                 PowerModule.class,
                 QSModule.class,
                 ReferenceScreenshotModule.class,
+                StatusBarEventsModule.class,
                 VolumeModule.class,
         }
 )
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldStateLoggingProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldStateLoggingProviderImpl.kt
index 2683971..981f429 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldStateLoggingProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldStateLoggingProviderImpl.kt
@@ -61,8 +61,6 @@
         foldStateProvider.stop()
     }
 
-    override fun onHingeAngleUpdate(angle: Float) {}
-
     override fun onFoldUpdate(@FoldUpdate update: Int) {
         val now = clock.elapsedRealtime()
         when (update) {
@@ -77,6 +75,10 @@
         }
     }
 
+    override fun onUnfoldedScreenAvailable() {
+        Log.d(TAG, "Unfolded screen available")
+    }
+
     private fun dispatchState(@LoggedFoldedStates current: Int) {
         val now = clock.elapsedRealtime()
         val previous = lastState
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
index 7726d09..8214822 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
@@ -3,26 +3,43 @@
 import android.os.SystemProperties
 import android.os.VibrationEffect
 import android.os.Vibrator
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
-/**
- * Class that plays a haptics effect during unfolding a foldable device
- */
+/** Class that plays a haptics effect during unfolding a foldable device */
 @SysUIUnfoldScope
 class UnfoldHapticsPlayer
 @Inject
 constructor(
     unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+    foldProvider: FoldProvider,
+    @Main private val mainExecutor: Executor,
     private val vibrator: Vibrator?
 ) : TransitionProgressListener {
 
+    private var isFirstAnimationAfterUnfold = false
+
     init {
         if (vibrator != null) {
             // We don't need to remove the callback because we should listen to it
             // the whole time when SystemUI process is alive
             unfoldTransitionProgressProvider.addCallback(this)
         }
+
+        foldProvider.registerCallback(
+            object : FoldCallback {
+                override fun onFoldUpdated(isFolded: Boolean) {
+                    if (isFolded) {
+                        isFirstAnimationAfterUnfold = true
+                    }
+                }
+            },
+            mainExecutor
+        )
     }
 
     private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN
@@ -36,6 +53,13 @@
     }
 
     override fun onTransitionFinishing() {
+        // Run haptics only when unfolding the device (first animation after unfolding)
+        if (!isFirstAnimationAfterUnfold) {
+            return
+        }
+
+        isFirstAnimationAfterUnfold = false
+
         // Run haptics only if the animation is long enough to notice
         if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) {
             playHaptics()
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 523cf68..19a0866 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -36,6 +36,9 @@
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.LinearLightRevealEffect
@@ -57,6 +60,7 @@
 @Inject
 constructor(
     private val context: Context,
+    private val featureFlags: FeatureFlags,
     private val deviceStateManager: DeviceStateManager,
     private val contentResolver: ContentResolver,
     private val displayManager: DisplayManager,
@@ -65,6 +69,7 @@
     @Main private val executor: Executor,
     private val threadFactory: ThreadFactory,
     private val rotationChangeProvider: RotationChangeProvider,
+    private val displayTracker: DisplayTracker
 ) {
 
     private val transitionListener = TransitionListener()
@@ -81,6 +86,7 @@
     private var scrimView: LightRevealScrim? = null
     private var isFolded: Boolean = false
     private var isUnfoldHandled: Boolean = true
+    private var overlayAddReason: AddOverlayReason? = null
 
     private var currentRotation: Int = context.display!!.rotation
 
@@ -100,7 +106,7 @@
                 .setName("unfold-overlay-container")
 
         displayAreaHelper.get().attachToRootDisplayArea(
-            Display.DEFAULT_DISPLAY,
+            displayTracker.defaultDisplayId,
             containerBuilder
         ) { builder ->
             executor.execute {
@@ -158,6 +164,8 @@
         ensureInBackground()
         ensureOverlayRemoved()
 
+        overlayAddReason = reason
+
         val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
         val params = getLayoutParams()
         val newView =
@@ -170,11 +178,7 @@
                 .apply {
                     revealEffect = createLightRevealEffect()
                     isScrimOpaqueChangedListener = Consumer {}
-                    revealAmount =
-                        when (reason) {
-                            FOLD -> TRANSPARENT
-                            UNFOLD -> BLACK
-                        }
+                    revealAmount = calculateRevealAmount()
                 }
 
         newRoot.setView(newView, params)
@@ -207,6 +211,31 @@
         root = newRoot
     }
 
+    private fun calculateRevealAmount(animationProgress: Float? = null): Float {
+        val overlayAddReason = overlayAddReason ?: UNFOLD
+
+        if (animationProgress == null) {
+            // Animation progress is unknown, calculate the initial value based on the overlay
+            // add reason
+            return when (overlayAddReason) {
+                FOLD -> TRANSPARENT
+                UNFOLD -> BLACK
+            }
+        }
+
+        val showVignetteWhenFolding =
+            featureFlags.isEnabled(Flags.ENABLE_DARK_VIGNETTE_WHEN_FOLDING)
+
+        return if (!showVignetteWhenFolding && overlayAddReason == FOLD) {
+            // Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off
+            // and we are folding the device. We still add the overlay to block touches
+            // while the animation is running but the overlay is transparent.
+            TRANSPARENT
+        } else {
+            animationProgress
+        }
+    }
+
     private fun getLayoutParams(): WindowManager.LayoutParams {
         val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
 
@@ -259,7 +288,7 @@
     private inner class TransitionListener : TransitionProgressListener {
 
         override fun onTransitionProgress(progress: Float) {
-            executeInBackground { scrimView?.revealAmount = progress }
+            executeInBackground { scrimView?.revealAmount = calculateRevealAmount(progress) }
         }
 
         override fun onTransitionFinished() {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index 59ad24a..2709da3 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -17,6 +17,9 @@
 package com.android.systemui.unfold
 
 import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.LifecycleScreenStatusProvider
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
 import com.android.systemui.unfold.system.SystemUnfoldSharedModule
@@ -32,6 +35,7 @@
 import dagger.Module
 import dagger.Provides
 import java.util.Optional
+import java.util.concurrent.Executor
 import javax.inject.Named
 import javax.inject.Singleton
 
@@ -40,6 +44,20 @@
 
     @Provides @UnfoldTransitionATracePrefix fun tracingTagPrefix() = "systemui"
 
+    /** A globally available FoldStateListener that allows one to query the fold state. */
+    @Provides
+    @Singleton
+    fun providesFoldStateListener(
+        deviceStateManager: DeviceStateManager,
+        @Application context: Context,
+        @Main executor: Executor
+    ): DeviceStateManager.FoldStateListener {
+        val listener = DeviceStateManager.FoldStateListener(context)
+        deviceStateManager.registerCallback(executor, listener)
+
+        return listener
+    }
+
     @Provides
     @Singleton
     fun providesFoldStateLoggingProvider(
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index 2b29885..f7c8bac 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -21,6 +21,7 @@
 
 import com.android.settingslib.users.EditUserInfoController;
 import com.android.systemui.user.data.repository.UserRepositoryModule;
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule;
 import com.android.systemui.user.ui.dialog.UserDialogModule;
 
 import dagger.Binds;
@@ -36,6 +37,7 @@
         includes = {
                 UserDialogModule.class,
                 UserRepositoryModule.class,
+                HeadlessSystemUserModeModule.class,
         }
 )
 public abstract class UserModule {
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 c0f0390..b2b7c0b 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
@@ -30,6 +30,8 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.util.settings.GlobalSettings
@@ -42,12 +44,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
@@ -68,6 +72,9 @@
     /** [UserInfo] of the currently-selected user. */
     val selectedUserInfo: Flow<UserInfo>
 
+    /** Whether user switching is currently in progress. */
+    val userSwitchingInProgress: Flow<Boolean>
+
     /** User ID of the last non-guest selected user. */
     val lastSelectedNonGuestUserId: Int
 
@@ -108,11 +115,28 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val globalSettings: GlobalSettings,
     private val tracker: UserTracker,
+    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()
@@ -129,6 +153,10 @@
     private var _isGuestUserResetting: Boolean = false
     override var isGuestUserResetting: Boolean = _isGuestUserResetting
 
+    private val _isUserSwitchingInProgress = MutableStateFlow(false)
+    override val userSwitchingInProgress: Flow<Boolean>
+        get() = _isUserSwitchingInProgress
+
     override val isGuestUserCreationScheduled = AtomicBoolean()
 
     override val isStatusBarUserChipEnabled: Boolean =
@@ -140,7 +168,9 @@
 
     init {
         observeSelectedUser()
-        observeUserSettings()
+        if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) {
+            observeUserSwitching()
+        }
     }
 
     override fun refreshUsers() {
@@ -166,6 +196,28 @@
         return _userSwitcherSettings.value.isSimpleUserSwitcher
     }
 
+    private fun observeUserSwitching() {
+        conflatedCallbackFlow {
+                val callback =
+                    object : UserTracker.Callback {
+                        override fun onUserChanging(newUser: Int, userContext: Context) {
+                            trySendWithFailureLogging(true, TAG, "userSwitching started")
+                        }
+
+                        override fun onUserChanged(newUserId: Int, userContext: Context) {
+                            trySendWithFailureLogging(false, TAG, "userSwitching completed")
+                        }
+                    }
+                tracker.addCallback(callback, mainDispatcher.asExecutor())
+                trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
+                awaitClose { tracker.removeCallback(callback) }
+            }
+            .onEach { _isUserSwitchingInProgress.value = it }
+            // TODO (b/262838215), Make this stateIn and initialize directly in field declaration
+            //  once the flag is launched
+            .launchIn(applicationScope)
+    }
+
     private fun observeSelectedUser() {
         conflatedCallbackFlow {
                 fun send() {
@@ -174,7 +226,7 @@
 
                 val callback =
                     object : UserTracker.Callback {
-                        override fun onUserChanged(newUser: Int, userContext: Context) {
+                        override fun onUserChanging(newUser: Int, userContext: Context) {
                             send()
                         }
 
@@ -198,23 +250,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/user/domain/interactor/HeadlessSystemUserMode.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/HeadlessSystemUserMode.kt
new file mode 100644
index 0000000..756e6a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/HeadlessSystemUserMode.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.user.domain.interactor
+
+import android.os.UserManager
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+interface HeadlessSystemUserMode {
+
+    fun isHeadlessSystemUserMode(): Boolean
+}
+
+@SysUISingleton
+class HeadlessSystemUserModeImpl @Inject constructor() : HeadlessSystemUserMode {
+    override fun isHeadlessSystemUserMode(): Boolean {
+        return UserManager.isHeadlessSystemUserMode()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/HeadlessSystemUserModeModule.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/user/domain/interactor/HeadlessSystemUserModeModule.kt
index 67733e9..0efa2d8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/HeadlessSystemUserModeModule.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,16 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.user.domain.interactor
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+import dagger.Binds
+
+@dagger.Module
+interface HeadlessSystemUserModeModule {
+
+    @Binds
+    fun bindIsHeadlessSystemUserMode(impl: HeadlessSystemUserModeImpl): HeadlessSystemUserMode
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index d7b0971..3f895ad 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -86,6 +86,7 @@
     private val keyguardInteractor: KeyguardInteractor,
     private val featureFlags: FeatureFlags,
     private val manager: UserManager,
+    private val headlessSystemUserMode: HeadlessSystemUserMode,
     @Application private val applicationScope: CoroutineScope,
     telephonyInteractor: TelephonyInteractor,
     broadcastDispatcher: BroadcastDispatcher,
@@ -115,9 +116,7 @@
     private val callbackMutex = Mutex()
     private val callbacks = mutableSetOf<UserCallback>()
     private val userInfos: Flow<List<UserInfo>> =
-        repository.userInfos.map { userInfos ->
-            userInfos.filter { it.isFull }
-        }
+        repository.userInfos.map { userInfos -> userInfos.filter { it.isFull } }
 
     /** List of current on-device users to select from. */
     val users: Flow<List<UserModel>>
@@ -445,7 +444,8 @@
                     )
                 )
             }
-            UserActionModel.ADD_SUPERVISED_USER ->
+            UserActionModel.ADD_SUPERVISED_USER -> {
+                dismissDialog()
                 activityStarter.startActivity(
                     Intent()
                         .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
@@ -453,6 +453,7 @@
                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
                     /* dismissShade= */ true,
                 )
+            }
             UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
                 activityStarter.startActivity(
                     Intent(Settings.ACTION_USER_SETTINGS),
@@ -493,7 +494,7 @@
 
     fun showUserSwitcher(context: Context, expandable: Expandable) {
         if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
-            showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog)
+            showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable))
             return
         }
 
@@ -560,7 +561,10 @@
             actionType = action,
             isRestricted = isRestricted,
             isSwitchToEnabled =
-                canSwitchUsers(selectedUserId) &&
+                canSwitchUsers(
+                    selectedUserId = selectedUserId,
+                    isAction = true,
+                ) &&
                     // If the user is auto-created is must not be currently resetting.
                     !(isGuestUserAutoCreated && isGuestUserResetting),
         )
@@ -712,10 +716,32 @@
         }
     }
 
-    private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
-        return withContext(backgroundDispatcher) {
-            manager.getUserSwitchability(UserHandle.of(selectedUserId))
-        } == UserManager.SWITCHABILITY_STATUS_OK
+    private suspend fun canSwitchUsers(
+        selectedUserId: Int,
+        isAction: Boolean = false,
+    ): Boolean {
+        val isHeadlessSystemUserMode =
+            withContext(backgroundDispatcher) { headlessSystemUserMode.isHeadlessSystemUserMode() }
+        // Whether menu item should be active. True if item is a user or if any user has
+        // signed in since reboot or in all cases for non-headless system user mode.
+        val isItemEnabled = !isAction || !isHeadlessSystemUserMode || isAnyUserUnlocked()
+        return isItemEnabled &&
+            withContext(backgroundDispatcher) {
+                manager.getUserSwitchability(UserHandle.of(selectedUserId))
+            } == UserManager.SWITCHABILITY_STATUS_OK
+    }
+
+    private suspend fun isAnyUserUnlocked(): Boolean {
+        return manager
+            .getUsers(
+                /* excludePartial= */ true,
+                /* excludeDying= */ true,
+                /* excludePreCreated= */ true
+            )
+            .any { user ->
+                user.id != UserHandle.USER_SYSTEM &&
+                    withContext(backgroundDispatcher) { manager.isUserUnlocked(user.userHandle) }
+            }
     }
 
     @SuppressLint("UseCompatLoadingForDrawables")
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
index 85c2964..14cc3e7 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -18,11 +18,13 @@
 package com.android.systemui.user.domain.model
 
 import android.os.UserHandle
+import com.android.systemui.animation.Expandable
 import com.android.systemui.qs.user.UserSwitchDialogController
 
 /** Encapsulates a request to show a dialog. */
 sealed class ShowDialogRequestModel(
     open val dialogShower: UserSwitchDialogController.DialogShower? = null,
+    open val expandable: Expandable? = null,
 ) {
     data class ShowAddUserDialog(
         val userHandle: UserHandle,
@@ -45,5 +47,7 @@
     ) : ShowDialogRequestModel(dialogShower)
 
     /** Show the user switcher dialog */
-    object ShowUserSwitcherDialog : ShowDialogRequestModel()
+    data class ShowUserSwitcherDialog(
+        override val expandable: Expandable?,
+    ) : ShowDialogRequestModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
index a9d66de..5a25a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
@@ -63,11 +63,10 @@
                 }
 
                 // Use broadcast instead of ShadeController, as this dialog may have started in
-                // another
-                // process where normal dagger bindings are not available.
+                // another process where normal dagger bindings are not available.
                 broadcastSender.sendBroadcastAsUser(
                     Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
-                    UserHandle.CURRENT
+                    userHandle
                 )
 
                 context.startActivityAsUser(
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt
new file mode 100644
index 0000000..3fe2a7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.ui.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
+
+/** Extracted from [UserSwitchDialogController] */
+class DialogShowerImpl(
+    private val animateFrom: Dialog,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+) : DialogInterface by animateFrom, DialogShower {
+    override fun showDialog(dialog: Dialog, cuj: DialogCuj) {
+        dialogLaunchAnimator.showFromDialog(dialog, animateFrom = animateFrom, cuj)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
index ed25898..b8ae257 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
@@ -60,6 +60,7 @@
         setView(gridFrame)
 
         adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
+        adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator))
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 4141054..79721b3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -66,12 +66,6 @@
     private fun startHandlingDialogShowRequests() {
         applicationScope.get().launch {
             interactor.get().dialogShowRequests.filterNotNull().collect { request ->
-                currentDialog?.let {
-                    if (it.isShowing) {
-                        it.cancel()
-                    }
-                }
-
                 val (dialog, dialogCuj) =
                     when (request) {
                         is ShowDialogRequestModel.ShowAddUserDialog ->
@@ -133,7 +127,10 @@
                     }
                 currentDialog = dialog
 
-                if (request.dialogShower != null && dialogCuj != null) {
+                val controller = request.expandable?.dialogLaunchController(dialogCuj)
+                if (controller != null) {
+                    dialogLaunchAnimator.get().show(dialog, controller)
+                } else if (request.dialogShower != null && dialogCuj != null) {
                     request.dialogShower?.showDialog(dialog, dialogCuj)
                 } else {
                     dialog.show()
diff --git a/packages/SystemUI/src/com/android/systemui/util/BackupManagerProxy.kt b/packages/SystemUI/src/com/android/systemui/util/BackupManagerProxy.kt
new file mode 100644
index 0000000..f542434
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/BackupManagerProxy.kt
@@ -0,0 +1,17 @@
+package com.android.systemui.util
+
+import android.app.backup.BackupManager
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/** Wrapper around [BackupManager] useful for testing. */
+@SysUISingleton
+class BackupManagerProxy @Inject constructor() {
+
+    /** Wrapped version of [BackupManager.dataChanged] */
+    fun dataChanged(packageName: String) = BackupManager.dataChanged(packageName)
+
+    /** Wrapped version of [BackupManager.dataChangedForUser] */
+    fun dataChangedForUser(userId: Int, packageName: String) =
+        BackupManager.dataChangedForUser(userId, packageName)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java
index d54de3fa..c2727fc 100644
--- a/packages/SystemUI/src/com/android/systemui/util/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java
@@ -14,8 +14,6 @@
 
 package com.android.systemui.util;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import android.Manifest;
 import android.content.Context;
 import android.content.Intent;
@@ -26,6 +24,7 @@
 
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.R;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.system.QuickStepContract;
 
 import java.util.List;
@@ -71,8 +70,9 @@
      * {@link android.view.WindowManagerPolicyConstants#NAV_BAR_MODE_GESTURAL} AND
      * the context is that of the default display
      */
-    public static boolean isGesturalModeOnDefaultDisplay(Context context, int navMode) {
-        return context.getDisplayId() == DEFAULT_DISPLAY
+    public static boolean isGesturalModeOnDefaultDisplay(Context context,
+            DisplayTracker displayTracker, int navMode) {
+        return context.getDisplayId() == displayTracker.getDefaultDisplayId()
                 && QuickStepContract.isGesturalMode(navMode);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
index 08ee0af..56c5d3b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
@@ -27,6 +27,8 @@
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.animation.LaunchableView
+import com.android.systemui.animation.LaunchableViewDelegate
 import com.android.systemui.statusbar.CrossFadeHelper
 
 /**
@@ -38,7 +40,7 @@
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0
-) : ConstraintLayout(context, attrs, defStyleAttr) {
+) : ConstraintLayout(context, attrs, defStyleAttr), LaunchableView {
 
     private val boundsRect = Rect()
     private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
@@ -50,7 +52,11 @@
 
     private var desiredMeasureWidth = 0
     private var desiredMeasureHeight = 0
-    private var transitionVisibility = View.VISIBLE
+    private val delegate =
+        LaunchableViewDelegate(
+            this,
+            superSetVisibility = { super.setVisibility(it) },
+        )
 
     /**
      * The measured state of this view which is the one we will lay ourselves out with. This
@@ -83,11 +89,12 @@
         }
     }
 
-    override fun setTransitionVisibility(visibility: Int) {
-        // We store the last transition visibility assigned to this view to restore it later if
-        // necessary.
-        super.setTransitionVisibility(visibility)
-        transitionVisibility = visibility
+    override fun setShouldBlockVisibilityChanges(block: Boolean) {
+        delegate.setShouldBlockVisibilityChanges(block)
+    }
+
+    override fun setVisibility(visibility: Int) {
+        delegate.setVisibility(visibility)
     }
 
     override fun onFinishInflate() {
@@ -173,14 +180,6 @@
         translationY = currentState.translation.y
 
         CrossFadeHelper.fadeIn(this, currentState.alpha)
-
-        // CrossFadeHelper#fadeIn will change this view visibility, which overrides the transition
-        // visibility. We set the transition visibility again to make sure that this view plays well
-        // with GhostView, which sets the transition visibility and is used for activity launch
-        // animations.
-        if (transitionVisibility != View.VISIBLE) {
-            setTransitionVisibility(transitionVisibility)
-        }
     }
 
     private fun applyCurrentStateOnPredraw() {
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..8d32a48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java
@@ -0,0 +1,74 @@
+/*
+ * 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() {
+        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() {
+        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/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index b61b2e6..47d505e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -16,14 +16,21 @@
 
 package com.android.systemui.util.kotlin
 
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.SystemClockImpl
+import kotlinx.coroutines.CoroutineStart
 import java.util.concurrent.atomic.AtomicReference
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
+import kotlin.math.max
 
 /**
  * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
@@ -167,3 +174,89 @@
  * Note that the returned Flow will not emit anything until [other] has emitted at least one value.
  */
 fun <A> Flow<*>.sample(other: Flow<A>): Flow<A> = sample(other) { _, a -> a }
+
+/**
+ * Returns a flow that mirrors the original flow, but delays values following emitted values for the
+ * given [periodMs]. If the original flow emits more than one value during this period, only the
+ * latest value is emitted.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * flow {
+ *     emit(1)     // t=0ms
+ *     delay(90)
+ *     emit(2)     // t=90ms
+ *     delay(90)
+ *     emit(3)     // t=180ms
+ *     delay(1010)
+ *     emit(4)     // t=1190ms
+ *     delay(1010)
+ *     emit(5)     // t=2200ms
+ * }.throttle(1000)
+ * ```
+ *
+ * produces the following emissions at the following times
+ *
+ * ```text
+ * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
+ * ```
+ */
+fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, SystemClockImpl())
+
+/**
+ * Returns a flow that mirrors the original flow, but delays values following emitted values for the
+ * given [periodMs] as reported by the given [clock]. If the original flow emits more than one value
+ * during this period, only The latest value is emitted.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * flow {
+ *     emit(1)     // t=0ms
+ *     delay(90)
+ *     emit(2)     // t=90ms
+ *     delay(90)
+ *     emit(3)     // t=180ms
+ *     delay(1010)
+ *     emit(4)     // t=1190ms
+ *     delay(1010)
+ *     emit(5)     // t=2200ms
+ * }.throttle(1000)
+ * ```
+ *
+ * produces the following emissions at the following times
+ *
+ * ```text
+ * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
+ * ```
+ */
+fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock): Flow<T> = channelFlow {
+    coroutineScope {
+        var previousEmitTimeMs = 0L
+        var delayJob: Job? = null
+        var sendJob: Job? = null
+        val outerScope = this
+
+        collect {
+            delayJob?.cancel()
+            sendJob?.join()
+            val currentTimeMs = clock.elapsedRealtime()
+            val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
+            val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
+            if (timeUntilNextEmit > 0L) {
+                // We create delayJob to allow cancellation during the delay period
+                delayJob = launch {
+                    delay(timeUntilNextEmit)
+                    sendJob = outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+                        send(it)
+                        previousEmitTimeMs = clock.elapsedRealtime()
+                    }
+                }
+            } else {
+                send(it)
+                previousEmitTimeMs = currentTimeMs
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
new file mode 100644
index 0000000..c74e71f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.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.util.leak
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface GarbageMonitorModule {
+    /** Inject into GarbageMonitor.Service. */
+    @Binds
+    @IntoMap
+    @ClassKey(GarbageMonitor::class)
+    fun bindGarbageMonitorService(sysui: GarbageMonitor.Service): CoreStartable
+
+    @Binds
+    @IntoMap
+    @StringKey(GarbageMonitor.MemoryTile.TILE_SPEC)
+    fun bindMemoryTile(memoryTile: GarbageMonitor.MemoryTile): QSTileImpl<*>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
index 4351afe..a0d22f3 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
@@ -29,12 +29,12 @@
 import android.net.Uri;
 import android.os.Debug;
 import android.os.SystemProperties;
-import android.os.UserHandle;
 import android.util.Log;
 
 import androidx.core.content.FileProvider;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
 
 import com.google.android.collect.Lists;
 
@@ -62,13 +62,15 @@
     static final String LEAK_DUMP = "leak.dump";
 
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final LeakDetector mLeakDetector;
     private final String mLeakReportEmail;
 
     @Inject
-    public LeakReporter(Context context, LeakDetector leakDetector,
+    public LeakReporter(Context context, UserTracker userTracker, LeakDetector leakDetector,
             @Nullable @Named(LEAK_REPORT_EMAIL_NAME) String leakReportEmail) {
         mContext = context;
+        mUserTracker = userTracker;
         mLeakDetector = leakDetector;
         mLeakReportEmail = leakReportEmail;
     }
@@ -111,7 +113,7 @@
                             getIntent(hprofFile, dumpFile),
                             PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE,
                             null,
-                            UserHandle.CURRENT));
+                            mUserTracker.getUserHandle()));
             notiMan.notify(TAG, 0, builder.build());
         } catch (IOException e) {
             Log.e(TAG, "Couldn't dump heap for leak", e);
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java b/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java
index 0b2f004..31f35fc 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java
@@ -1,15 +1,17 @@
 /*
  * Copyright (C) 2017 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * 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.
+ * 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.leak;
@@ -26,7 +28,27 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
-public class RotationUtils {
+/**
+ * Utility class that provides device orientation.
+ *
+ * <p>Consider using {@link Surface.Rotation} or add a function that respects device aspect ratio
+ * and {@code android.internal.R.bool.config_reverseDefaultRotation}.
+ *
+ * <p>If you only care about the rotation, use {@link Surface.Rotation}, as it always gives the
+ * counter clock-wise rotation. (e.g. If you have a device that has a charging port at the bottom,
+ * rotating three times in counter clock direction will give you {@link Surface#ROTATION_270} while
+ * having the charging port on the left side of the device.)
+ *
+ * <p>If you need whether the device is in portrait or landscape (or their opposites), please add a
+ * function here that respects the device aspect ratio and
+ * {@code android.internal.R.bool.config_reverseDefaultRotation} together.
+ *
+ * <p>Note that {@code android.internal.R.bool.config_reverseDefaultRotation} does not change the
+ * winding order. In other words, the rotation order (counter clock-wise) will remain the same. It
+ * only flips the device orientation, such that portrait becomes upside down, landscape becomes
+ * seascape.
+ */
+public final class RotationUtils {
 
     public static final int ROTATION_NONE = 0;
     public static final int ROTATION_LANDSCAPE = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
index 1a30b0a..85fada2 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
@@ -20,14 +20,18 @@
 import android.net.Uri;
 import android.provider.Settings;
 
+import com.android.systemui.settings.UserTracker;
+
 import javax.inject.Inject;
 
 class GlobalSettingsImpl implements GlobalSettings {
     private final ContentResolver mContentResolver;
+    private final UserTracker mUserTracker;
 
     @Inject
-    GlobalSettingsImpl(ContentResolver contentResolver) {
+    GlobalSettingsImpl(ContentResolver contentResolver, UserTracker userTracker) {
         mContentResolver = contentResolver;
+        mUserTracker = userTracker;
     }
 
     @Override
@@ -36,13 +40,19 @@
     }
 
     @Override
+    public UserTracker getUserTracker() {
+        return mUserTracker;
+    }
+
+    @Override
     public Uri getUriFor(String name) {
         return Settings.Global.getUriFor(name);
     }
 
     @Override
     public String getStringForUser(String name, int userHandle) {
-        return Settings.Global.getStringForUser(mContentResolver, name, userHandle);
+        return Settings.Global.getStringForUser(mContentResolver, name,
+                getRealUserHandle(userHandle));
     }
 
     @Override
@@ -53,14 +63,16 @@
 
     @Override
     public boolean putStringForUser(String name, String value, int userHandle) {
-        return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle);
+        return Settings.Global.putStringForUser(mContentResolver, name, value,
+                getRealUserHandle(userHandle));
     }
 
     @Override
     public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
             int userHandle, boolean overrideableByRestore) {
         return Settings.Global.putStringForUser(
-                mContentResolver, name, value, tag, makeDefault, userHandle, overrideableByRestore);
+                mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle),
+                overrideableByRestore);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
index 020c234..f995436 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
@@ -20,14 +20,18 @@
 import android.net.Uri;
 import android.provider.Settings;
 
+import com.android.systemui.settings.UserTracker;
+
 import javax.inject.Inject;
 
 class SecureSettingsImpl implements SecureSettings {
     private final ContentResolver mContentResolver;
+    private final UserTracker mUserTracker;
 
     @Inject
-    SecureSettingsImpl(ContentResolver contentResolver) {
+    SecureSettingsImpl(ContentResolver contentResolver, UserTracker userTracker) {
         mContentResolver = contentResolver;
+        mUserTracker = userTracker;
     }
 
     @Override
@@ -36,13 +40,19 @@
     }
 
     @Override
+    public UserTracker getUserTracker() {
+        return mUserTracker;
+    }
+
+    @Override
     public Uri getUriFor(String name) {
         return Settings.Secure.getUriFor(name);
     }
 
     @Override
     public String getStringForUser(String name, int userHandle) {
-        return Settings.Secure.getStringForUser(mContentResolver, name, userHandle);
+        return Settings.Secure.getStringForUser(mContentResolver, name,
+                getRealUserHandle(userHandle));
     }
 
     @Override
@@ -52,14 +62,16 @@
 
     @Override
     public boolean putStringForUser(String name, String value, int userHandle) {
-        return Settings.Secure.putStringForUser(mContentResolver, name, value, userHandle);
+        return Settings.Secure.putStringForUser(mContentResolver, name, value,
+                getRealUserHandle(userHandle));
     }
 
     @Override
     public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
             int userHandle, boolean overrideableByRestore) {
         return Settings.Secure.putStringForUser(
-                mContentResolver, name, value, tag, makeDefault, userHandle, overrideableByRestore);
+                mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle),
+                overrideableByRestore);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
index 1bf5f07..b6846a3 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
@@ -22,8 +22,11 @@
 import android.content.ContentResolver;
 import android.database.ContentObserver;
 import android.net.Uri;
+import android.os.UserHandle;
 import android.provider.Settings;
 
+import com.android.systemui.settings.UserTracker;
+
 /**
  * Used to interact with Settings.Secure, Settings.Global, and Settings.System.
  *
@@ -46,6 +49,11 @@
     ContentResolver getContentResolver();
 
     /**
+     * Returns that {@link UserTracker} this instance was constructed with.
+     */
+    UserTracker getUserTracker();
+
+    /**
      * Returns the user id for the associated {@link ContentResolver}.
      */
     default int getUserId() {
@@ -53,6 +61,17 @@
     }
 
     /**
+     * Returns the actual current user handle when querying with the current user. Otherwise,
+     * returns the passed in user id.
+     */
+    default int getRealUserHandle(int userHandle) {
+        if (userHandle != UserHandle.USER_CURRENT) {
+            return userHandle;
+        }
+        return getUserTracker().getUserId();
+    }
+
+    /**
      * Construct the content URI for a particular name/value pair,
      * useful for monitoring changes with a ContentObserver.
      * @param name to look up in the table
@@ -84,18 +103,18 @@
      *
      * Implicitly calls {@link #getUriFor(String)} on the passed in name.
      */
-    default void registerContentObserver(String name, boolean notifyForDescendents,
+    default void registerContentObserver(String name, boolean notifyForDescendants,
             ContentObserver settingsObserver) {
-        registerContentObserver(getUriFor(name), notifyForDescendents, settingsObserver);
+        registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
     }
 
     /**
      * Convenience wrapper around
      * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
      */
-    default void registerContentObserver(Uri uri, boolean notifyForDescendents,
+    default void registerContentObserver(Uri uri, boolean notifyForDescendants,
             ContentObserver settingsObserver) {
-        registerContentObserverForUser(uri, notifyForDescendents, settingsObserver, getUserId());
+        registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
     }
 
     /**
@@ -127,10 +146,10 @@
      * Implicitly calls {@link #getUriFor(String)} on the passed in name.
      */
     default void registerContentObserverForUser(
-            String name, boolean notifyForDescendents, ContentObserver settingsObserver,
+            String name, boolean notifyForDescendants, ContentObserver settingsObserver,
             int userHandle) {
         registerContentObserverForUser(
-                getUriFor(name), notifyForDescendents, settingsObserver, userHandle);
+                getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
     }
 
     /**
@@ -138,10 +157,10 @@
      * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
      */
     default void registerContentObserverForUser(
-            Uri uri, boolean notifyForDescendents, ContentObserver settingsObserver,
+            Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
             int userHandle) {
         getContentResolver().registerContentObserver(
-                uri, notifyForDescendents, settingsObserver, userHandle);
+                uri, notifyForDescendants, settingsObserver, getRealUserHandle(userHandle));
     }
 
     /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
index 0b8257d..561495e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
@@ -19,7 +19,6 @@
 
 import android.annotation.UserIdInt
 import android.database.ContentObserver
-import android.os.UserHandle
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -29,8 +28,8 @@
 
     /** Returns a flow of [Unit] that is invoked each time that content is updated. */
     fun SettingsProxy.observerFlow(
+        @UserIdInt userId: Int,
         vararg names: String,
-        @UserIdInt userId: Int = UserHandle.USER_CURRENT,
     ): Flow<Unit> {
         return conflatedCallbackFlow {
             val observer =
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
index 0dbb76f..fba7ddf 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
@@ -20,14 +20,18 @@
 import android.net.Uri;
 import android.provider.Settings;
 
+import com.android.systemui.settings.UserTracker;
+
 import javax.inject.Inject;
 
 class SystemSettingsImpl implements SystemSettings {
     private final ContentResolver mContentResolver;
+    private final UserTracker mUserTracker;
 
     @Inject
-    SystemSettingsImpl(ContentResolver contentResolver) {
+    SystemSettingsImpl(ContentResolver contentResolver, UserTracker userTracker) {
         mContentResolver = contentResolver;
+        mUserTracker = userTracker;
     }
 
     @Override
@@ -36,13 +40,19 @@
     }
 
     @Override
+    public UserTracker getUserTracker() {
+        return mUserTracker;
+    }
+
+    @Override
     public Uri getUriFor(String name) {
         return Settings.System.getUriFor(name);
     }
 
     @Override
     public String getStringForUser(String name, int userHandle) {
-        return Settings.System.getStringForUser(mContentResolver, name, userHandle);
+        return Settings.System.getStringForUser(mContentResolver, name,
+                getRealUserHandle(userHandle));
     }
 
     @Override
@@ -52,7 +62,8 @@
 
     @Override
     public boolean putStringForUser(String name, String value, int userHandle) {
-        return Settings.System.putStringForUser(mContentResolver, name, value, userHandle);
+        return Settings.System.putStringForUser(mContentResolver, name, value,
+                getRealUserHandle(userHandle));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
index f71d988..a453726 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
@@ -25,6 +25,7 @@
 import android.provider.Settings;
 import android.view.WindowManager.LayoutParams;
 
+import com.android.internal.R;
 import com.android.settingslib.applications.InterestingConfigChanges;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.demomode.DemoMode;
@@ -55,7 +56,7 @@
     public static final String VOLUME_UP_SILENT = "sysui_volume_up_silent";
     public static final String VOLUME_SILENT_DO_NOT_DISTURB = "sysui_do_not_disturb";
 
-    public static final boolean DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT = false;
+    private final boolean mDefaultVolumeDownToEnterSilent;
     public static final boolean DEFAULT_VOLUME_UP_TO_EXIT_SILENT = false;
     public static final boolean DEFAULT_DO_NOT_DISTURB_WHEN_SILENT = false;
 
@@ -72,12 +73,7 @@
     private final KeyguardViewMediator mKeyguardViewMediator;
     private final ActivityStarter mActivityStarter;
     private VolumeDialog mDialog;
-    private VolumePolicy mVolumePolicy = new VolumePolicy(
-            DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT,  // volumeDownToEnterSilent
-            DEFAULT_VOLUME_UP_TO_EXIT_SILENT,  // volumeUpToExitSilent
-            DEFAULT_DO_NOT_DISTURB_WHEN_SILENT,  // doNotDisturbWhenSilent
-            400    // vibrateToSilentDebounce
-    );
+    private VolumePolicy mVolumePolicy;
 
     @Inject
     public VolumeDialogComponent(
@@ -107,7 +103,20 @@
                     mDialog = dialog;
                     mDialog.init(LayoutParams.TYPE_VOLUME_OVERLAY, mVolumeDialogCallback);
                 }).build();
+
+
+        mDefaultVolumeDownToEnterSilent = mContext.getResources()
+                .getBoolean(R.bool.config_volume_down_to_enter_silent);
+
+        mVolumePolicy = new VolumePolicy(
+                mDefaultVolumeDownToEnterSilent,  // volumeDownToEnterSilent
+                DEFAULT_VOLUME_UP_TO_EXIT_SILENT,  // volumeUpToExitSilent
+                DEFAULT_DO_NOT_DISTURB_WHEN_SILENT,  // doNotDisturbWhenSilent
+                400    // vibrateToSilentDebounce
+        );
+
         applyConfiguration();
+
         tunerService.addTunable(this, VOLUME_DOWN_SILENT, VOLUME_UP_SILENT,
                 VOLUME_SILENT_DO_NOT_DISTURB);
         demoModeController.addCallback(this);
@@ -121,7 +130,7 @@
 
         if (VOLUME_DOWN_SILENT.equals(key)) {
             volumeDownToEnterSilent =
-                TunerService.parseIntegerSwitch(newValue, DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT);
+                TunerService.parseIntegerSwitch(newValue, mDefaultVolumeDownToEnterSilent);
         } else if (VOLUME_UP_SILENT.equals(key)) {
             volumeUpToExitSilent =
                 TunerService.parseIntegerSwitch(newValue, DEFAULT_VOLUME_UP_TO_EXIT_SILENT);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 98d904e..89b66ee 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -46,7 +46,6 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.provider.Settings;
 import android.service.notification.Condition;
@@ -69,6 +68,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.RingerModeLiveData;
 import com.android.systemui.util.RingerModeTracker;
@@ -133,6 +133,7 @@
     private final CaptioningManager mCaptioningManager;
     private final KeyguardManager mKeyguardManager;
     private final ActivityManager mActivityManager;
+    private final UserTracker mUserTracker;
     protected C mCallbacks = new C();
     private final State mState = new State();
     protected final MediaSessionsCallbacks mMediaSessionsCallbacksW;
@@ -180,6 +181,7 @@
             CaptioningManager captioningManager,
             KeyguardManager keyguardManager,
             ActivityManager activityManager,
+            UserTracker userTracker,
             DumpManager dumpManager
     ) {
         mContext = context.getApplicationContext();
@@ -209,6 +211,7 @@
         mCaptioningManager = captioningManager;
         mKeyguardManager = keyguardManager;
         mActivityManager = activityManager;
+        mUserTracker = userTracker;
         dumpManager.registerDumpable("VolumeDialogControllerImpl", this);
 
         boolean accessibilityVolumeStreamActive = accessibilityManager
@@ -371,7 +374,7 @@
         if (System.currentTimeMillis() - mLastToggledRingerOn < TOUCH_FEEDBACK_TIMEOUT_MS) {
             try {
                 mAudioService.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD,
-                        UserHandle.USER_CURRENT);
+                        mUserTracker.getUserId());
             } catch (RemoteException e) {
                 // ignore
             }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 5641237..3138f26 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -72,6 +72,7 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.VibrationEffect;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.text.InputFilter;
@@ -108,6 +109,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.view.RotationPolicy;
@@ -127,11 +130,15 @@
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.AlphaTintDrawableWrapper;
+import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.RoundedCornerProgressDrawable;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -188,6 +195,9 @@
     private ViewGroup mDialogRowsView;
     private ViewGroup mRinger;
 
+    private DeviceConfigProxy mDeviceConfigProxy;
+    private Executor mExecutor;
+
     /**
      * Container for the top part of the dialog, which contains the ringer, the ringer drawer, the
      * volume rows, and the ellipsis button. This does not include the live caption button.
@@ -276,6 +286,13 @@
     private BackgroundBlurDrawable mDialogRowsViewBackground;
     private final InteractionJankMonitor mInteractionJankMonitor;
 
+    private boolean mSeparateNotification;
+
+    @VisibleForTesting
+    int mVolumeRingerIconDrawableId;
+    @VisibleForTesting
+    int mVolumeRingerMuteIconDrawableId;
+
     public VolumeDialogImpl(
             Context context,
             VolumeDialogController volumeDialogController,
@@ -286,6 +303,8 @@
             VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor,
+            DeviceConfigProxy deviceConfigProxy,
+            Executor executor,
             DumpManager dumpManager) {
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
@@ -328,6 +347,50 @@
         }
 
         initDimens();
+
+        mDeviceConfigProxy = deviceConfigProxy;
+        mExecutor = executor;
+        mSeparateNotification = mDeviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
+        updateRingerModeIconSet();
+    }
+
+    /**
+     * If ringer and notification are the same stream (T and earlier), use notification-like bell
+     * icon set.
+     * If ringer and notification are separated, then use generic speaker icons.
+     */
+    private void updateRingerModeIconSet() {
+        if (mSeparateNotification) {
+            mVolumeRingerIconDrawableId = R.drawable.ic_speaker_on;
+            mVolumeRingerMuteIconDrawableId = R.drawable.ic_speaker_mute;
+        } else {
+            mVolumeRingerIconDrawableId = R.drawable.ic_volume_ringer;
+            mVolumeRingerMuteIconDrawableId = R.drawable.ic_volume_ringer_mute;
+        }
+
+        if (mRingerDrawerMuteIcon != null) {
+            mRingerDrawerMuteIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
+        }
+        if (mRingerDrawerNormalIcon != null) {
+            mRingerDrawerNormalIcon.setImageResource(mVolumeRingerIconDrawableId);
+        }
+    }
+
+    /**
+     * Change icon for ring stream (not ringer mode icon)
+     */
+    private void updateRingRowIcon() {
+        Optional<VolumeRow> volumeRow = mRows.stream().filter(row -> row.stream == STREAM_RING)
+                .findFirst();
+        if (volumeRow.isPresent()) {
+            VolumeRow volRow = volumeRow.get();
+            volRow.iconRes = mSeparateNotification ? R.drawable.ic_ring_volume
+                    : R.drawable.ic_volume_ringer;
+            volRow.iconMuteRes = mSeparateNotification ? R.drawable.ic_ring_volume_off
+                    : R.drawable.ic_volume_ringer_mute;
+            volRow.setIcon(volRow.iconRes, mContext.getTheme());
+        }
     }
 
     @Override
@@ -344,6 +407,9 @@
         mController.getState();
 
         mConfigurationController.addCallback(this);
+
+        mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+                mExecutor, this::onDeviceConfigChange);
     }
 
     @Override
@@ -351,6 +417,24 @@
         mController.removeCallback(mControllerCallbackH);
         mHandler.removeCallbacksAndMessages(null);
         mConfigurationController.removeCallback(this);
+        mDeviceConfigProxy.removeOnPropertiesChangedListener(this::onDeviceConfigChange);
+    }
+
+    /**
+     * Update ringer mode icon based on the config
+     */
+    private void onDeviceConfigChange(DeviceConfig.Properties properties) {
+        Set<String> changeSet = properties.getKeyset();
+        if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
+            boolean newVal = properties.getBoolean(
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
+            if (newVal != mSeparateNotification) {
+                mSeparateNotification = newVal;
+                updateRingerModeIconSet();
+                updateRingRowIcon();
+
+            }
+        }
     }
 
     @Override
@@ -559,6 +643,8 @@
         mRingerDrawerNormalIcon = mDialog.findViewById(R.id.volume_drawer_normal_icon);
         mRingerDrawerNewSelectionBg = mDialog.findViewById(R.id.volume_drawer_selection_background);
 
+        updateRingerModeIconSet();
+
         setupRingerDrawer();
 
         mODICaptionsView = mDialog.findViewById(R.id.odi_captions);
@@ -582,8 +668,14 @@
             addRow(AudioManager.STREAM_MUSIC,
                     R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
             if (!AudioSystem.isSingleVolume(mContext)) {
-                addRow(AudioManager.STREAM_RING,
-                        R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true, false);
+                if (mSeparateNotification) {
+                    addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
+                            R.drawable.ic_ring_volume_off, true, false);
+                } else {
+                    addRow(AudioManager.STREAM_RING, R.drawable.ic_volume_ringer,
+                            R.drawable.ic_volume_ringer, true, false);
+                }
+
                 addRow(STREAM_ALARM,
                         R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false);
                 addRow(AudioManager.STREAM_VOICE_CALL,
@@ -1308,6 +1400,7 @@
             @Override
             public void onAnimationCancel(@NonNull Animator animation) {
                 mInteractionJankMonitor.cancel(CUJ_VOLUME_CONTROL);
+                Log.d(TAG, "onAnimationCancel");
             }
 
             @Override
@@ -1381,6 +1474,7 @@
         mHandler.removeMessages(H.DISMISS);
         mHandler.removeMessages(H.SHOW);
         if (mIsAnimatingDismiss) {
+            Log.d(TAG, "dismissH: isAnimatingDismiss");
             return;
         }
         mIsAnimatingDismiss = true;
@@ -1397,6 +1491,7 @@
                 .setDuration(mDialogHideAnimationDurationMs)
                 .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
                 .withEndAction(() -> mHandler.postDelayed(() -> {
+                    mController.notifyVisible(false);
                     mDialog.dismiss();
                     tryToRemoveCaptionsTooltip();
                     mIsAnimatingDismiss = false;
@@ -1407,7 +1502,6 @@
         animator.setListener(getJankListener(getDialogView(), TYPE_DISMISS,
                 mDialogHideAnimationDurationMs)).start();
         checkODICaptionsTooltip(true);
-        mController.notifyVisible(false);
         synchronized (mSafetyWarningLock) {
             if (mSafetyWarning != null) {
                 if (D.BUG) Log.d(TAG, "SafetyWarning dismissed");
@@ -1542,8 +1636,8 @@
                     mRingerIcon.setTag(Events.ICON_STATE_VIBRATE);
                     break;
                 case AudioManager.RINGER_MODE_SILENT:
-                    mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
-                    mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
+                    mRingerIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
+                    mSelectedRingerIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
                     mRingerIcon.setTag(Events.ICON_STATE_MUTE);
                     addAccessibilityDescription(mRingerIcon, RINGER_MODE_SILENT,
                             mContext.getString(R.string.volume_ringer_hint_unmute));
@@ -1552,14 +1646,14 @@
                 default:
                     boolean muted = (mAutomute && ss.level == 0) || ss.muted;
                     if (!isZenMuted && muted) {
-                        mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
-                        mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
+                        mRingerIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
+                        mSelectedRingerIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
                         addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL,
                                 mContext.getString(R.string.volume_ringer_hint_unmute));
                         mRingerIcon.setTag(Events.ICON_STATE_MUTE);
                     } else {
-                        mRingerIcon.setImageResource(R.drawable.ic_volume_ringer);
-                        mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer);
+                        mRingerIcon.setImageResource(mVolumeRingerIconDrawableId);
+                        mSelectedRingerIcon.setImageResource(mVolumeRingerIconDrawableId);
                         if (mController.hasVibrator()) {
                             addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL,
                                     mContext.getString(R.string.volume_ringer_hint_vibrate));
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index bd42033..0ab6c69 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -20,6 +20,7 @@
 import android.media.AudioManager;
 
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
@@ -28,11 +29,14 @@
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.volume.VolumeDialogComponent;
 import com.android.systemui.volume.VolumeDialogImpl;
 import com.android.systemui.volume.VolumePanelFactory;
 
+import java.util.concurrent.Executor;
+
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
@@ -57,6 +61,8 @@
             VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor,
+            DeviceConfigProxy deviceConfigProxy,
+            @Main Executor executor,
             DumpManager dumpManager) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
@@ -68,6 +74,8 @@
                 volumePanelFactory,
                 activityStarter,
                 interactionJankMonitor,
+                deviceConfigProxy,
+                executor,
                 dumpManager);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
index 4cbc709..4da5d49 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
@@ -273,7 +273,7 @@
             };
 
             mSecureSettings.registerContentObserverForUser(
-                    Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT),
+                    Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
                     false /* notifyForDescendants */,
                     mDefaultPaymentAppObserver,
                     UserHandle.USER_ALL);
@@ -293,7 +293,7 @@
             };
 
             mSecureSettings.registerContentObserverForUser(
-                    Settings.Secure.getUriFor(QuickAccessWalletClientImpl.SETTING_KEY),
+                    QuickAccessWalletClientImpl.SETTING_KEY,
                     false /* notifyForDescendants */,
                     mWalletPreferenceObserver,
                     UserHandle.USER_ALL);
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
new file mode 100644
index 0000000..7b8235a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.wallet.controller
+
+import android.Manifest
+import android.content.Context
+import android.content.IntentFilter
+import android.service.quickaccesswallet.GetWalletCardsError
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.service.quickaccesswallet.WalletCard
+import com.android.systemui.broadcast.BroadcastDispatcher
+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.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.shareIn
+
+@SysUISingleton
+class WalletContextualSuggestionsController
+@Inject
+constructor(
+    @Application private val applicationCoroutineScope: CoroutineScope,
+    private val walletController: QuickAccessWalletController,
+    broadcastDispatcher: BroadcastDispatcher,
+    featureFlags: FeatureFlags
+) {
+    private val allWalletCards: Flow<List<WalletCard>> =
+        if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+            conflatedCallbackFlow {
+                val callback =
+                    object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
+                        override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
+                            trySendWithFailureLogging(response.walletCards, TAG)
+                        }
+
+                        override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
+                            trySendWithFailureLogging(emptyList<WalletCard>(), TAG)
+                        }
+                    }
+
+                walletController.setupWalletChangeObservers(
+                    callback,
+                    QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+                    QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+                )
+                walletController.updateWalletPreference()
+                walletController.queryWalletCards(callback)
+
+                awaitClose {
+                    walletController.unregisterWalletChangeObservers(
+                        QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+                        QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+                    )
+                }
+            }
+        } else {
+            emptyFlow()
+        }
+
+    private val contextualSuggestionsCardIds: Flow<Set<String>> =
+        if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+            broadcastDispatcher.broadcastFlow(
+                filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS),
+                permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE,
+                flags = Context.RECEIVER_EXPORTED
+            ) { intent, _ ->
+                if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) {
+                    intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet()
+                } else {
+                    emptySet()
+                }
+            }
+        } else {
+            emptyFlow()
+        }
+
+    val contextualSuggestionCards: Flow<List<WalletCard>> =
+        combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
+                cards.filter { card -> ids.contains(card.cardId) }
+            }
+            .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly)
+
+    companion object {
+        private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS =
+            "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS"
+
+        private const val UPDATE_CARD_IDS_EXTRA = "cardIds"
+
+        private const val TAG = "WalletSuggestions"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
index 2c901d2..9429d89 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
@@ -22,6 +22,8 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.QuickAccessWalletTile;
 import com.android.systemui.wallet.ui.WalletActivity;
 
 import java.util.concurrent.Executor;
@@ -31,6 +33,7 @@
 import dagger.Provides;
 import dagger.multibindings.ClassKey;
 import dagger.multibindings.IntoMap;
+import dagger.multibindings.StringKey;
 
 
 /**
@@ -52,4 +55,11 @@
             @Background Executor bgExecutor) {
         return QuickAccessWalletClient.create(context, bgExecutor);
     }
+
+    /** */
+    @Binds
+    @IntoMap
+    @StringKey(QuickAccessWalletTile.TILE_SPEC)
+    public abstract QSTileImpl<?> bindQuickAccessWalletTile(
+            QuickAccessWalletTile quickAccessWalletTile);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 1f1b32c..8b925b7 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -26,7 +26,6 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Trace;
-import android.os.UserHandle;
 import android.service.wallpaper.WallpaperService;
 import android.util.Log;
 import android.view.Surface;
@@ -37,6 +36,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.io.FileDescriptor;
@@ -58,6 +58,8 @@
     private volatile int mPages = 1;
     private boolean mPagesComputed = false;
 
+    private final UserTracker mUserTracker;
+
     // used for most tasks (call canvas.drawBitmap, load/unload the bitmap)
     @Background
     private final DelayableExecutor mBackgroundExecutor;
@@ -66,9 +68,11 @@
     private static final int DELAY_UNLOAD_BITMAP = 2000;
 
     @Inject
-    public ImageWallpaper(@Background DelayableExecutor backgroundExecutor) {
+    public ImageWallpaper(@Background DelayableExecutor backgroundExecutor,
+            UserTracker userTracker) {
         super();
         mBackgroundExecutor = backgroundExecutor;
+        mUserTracker = userTracker;
     }
 
     @Override
@@ -288,7 +292,7 @@
             boolean loadSuccess = false;
             Bitmap bitmap;
             try {
-                bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false);
+                bitmap = mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false);
                 if (bitmap != null
                         && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
                     throw new RuntimeException("Wallpaper is too large to draw!");
@@ -300,9 +304,9 @@
                 // default wallpaper can't be loaded.
                 Log.w(TAG, "Unable to load wallpaper!", exception);
                 mWallpaperManager.clearWallpaper(
-                        WallpaperManager.FLAG_SYSTEM, UserHandle.USER_CURRENT);
+                        WallpaperManager.FLAG_SYSTEM, mUserTracker.getUserId());
                 try {
-                    bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false);
+                    bitmap = mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false);
                 } catch (RuntimeException | OutOfMemoryError e) {
                     Log.w(TAG, "Unable to load default wallpaper!", e);
                     bitmap = null;
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 7033ccd..5d896cb 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -236,7 +236,8 @@
 
         // Store callback in a field so it won't get GC'd
         mStatusBarWindowCallback =
-                (keyguardShowing, keyguardOccluded, bouncerShowing, isDozing, panelExpanded) ->
+                (keyguardShowing, keyguardOccluded, bouncerShowing, isDozing, panelExpanded,
+                        isDreaming) ->
                         mBubbles.onNotificationPanelExpandedChanged(panelExpanded);
         notificationShadeWindowController.registerCallback(mStatusBarWindowCallback);
 
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 8ef98f0..bd60401 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.wmshell;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
@@ -50,6 +48,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.notetask.NoteTaskInitializer;
+import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.statusbar.CommandQueue;
@@ -124,6 +123,7 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final ProtoTracer mProtoTracer;
     private final UserTracker mUserTracker;
+    private final DisplayTracker mDisplayTracker;
     private final NoteTaskInitializer mNoteTaskInitializer;
     private final Executor mSysUiMainExecutor;
 
@@ -186,6 +186,7 @@
             ProtoTracer protoTracer,
             WakefulnessLifecycle wakefulnessLifecycle,
             UserTracker userTracker,
+            DisplayTracker displayTracker,
             NoteTaskInitializer noteTaskInitializer,
             @Main Executor sysUiMainExecutor) {
         mContext = context;
@@ -203,6 +204,7 @@
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
         mUserTracker = userTracker;
+        mDisplayTracker = displayTracker;
         mNoteTaskInitializer = noteTaskInitializer;
         mSysUiMainExecutor = sysUiMainExecutor;
     }
@@ -268,7 +270,7 @@
             public void onStartTransition(boolean isEntering) {
                 mSysUiMainExecutor.execute(() -> {
                     mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
-                            true).commitUpdate(DEFAULT_DISPLAY);
+                            true).commitUpdate(mDisplayTracker.getDefaultDisplayId());
                 });
             }
 
@@ -276,7 +278,7 @@
             public void onStartFinished(Rect bounds) {
                 mSysUiMainExecutor.execute(() -> {
                     mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
-                            true).commitUpdate(DEFAULT_DISPLAY);
+                            true).commitUpdate(mDisplayTracker.getDefaultDisplayId());
                 });
             }
 
@@ -284,7 +286,7 @@
             public void onStopFinished(Rect bounds) {
                 mSysUiMainExecutor.execute(() -> {
                     mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
-                            false).commitUpdate(DEFAULT_DISPLAY);
+                            false).commitUpdate(mDisplayTracker.getDefaultDisplayId());
                 });
             }
         });
@@ -333,7 +335,8 @@
             @Override
             public void setImeWindowStatus(int displayId, IBinder token, int vis,
                     int backDisposition, boolean showImeSwitcher) {
-                if (displayId == DEFAULT_DISPLAY && (vis & InputMethodService.IME_VISIBLE) != 0) {
+                if (displayId == mDisplayTracker.getDefaultDisplayId()
+                        && (vis & InputMethodService.IME_VISIBLE) != 0) {
                     oneHanded.stopOneHanded(
                             OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT);
                 }
@@ -346,7 +349,7 @@
             @Override
             public void onVisibilityChanged(boolean hasFreeformTasks) {
                 mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
-                        .commitUpdate(DEFAULT_DISPLAY);
+                        .commitUpdate(mDisplayTracker.getDefaultDisplayId());
             }
         }, mSysUiMainExecutor);
     }
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/ActiveUnlockConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
index 39cc34b..e84a975 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
@@ -21,14 +21,23 @@
 import android.hardware.biometrics.BiometricFaceConstants
 import android.net.Uri
 import android.os.Handler
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_BIOMETRIC
 import android.os.UserHandle
-import android.provider.Settings
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_WAKE
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.FakeSettings
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -39,18 +48,12 @@
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import java.io.PrintWriter
 
 @SmallTest
 class ActiveUnlockConfigTest : SysuiTestCase() {
-    private val fakeWakeUri = Uri.Builder().appendPath("wake").build()
-    private val fakeUnlockIntentUri = Uri.Builder().appendPath("unlock-intent").build()
-    private val fakeBioFailUri = Uri.Builder().appendPath("bio-fail").build()
-    private val fakeFaceErrorsUri = Uri.Builder().appendPath("face-errors").build()
-    private val fakeFaceAcquiredUri = Uri.Builder().appendPath("face-acquired").build()
-    private val fakeUnlockIntentBioEnroll = Uri.Builder().appendPath("unlock-intent-bio").build()
 
-    @Mock
-    private lateinit var secureSettings: SecureSettings
+    private lateinit var secureSettings: FakeSettings
     @Mock
     private lateinit var contentResolver: ContentResolver
     @Mock
@@ -59,30 +62,19 @@
     private lateinit var dumpManager: DumpManager
     @Mock
     private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var mockPrintWriter: PrintWriter
 
     @Captor
     private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
 
     private lateinit var activeUnlockConfig: ActiveUnlockConfig
+    private var currentUser: Int = 0
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
-        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_WAKE))
-                .thenReturn(fakeWakeUri)
-        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT))
-                .thenReturn(fakeUnlockIntentUri)
-        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
-                .thenReturn(fakeBioFailUri)
-        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS))
-                .thenReturn(fakeFaceErrorsUri)
-        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO))
-                .thenReturn(fakeFaceAcquiredUri)
-        `when`(secureSettings.getUriFor(
-                Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED))
-                .thenReturn(fakeUnlockIntentBioEnroll)
-
+        currentUser = KeyguardUpdateMonitor.getCurrentUser()
+        secureSettings = FakeSettings()
         activeUnlockConfig = ActiveUnlockConfig(
                 handler,
                 secureSettings,
@@ -92,104 +84,93 @@
     }
 
     @Test
-    fun testRegsitersForSettingsChanges() {
+    fun registersForSettingsChanges() {
         verifyRegisterSettingObserver()
     }
 
     @Test
-    fun testOnWakeupSettingChanged() {
-        verifyRegisterSettingObserver()
-
+    fun onWakeupSettingChanged() {
         // GIVEN no active unlock settings enabled
         assertFalse(
                 activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE)
         )
 
         // WHEN unlock on wake is allowed
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeWakeUri)
+        secureSettings.putIntForUser(ACTIVE_UNLOCK_ON_WAKE, 1, currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE))
 
         // THEN active unlock triggers allowed on: wake, unlock-intent, and biometric failure
         assertTrue(
                 activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE)
         )
         assertTrue(
                 activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT)
         )
         assertTrue(
                 activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL)
         )
     }
 
     @Test
-    fun testOnUnlockIntentSettingChanged() {
-        verifyRegisterSettingObserver()
-
+    fun onUnlockIntentSettingChanged() {
         // GIVEN no active unlock settings enabled
         assertFalse(
                 activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+                        ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT)
         )
 
         // WHEN unlock on biometric failed is allowed
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeUnlockIntentUri)
+        secureSettings.putIntForUser(ACTIVE_UNLOCK_ON_UNLOCK_INTENT, 1, currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT))
 
         // THEN active unlock triggers allowed on: biometric failure ONLY
         assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
         assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
         assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
     }
 
     @Test
-    fun testOnBioFailSettingChanged() {
-        verifyRegisterSettingObserver()
-
+    fun onBioFailSettingChanged() {
         // GIVEN no active unlock settings enabled and triggering unlock intent on biometric
         // enrollment setting is disabled (empty string is disabled, null would use the default)
-        `when`(secureSettings.getStringForUser(
-                Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
-                0)).thenReturn("")
-        updateSetting(fakeUnlockIntentBioEnroll)
+        secureSettings.putStringForUser(
+                ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED, "", currentUser)
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+        ))
         assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
 
         // WHEN unlock on biometric failed is allowed
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeBioFailUri)
+        secureSettings.putIntForUser(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, 1, currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
 
         // THEN active unlock triggers allowed on: biometric failure ONLY
         assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
         assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
         assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
     }
 
     @Test
-    fun testFaceErrorSettingsChanged() {
-        verifyRegisterSettingObserver()
-
+    fun faceErrorSettingsChanged() {
         // GIVEN unlock on biometric fail
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeBioFailUri)
+        secureSettings.putIntForUser(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, 1, currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
 
         // WHEN face error timeout (3), allow trigger active unlock
-        `when`(secureSettings.getStringForUser(Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS,
-                0)).thenReturn("3")
-        updateSetting(fakeFaceAcquiredUri)
+        secureSettings.putStringForUser(
+            ACTIVE_UNLOCK_ON_FACE_ERRORS, "3", currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ERRORS))
 
         // THEN active unlock triggers allowed on error TIMEOUT
         assertTrue(activeUnlockConfig.shouldRequestActiveUnlockOnFaceError(
@@ -200,20 +181,18 @@
     }
 
     @Test
-    fun testFaceAcquiredSettingsChanged() {
-        verifyRegisterSettingObserver()
-
+    fun faceAcquiredSettingsChanged() {
         // GIVEN unlock on biometric fail
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeBioFailUri)
+        secureSettings.putStringForUser(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, "1", currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
 
         // WHEN face acquiredMsg DARK_GLASSESand MOUTH_COVERING are allowed to trigger
-        `when`(secureSettings.getStringForUser(Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
-                0)).thenReturn(
+        secureSettings.putStringForUser(
+            ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
                 "${BiometricFaceConstants.FACE_ACQUIRED_MOUTH_COVERING_DETECTED}" +
-                        "|${BiometricFaceConstants.FACE_ACQUIRED_DARK_GLASSES_DETECTED}")
-        updateSetting(fakeFaceAcquiredUri)
+                        "|${BiometricFaceConstants.FACE_ACQUIRED_DARK_GLASSES_DETECTED}",
+            currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO))
 
         // THEN active unlock triggers allowed on acquired messages DARK_GLASSES & MOUTH_COVERING
         assertTrue(activeUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo(
@@ -228,75 +207,209 @@
     }
 
     @Test
-    fun testTriggerOnUnlockIntentWhenBiometricEnrolledNone() {
-        verifyRegisterSettingObserver()
-
+    fun triggerOnUnlockIntentWhenBiometricEnrolledNone() {
         // GIVEN unlock on biometric fail
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeBioFailUri)
+        secureSettings.putIntForUser(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, 1, currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
 
         // GIVEN fingerprint and face are NOT enrolled
         activeUnlockConfig.keyguardUpdateMonitor = keyguardUpdateMonitor
-        `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(false)
+        `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(false)
         `when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(false)
 
         // WHEN unlock intent is allowed when NO biometrics are enrolled (0)
-        `when`(secureSettings.getStringForUser(
-                Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
-                0)).thenReturn("${ActiveUnlockConfig.BIOMETRIC_TYPE_NONE}")
-        updateSetting(fakeUnlockIntentBioEnroll)
+
+        secureSettings.putStringForUser(
+            ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
+            "${ActiveUnlockConfig.BiometricType.NONE.intValue}", currentUser)
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+        ))
 
         // THEN active unlock triggers allowed on unlock intent
         assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
     }
 
     @Test
-    fun testTriggerOnUnlockIntentWhenBiometricEnrolledFingerprintOrFaceOnly() {
-        verifyRegisterSettingObserver()
-
+    fun triggerOnUnlockIntentWhenBiometricEnrolledFingerprintOrFaceOnly() {
         // GIVEN unlock on biometric fail
-        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
-                0, 0)).thenReturn(1)
-        updateSetting(fakeBioFailUri)
+        secureSettings.putIntForUser(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, 1, currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
 
         // GIVEN fingerprint and face are both enrolled
         activeUnlockConfig.keyguardUpdateMonitor = keyguardUpdateMonitor
-        `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true)
+        `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true)
         `when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(true)
 
         // WHEN unlock intent is allowed when ONLY fingerprint is enrolled or NO biometircs
         // are enrolled
-        `when`(secureSettings.getStringForUser(
-                Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
-                0)).thenReturn(
-                "${ActiveUnlockConfig.BIOMETRIC_TYPE_ANY_FACE}" +
-                        "|${ActiveUnlockConfig.BIOMETRIC_TYPE_ANY_FINGERPRINT}")
-        updateSetting(fakeUnlockIntentBioEnroll)
+        secureSettings.putStringForUser(
+                ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
+                "${ActiveUnlockConfig.BiometricType.ANY_FACE.intValue}" +
+                        "|${ActiveUnlockConfig.BiometricType.ANY_FINGERPRINT.intValue}",
+            currentUser)
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+        ))
 
         // THEN active unlock triggers NOT allowed on unlock intent
         assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
 
         // WHEN fingerprint ONLY enrolled
-        `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(false)
+        `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(false)
         `when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(true)
 
         // THEN active unlock triggers allowed on unlock intent
         assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
 
         // WHEN face ONLY enrolled
-        `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true)
+        `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true)
         `when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(false)
 
         // THEN active unlock triggers allowed on unlock intent
         assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
+    }
+
+    @Test
+    fun isWakeupConsideredUnlockIntent_singleValue() {
+        // GIVEN lift is considered an unlock intent
+        secureSettings.putIntForUser(
+            ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+            PowerManager.WAKE_REASON_LIFT,
+            currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS))
+
+        // THEN only WAKE_REASON_LIFT is considered an unlock intent
+        for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+            if (wakeReason == PowerManager.WAKE_REASON_LIFT) {
+                assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+            } else {
+                assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+            }
+        }
+    }
+
+    @Test
+    fun isWakeupConsideredUnlockIntent_multiValue() {
+        // GIVEN lift and tap are considered an unlock intent
+        secureSettings.putStringForUser(
+            ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+            PowerManager.WAKE_REASON_LIFT.toString() +
+                    "|" +
+                    PowerManager.WAKE_REASON_TAP.toString(),
+            currentUser
+        )
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS))
+
+        // THEN WAKE_REASON_LIFT and WAKE_REASON TAP are considered an unlock intent
+        for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+            if (wakeReason == PowerManager.WAKE_REASON_LIFT ||
+                wakeReason == PowerManager.WAKE_REASON_TAP) {
+                assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+            } else {
+                assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+            }
+        }
+    }
+
+    @Test
+    fun isWakeupConsideredUnlockIntent_emptyValues() {
+        // GIVEN lift and tap are considered an unlock intent
+        secureSettings.putStringForUser(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS, " ",
+            currentUser)
+        updateSetting(secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS))
+
+        // THEN no wake up gestures are considered an unlock intent
+        for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+            assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+        }
+    }
+
+    @Test
+    fun isWakeupForceDismissKeyguard_singleValue() {
+        verifyRegisterSettingObserver()
+
+        // GIVEN lift is considered an unlock intent
+        secureSettings.putStringForUser(ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD,
+            PowerManager.WAKE_REASON_LIFT.toString(), currentUser)
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD
+        ))
+
+        // THEN only WAKE_REASON_LIFT is considered an unlock intent
+        for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+            if (wakeReason == PowerManager.WAKE_REASON_LIFT) {
+                assertTrue(activeUnlockConfig.shouldWakeupForceDismissKeyguard(wakeReason))
+            } else {
+                assertFalse(activeUnlockConfig.shouldWakeupForceDismissKeyguard(wakeReason))
+            }
+        }
+    }
+
+    @Test
+    fun isWakeupForceDismissKeyguard_emptyValues() {
+        verifyRegisterSettingObserver()
+
+        // GIVEN lift and tap are considered an unlock intent
+        secureSettings.putStringForUser(ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD,
+            " ", currentUser)
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD
+        ))
+
+        // THEN no wake up gestures are considered an unlock intent
+        for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+            assertFalse(activeUnlockConfig.shouldWakeupForceDismissKeyguard(wakeReason))
+        }
+    }
+
+    @Test
+    fun isWakeupForceDismissKeyguard_multiValue() {
+        verifyRegisterSettingObserver()
+
+        // GIVEN lift and tap are considered an unlock intent
+        secureSettings.putStringForUser(ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD,
+            PowerManager.WAKE_REASON_LIFT.toString() +
+                    "|" +
+                    PowerManager.WAKE_REASON_TAP.toString(),
+            currentUser
+        )
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_WAKEUPS_TO_FORCE_DISMISS_KEYGUARD
+        ))
+
+        // THEN WAKE_REASON_LIFT and WAKE_REASON TAP are considered an unlock intent
+        for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+            if (wakeReason == PowerManager.WAKE_REASON_LIFT ||
+                wakeReason == PowerManager.WAKE_REASON_TAP) {
+                assertTrue(activeUnlockConfig.shouldWakeupForceDismissKeyguard(wakeReason))
+            } else {
+                assertFalse(activeUnlockConfig.shouldWakeupForceDismissKeyguard(wakeReason))
+            }
+        }
+    }
+
+    @Test
+    fun dump_onUnlockIntentWhenBiometricEnrolled_invalidNum_noArrayOutOfBoundsException() {
+        // GIVEN an invalid input (-1)
+        secureSettings.putStringForUser(ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
+            "-1", currentUser)
+
+        // WHEN the setting updates
+        updateSetting(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+        ))
+
+        // THEN no exception thrown
+        activeUnlockConfig.dump(mockPrintWriter, emptyArray())
     }
 
     private fun updateSetting(uri: Uri) {
+        verifyRegisterSettingObserver()
         settingsObserverCaptor.value.onChange(
                 false,
                 listOf(uri),
@@ -306,12 +419,17 @@
     }
 
     private fun verifyRegisterSettingObserver() {
-        verifyRegisterSettingObserver(fakeWakeUri)
-        verifyRegisterSettingObserver(fakeUnlockIntentUri)
-        verifyRegisterSettingObserver(fakeBioFailUri)
-        verifyRegisterSettingObserver(fakeFaceErrorsUri)
-        verifyRegisterSettingObserver(fakeFaceAcquiredUri)
-        verifyRegisterSettingObserver(fakeUnlockIntentBioEnroll)
+        verifyRegisterSettingObserver(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE))
+        verifyRegisterSettingObserver(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT))
+        verifyRegisterSettingObserver(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
+        verifyRegisterSettingObserver(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ERRORS))
+        verifyRegisterSettingObserver(secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO))
+        verifyRegisterSettingObserver(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+        ))
+        verifyRegisterSettingObserver(secureSettings.getUriFor(
+            ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS
+        ))
     }
 
     private fun verifyRegisterSettingObserver(uri: Uri) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index e8f8e25..480b8f9 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -32,16 +33,17 @@
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.ClockTickRate
 import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
-import java.util.TimeZone
-import java.util.concurrent.Executor
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.yield
@@ -50,15 +52,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
+import java.util.TimeZone
+import java.util.concurrent.Executor
+import org.mockito.Mockito.`when` as whenever
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -72,7 +75,7 @@
     @Mock private lateinit var animations: ClockAnimations
     @Mock private lateinit var events: ClockEvents
     @Mock private lateinit var clock: ClockController
-    @Mock private lateinit var mainExecutor: Executor
+    @Mock private lateinit var mainExecutor: DelayableExecutor
     @Mock private lateinit var bgExecutor: Executor
     @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var smallClockController: ClockFaceController
@@ -81,8 +84,11 @@
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
     @Mock private lateinit var parentView: View
     @Mock private lateinit var transitionRepository: KeyguardTransitionRepository
+    @Mock private lateinit var commandQueue: CommandQueue
     private lateinit var repository: FakeKeyguardRepository
-    @Mock private lateinit var logBuffer: LogBuffer
+    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+    @Mock private lateinit var smallLogBuffer: LogBuffer
+    @Mock private lateinit var largeLogBuffer: LogBuffer
     private lateinit var underTest: ClockEventController
 
     @Before
@@ -95,11 +101,19 @@
         whenever(largeClockController.events).thenReturn(largeClockEvents)
         whenever(clock.events).thenReturn(events)
         whenever(clock.animations).thenReturn(animations)
+        whenever(smallClockEvents.tickRate).thenReturn(ClockTickRate.PER_MINUTE)
+        whenever(largeClockEvents.tickRate).thenReturn(ClockTickRate.PER_MINUTE)
 
         repository = FakeKeyguardRepository()
+        bouncerRepository = FakeKeyguardBouncerRepository()
 
         underTest = ClockEventController(
-            KeyguardInteractor(repository = repository),
+            KeyguardInteractor(
+                repository = repository,
+                commandQueue = commandQueue,
+                featureFlags = featureFlags,
+                bouncerRepository = bouncerRepository,
+            ),
             KeyguardTransitionInteractor(repository = transitionRepository),
             broadcastDispatcher,
             batteryController,
@@ -109,7 +123,8 @@
             context,
             mainExecutor,
             bgExecutor,
-            logBuffer,
+            smallLogBuffer,
+            largeLogBuffer,
             featureFlags
         )
         underTest.clock = clock
@@ -136,8 +151,9 @@
 
     @Test
     fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
-        verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
-        verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
+        // TODO(b/266103601): delete this test and add more coverage for updateColors()
+        // verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
+        // verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
@@ -148,9 +164,6 @@
 
     @Test
     fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
-        verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
-        verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onDensityOrFontScaleChanged()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt
new file mode 100644
index 0000000..30fed0b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.keyguard
+
+import android.app.ActivityTaskManager
+import android.content.pm.PackageManager
+import android.os.PowerManager
+import android.telecom.TelecomManager
+import android.telephony.TelephonyManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class EmergencyButtonControllerTest : SysuiTestCase() {
+    lateinit var underTest: EmergencyButtonController
+    @Mock lateinit var emergencyButton: EmergencyButton
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock lateinit var telephonyManager: TelephonyManager
+    @Mock lateinit var powerManager: PowerManager
+    @Mock lateinit var activityTaskManager: ActivityTaskManager
+    @Mock lateinit var shadeController: ShadeController
+    @Mock lateinit var telecomManager: TelecomManager
+    @Mock lateinit var metricsLogger: MetricsLogger
+    @Mock lateinit var lockPatternUtils: LockPatternUtils
+    @Mock lateinit var packageManager: PackageManager
+    val fakeSystemClock = FakeSystemClock()
+    val mainExecutor = FakeExecutor(fakeSystemClock)
+    val backgroundExecutor = FakeExecutor(fakeSystemClock)
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            EmergencyButtonController(
+                emergencyButton,
+                configurationController,
+                keyguardUpdateMonitor,
+                telephonyManager,
+                powerManager,
+                activityTaskManager,
+                shadeController,
+                telecomManager,
+                metricsLogger,
+                lockPatternUtils,
+                mainExecutor,
+                backgroundExecutor
+            )
+        context.setMockPackageManager(packageManager)
+        Mockito.`when`(emergencyButton.context).thenReturn(context)
+    }
+
+    @Test
+    fun testUpdateEmergencyButton() {
+        Mockito.`when`(telecomManager.isInCall).thenReturn(true)
+        Mockito.`when`(lockPatternUtils.isSecure(anyInt())).thenReturn(true)
+        Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY))
+            .thenReturn(true)
+        underTest.updateEmergencyCallButton()
+        backgroundExecutor.runAllReady()
+        verify(emergencyButton, never())
+            .updateEmergencyCallButton(
+                /* isInCall= */ any(),
+                /* hasTelephonyRadio= */ any(),
+                /* simLocked= */ any(),
+                /* isSecure= */ any()
+            )
+        mainExecutor.runAllReady()
+        verify(emergencyButton)
+            .updateEmergencyCallButton(
+                /* isInCall= */ eq(true),
+                /* hasTelephonyRadio= */ eq(true),
+                /* simLocked= */ any(),
+                /* isSecure= */ eq(true)
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index 1059543..50645e5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -150,4 +150,11 @@
                 getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
                 false);
     }
+
+
+    @Test
+    public void testReset() {
+        mKeyguardAbsKeyInputViewController.reset();
+        verify(mKeyguardMessageAreaController).setMessage("", false);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index c8e7538..a5f90f8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,11 +29,11 @@
 
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
@@ -48,6 +48,8 @@
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.ClockEvents;
 import com.android.systemui.plugins.ClockFaceController;
+import com.android.systemui.plugins.ClockFaceEvents;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.shared.clocks.ClockRegistry;
@@ -99,6 +101,8 @@
     @Mock
     private ClockEvents mClockEvents;
     @Mock
+    private ClockFaceEvents mClockFaceEvents;
+    @Mock
     DumpManager mDumpManager;
     @Mock
     ClockEventController mClockEventController;
@@ -115,7 +119,14 @@
     private FrameLayout mLargeClockFrame;
     @Mock
     private SecureSettings mSecureSettings;
+    @Mock
+    private LogBuffer mLogBuffer;
 
+    private final View mFakeDateView = (View) (new ViewGroup(mContext) {
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {}
+    });
+    private final View mFakeWeatherView = new View(mContext);
     private final View mFakeSmartspaceView = new View(mContext);
 
     private KeyguardClockSwitchController mController;
@@ -143,6 +154,8 @@
         when(mLargeClockView.getContext()).thenReturn(getContext());
 
         when(mView.isAttachedToWindow()).thenReturn(true);
+        when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
+        when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mExecutor = new FakeExecutor(new FakeSystemClock());
         mController = new KeyguardClockSwitchController(
@@ -156,7 +169,8 @@
                 mSecureSettings,
                 mExecutor,
                 mDumpManager,
-                mClockEventController
+                mClockEventController,
+                mLogBuffer
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
@@ -165,6 +179,8 @@
         when(mClockController.getLargeClock()).thenReturn(mLargeClockController);
         when(mClockController.getSmallClock()).thenReturn(mSmallClockController);
         when(mClockController.getEvents()).thenReturn(mClockEvents);
+        when(mSmallClockController.getEvents()).thenReturn(mClockFaceEvents);
+        when(mLargeClockController.getEvents()).thenReturn(mClockFaceEvents);
         when(mClockController.getAnimations()).thenReturn(mClockAnimations);
         when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
         when(mClockEventController.getClock()).thenReturn(mClockController);
@@ -225,7 +241,7 @@
         mController.init();
         verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
 
-        listenerArgumentCaptor.getValue().onClockChanged();
+        listenerArgumentCaptor.getValue().onCurrentClockChanged();
         verify(mView, times(2)).setClock(mClockController, StatusBarState.SHADE);
         verify(mClockEventController, times(2)).setClock(mClockController);
     }
@@ -249,6 +265,19 @@
     }
 
     @Test
+    public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
+        when(mSmartspaceController.isEnabled()).thenReturn(true);
+        when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
+        mController.init();
+
+        mController.onLocaleListChanged();
+        // Should be called once on initial setup, then once again for locale change
+        verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView);
+        verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView);
+        verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
+    }
+
+    @Test
     public void testSmartspaceDisabledShowsKeyguardStatusArea() {
         when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
@@ -271,8 +300,9 @@
         ArgumentCaptor<ContentObserver> observerCaptor =
                 ArgumentCaptor.forClass(ContentObserver.class);
         mController.init();
-        verify(mSecureSettings).registerContentObserverForUser(any(Uri.class),
-                anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
+        verify(mSecureSettings).registerContentObserverForUser(
+                eq(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+                    anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
         ContentObserver observer = observerCaptor.getValue();
         mExecutor.runAllReady();
 
@@ -318,6 +348,22 @@
         assertEquals(0, mController.getClockBottom(10));
     }
 
+    @Test
+    public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() {
+        when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
+        ArgumentCaptor<ContentObserver> observerCaptor =
+                ArgumentCaptor.forClass(ContentObserver.class);
+        mController.init();
+        verify(mSecureSettings).registerContentObserverForUser(
+                eq(Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED), anyBoolean(),
+                    observerCaptor.capture(), eq(UserHandle.USER_ALL));
+        ContentObserver observer = observerCaptor.getValue();
+        mExecutor.runAllReady();
+        // When a settings change has occurred, check that view is visible.
+        observer.onChange(true);
+        mExecutor.runAllReady();
+        assertEquals(View.VISIBLE, mFakeWeatherView.getVisibility());
+    }
 
     private void verifyAttachment(VerificationMode times) {
         verify(mClockRegistry, times).registerClockChangeListener(
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
index 01365b4..1a365ef 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
@@ -25,9 +25,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
-import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -39,6 +37,7 @@
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.settings.FakeDisplayTracker;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -58,12 +57,12 @@
     @Mock
     private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
     @Mock
-    private DisplayManager mDisplayManager;
-    @Mock
     private KeyguardDisplayManager.KeyguardPresentation mKeyguardPresentation;
 
+    private Executor mMainExecutor = Runnable::run;
     private Executor mBackgroundExecutor = Runnable::run;
     private KeyguardDisplayManager mManager;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     // The default and secondary displays are both in the default group
     private Display mDefaultDisplay;
@@ -75,9 +74,9 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
         mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController,
-                mKeyguardStatusViewComponentFactory, mBackgroundExecutor));
+                mKeyguardStatusViewComponentFactory, mDisplayTracker, mMainExecutor,
+                mBackgroundExecutor));
         doReturn(mKeyguardPresentation).when(mManager).createPresentation(any());
 
         mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY,
@@ -96,23 +95,21 @@
 
     @Test
     public void testShow_defaultDisplayOnly() {
-        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay});
         mManager.show();
         verify(mManager, never()).createPresentation(any());
     }
 
     @Test
     public void testShow_includeSecondaryDisplay() {
-        when(mDisplayManager.getDisplays()).thenReturn(
-                new Display[]{mDefaultDisplay, mSecondaryDisplay});
+        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
         mManager.show();
         verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
     }
 
     @Test
     public void testShow_includeAlwaysUnlockedDisplay() {
-        when(mDisplayManager.getDisplays()).thenReturn(
-                new Display[]{mDefaultDisplay, mAlwaysUnlockedDisplay});
+        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mAlwaysUnlockedDisplay});
 
         mManager.show();
         verify(mManager, never()).createPresentation(any());
@@ -120,9 +117,8 @@
 
     @Test
     public void testShow_includeSecondaryAndAlwaysUnlockedDisplays() {
-        when(mDisplayManager.getDisplays()).thenReturn(
+        mDisplayTracker.setAllDisplays(
                 new Display[]{mDefaultDisplay, mSecondaryDisplay, mAlwaysUnlockedDisplay});
-
         mManager.show();
         verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewControllerTest.java
deleted file mode 100644
index 4021652..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewControllerTest.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.keyguard;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.media.AudioManager;
-import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableResources;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class KeyguardHostViewControllerTest extends SysuiTestCase {
-    @Mock
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-
-    private KeyguardHostView mKeyguardHostView;
-    @Mock
-    private AudioManager mAudioManager;
-    @Mock
-    private TelephonyManager mTelephonyManager;
-    @Mock
-    private ViewMediatorCallback mViewMediatorCallback;
-    @Mock
-    KeyguardSecurityContainerController.Factory mKeyguardSecurityContainerControllerFactory;
-    @Mock
-    private KeyguardSecurityContainerController mKeyguardSecurityContainerController;
-
-    @Rule
-    public MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    private TestableResources mTestableResources;
-    private KeyguardHostViewController mKeyguardHostViewController;
-
-    @Before
-    public void setup() {
-        mTestableResources = mContext.getOrCreateTestableResources();
-
-        mKeyguardHostView = new KeyguardHostView(mContext);
-
-        // Explicitly disable one handed keyguard.
-        mTestableResources.addOverride(
-                R.bool.can_use_one_handed_bouncer, false);
-
-        when(mKeyguardSecurityContainerControllerFactory.create(any(
-                KeyguardSecurityContainer.SecurityCallback.class)))
-                .thenReturn(mKeyguardSecurityContainerController);
-        mKeyguardHostViewController = new KeyguardHostViewController(
-                mKeyguardHostView, mKeyguardUpdateMonitor, mAudioManager, mTelephonyManager,
-                mViewMediatorCallback, mKeyguardSecurityContainerControllerFactory);
-    }
-
-    @Test
-    public void testHasDismissActions() {
-        assertFalse("Action not set yet", mKeyguardHostViewController.hasDismissActions());
-        mKeyguardHostViewController.setOnDismissAction(mock(OnDismissAction.class),
-                null /* cancelAction */);
-        assertTrue("Action should exist", mKeyguardHostViewController.hasDismissActions());
-    }
-
-    @Test
-    public void testOnStartingToHide() {
-        mKeyguardHostViewController.onStartingToHide();
-        verify(mKeyguardSecurityContainerController).onStartingToHide();
-    }
-
-    @Test
-    public void onBouncerVisible_propagatesToKeyguardSecurityContainerController() {
-        mKeyguardHostViewController.onBouncerVisibilityChanged(ViewGroup.VISIBLE);
-        mKeyguardHostViewController.onBouncerVisibilityChanged(ViewGroup.INVISIBLE);
-
-        InOrder order = inOrder(mKeyguardSecurityContainerController);
-        order.verify(mKeyguardSecurityContainerController).onBouncerVisibilityChanged(View.VISIBLE);
-        order.verify(mKeyguardSecurityContainerController).onBouncerVisibilityChanged(
-                View.INVISIBLE);
-    }
-
-    @Test
-    public void testGravityReappliedOnConfigurationChange() {
-        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT);
-        mKeyguardHostView.setLayoutParams(lp);
-
-        // Set initial gravity
-        mTestableResources.addOverride(R.integer.keyguard_host_view_gravity,
-                Gravity.CENTER);
-
-        // Kick off the initial pass...
-        mKeyguardHostViewController.init();
-        assertEquals(
-                ((FrameLayout.LayoutParams) mKeyguardHostView.getLayoutParams()).gravity,
-                Gravity.CENTER);
-
-        // Now simulate a config change
-        mTestableResources.addOverride(R.integer.keyguard_host_view_gravity,
-                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
-
-        mKeyguardHostViewController.updateResources();
-        assertEquals(
-                ((FrameLayout.LayoutParams) mKeyguardHostView.getLayoutParams()).gravity,
-                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
-    }
-
-    @Test
-    public void testGravityUsesOneHandGravityWhenApplicable() {
-        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT);
-        mKeyguardHostView.setLayoutParams(lp);
-
-        mTestableResources.addOverride(
-                R.integer.keyguard_host_view_gravity,
-                Gravity.CENTER);
-        mTestableResources.addOverride(
-                R.integer.keyguard_host_view_one_handed_gravity,
-                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
-
-        // Start disabled.
-        mTestableResources.addOverride(
-                R.bool.can_use_one_handed_bouncer, false);
-
-        mKeyguardHostViewController.init();
-        assertEquals(
-                ((FrameLayout.LayoutParams) mKeyguardHostView.getLayoutParams()).gravity,
-                Gravity.CENTER);
-
-        // And enable
-        mTestableResources.addOverride(
-                R.bool.can_use_one_handed_bouncer, true);
-
-        mKeyguardHostViewController.updateResources();
-        assertEquals(
-                ((FrameLayout.LayoutParams) mKeyguardHostView.getLayoutParams()).gravity,
-                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
-    }
-
-    @Test
-    public void testUpdateKeyguardPositionDelegatesToSecurityContainer() {
-        mKeyguardHostViewController.updateKeyguardPosition(1.0f);
-
-        verify(mKeyguardSecurityContainerController).updateKeyguardPosition(1.0f);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index d912793..ed928702 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -18,8 +18,10 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import android.view.View
 import android.view.inputmethod.InputMethodManager
 import android.widget.EditText
+import android.widget.ImageView
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
 import com.android.internal.widget.LockPatternUtils
@@ -30,6 +32,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
@@ -37,6 +40,7 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -76,7 +80,9 @@
     Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry))
         .thenReturn(passwordEntry)
     `when`(keyguardPasswordView.resources).thenReturn(context.resources)
-    keyguardPasswordViewController =
+    `when`(keyguardPasswordView.findViewById<ImageView>(R.id.switch_ime_button))
+        .thenReturn(mock(ImageView::class.java))
+     keyguardPasswordViewController =
         KeyguardPasswordViewController(
             keyguardPasswordView,
             keyguardUpdateMonitor,
@@ -113,6 +119,18 @@
   }
 
   @Test
+  fun onApplyWindowInsetsListener_onApplyWindowInsets() {
+      `when`(keyguardViewController.isBouncerShowing).thenReturn(false)
+      val argumentCaptor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+
+      keyguardPasswordViewController.onViewAttached()
+      verify(keyguardPasswordView).setOnApplyWindowInsetsListener(argumentCaptor.capture())
+      argumentCaptor.value.onApplyWindowInsets(keyguardPasswordView, null)
+
+      verify(keyguardPasswordView).hideKeyboard()
+  }
+
+  @Test
   fun testHideKeyboardWhenOnPause() {
     keyguardPasswordViewController.onPause()
     keyguardPasswordView.post {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 84f6d91..bbc7bc9 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -23,28 +23,35 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.biometrics.BiometricOverlayConstants;
+import android.media.AudioManager;
+import android.telephony.TelephonyManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.WindowInsetsController;
+import android.widget.FrameLayout;
 
 import androidx.test.filters.SmallTest;
 
@@ -60,6 +67,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.log.SessionTracker;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -71,6 +79,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
@@ -83,10 +92,8 @@
 @TestableLooper.RunWithLooper()
 public class KeyguardSecurityContainerControllerTest extends SysuiTestCase {
     private static final int TARGET_USER_ID = 100;
-
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
-
     @Mock
     private KeyguardSecurityContainer mView;
     @Mock
@@ -108,8 +115,6 @@
     @Mock
     private KeyguardInputViewController mInputViewController;
     @Mock
-    private KeyguardSecurityContainer.SecurityCallback mSecurityCallback;
-    @Mock
     private WindowInsetsController mWindowInsetsController;
     @Mock
     private KeyguardSecurityViewFlipper mSecurityViewFlipper;
@@ -126,8 +131,6 @@
     @Mock
     private EmergencyButtonController mEmergencyButtonController;
     @Mock
-    private Resources mResources;
-    @Mock
     private FalsingCollector mFalsingCollector;
     @Mock
     private FalsingManager mFalsingManager;
@@ -147,6 +150,12 @@
     private KeyguardPasswordViewController mKeyguardPasswordViewControllerMock;
     @Mock
     private FalsingA11yDelegate mFalsingA11yDelegate;
+    @Mock
+    private TelephonyManager mTelephonyManager;
+    @Mock
+    private ViewMediatorCallback mViewMediatorCallback;
+    @Mock
+    private AudioManager mAudioManager;
 
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback;
@@ -158,18 +167,25 @@
     private KeyguardSecurityContainerController mKeyguardSecurityContainerController;
     private KeyguardPasswordViewController mKeyguardPasswordViewController;
     private KeyguardPasswordView mKeyguardPasswordView;
+    private TestableResources mTestableResources;
 
     @Before
     public void setup() {
         mConfiguration = new Configuration();
         mConfiguration.setToDefaults(); // Defaults to ORIENTATION_UNDEFINED.
+        mTestableResources = mContext.getOrCreateTestableResources();
 
-        when(mResources.getConfiguration()).thenReturn(mConfiguration);
         when(mView.getContext()).thenReturn(mContext);
-        when(mView.getResources()).thenReturn(mResources);
+        when(mView.getResources()).thenReturn(mContext.getResources());
+        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(/* width=  */ 0, /* height= */
+                0);
+        lp.gravity = 0;
+        when(mView.getLayoutParams()).thenReturn(lp);
         when(mAdminSecondaryLockScreenControllerFactory.create(any(KeyguardSecurityCallback.class)))
                 .thenReturn(mAdminSecondaryLockScreenController);
         when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController);
+        when(mKeyguardSecurityViewFlipperController.getSecurityView(any(SecurityMode.class),
+                any(KeyguardSecurityCallback.class))).thenReturn(mInputViewController);
         mKeyguardPasswordView = spy((KeyguardPasswordView) LayoutInflater.from(mContext).inflate(
                 R.layout.keyguard_password_view, null));
         when(mKeyguardPasswordView.getRootView()).thenReturn(mSecurityViewFlipper);
@@ -178,20 +194,21 @@
         when(mKeyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea.class)))
                 .thenReturn(mKeyguardMessageAreaController);
         when(mKeyguardPasswordView.getWindowInsetsController()).thenReturn(mWindowInsetsController);
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN);
         mKeyguardPasswordViewController = new KeyguardPasswordViewController(
                 (KeyguardPasswordView) mKeyguardPasswordView, mKeyguardUpdateMonitor,
                 SecurityMode.Password, mLockPatternUtils, null,
                 mKeyguardMessageAreaControllerFactory, null, null, mEmergencyButtonController,
                 null, mock(Resources.class), null, mKeyguardViewController);
 
-        mKeyguardSecurityContainerController = new KeyguardSecurityContainerController.Factory(
+        mKeyguardSecurityContainerController = new KeyguardSecurityContainerController(
                 mView, mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils,
                 mKeyguardUpdateMonitor, mKeyguardSecurityModel, mMetricsLogger, mUiEventLogger,
                 mKeyguardStateController, mKeyguardSecurityViewFlipperController,
                 mConfigurationController, mFalsingCollector, mFalsingManager,
                 mUserSwitcherController, mFeatureFlags, mGlobalSettings,
-                mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate).create(
-                mSecurityCallback);
+                mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate,
+                mTelephonyManager, mViewMediatorCallback, mAudioManager);
     }
 
     @Test
@@ -243,7 +260,8 @@
                 eq(mFalsingA11yDelegate));
 
         // Update rotation. Should trigger update
-        mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mTestableResources.getResources().getConfiguration().orientation =
+                Configuration.ORIENTATION_LANDSCAPE;
 
         mKeyguardSecurityContainerController.updateResources();
         verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
@@ -277,7 +295,7 @@
 
     @Test
     public void showSecurityScreen_oneHandedMode_flagDisabled_noOneHandedMode() {
-        when(mResources.getBoolean(R.bool.can_use_one_handed_bouncer)).thenReturn(false);
+        mTestableResources.addOverride(R.bool.can_use_one_handed_bouncer, false);
         when(mKeyguardSecurityViewFlipperController.getSecurityView(
                 eq(SecurityMode.Pattern), any(KeyguardSecurityCallback.class)))
                 .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
@@ -291,7 +309,7 @@
 
     @Test
     public void showSecurityScreen_oneHandedMode_flagEnabled_oneHandedMode() {
-        when(mResources.getBoolean(R.bool.can_use_one_handed_bouncer)).thenReturn(true);
+        mTestableResources.addOverride(R.bool.can_use_one_handed_bouncer, true);
         when(mKeyguardSecurityViewFlipperController.getSecurityView(
                 eq(SecurityMode.Pattern), any(KeyguardSecurityCallback.class)))
                 .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
@@ -305,7 +323,7 @@
 
     @Test
     public void showSecurityScreen_twoHandedMode_flagEnabled_noOneHandedMode() {
-        when(mResources.getBoolean(R.bool.can_use_one_handed_bouncer)).thenReturn(true);
+        mTestableResources.addOverride(R.bool.can_use_one_handed_bouncer, true);
         setupGetSecurityView();
 
         mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
@@ -344,119 +362,9 @@
     }
 
     @Test
-    public void onBouncerVisibilityChanged_allConditionsGood_sideFpsHintShown() {
-        setupConditionsToEnableSideFpsHint();
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-
-        verify(mSideFpsController).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).hide(any());
-    }
-
-    @Test
-    public void onBouncerVisibilityChanged_fpsSensorNotRunning_sideFpsHintHidden() {
-        setupConditionsToEnableSideFpsHint();
-        setFingerprintDetectionRunning(false);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
-    }
-
-    @Test
-    public void onBouncerVisibilityChanged_withoutSidedSecurity_sideFpsHintHidden() {
-        setupConditionsToEnableSideFpsHint();
-        setSideFpsHintEnabledFromResources(false);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
-    }
-
-    @Test
-    public void onBouncerVisibilityChanged_unlockingWithFingerprintNotAllowed_sideFpsHintHidden() {
-        setupConditionsToEnableSideFpsHint();
-        setUnlockingWithFingerprintAllowed(false);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
-    }
-
-    @Test
-    public void onBouncerVisibilityChanged_sideFpsHintShown_sideFpsHintHidden() {
-        setupGetSecurityView();
-        setupConditionsToEnableSideFpsHint();
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        verify(mSideFpsController, atLeastOnce()).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.INVISIBLE);
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
-    }
-
-    @Test
-    public void onStartingToHide_sideFpsHintShown_sideFpsHintHidden() {
-        setupGetSecurityView();
-        setupConditionsToEnableSideFpsHint();
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        verify(mSideFpsController, atLeastOnce()).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onStartingToHide();
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
-    }
-
-    @Test
-    public void onPause_sideFpsHintShown_sideFpsHintHidden() {
-        setupGetSecurityView();
-        setupConditionsToEnableSideFpsHint();
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        verify(mSideFpsController, atLeastOnce()).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onPause();
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
-    }
-
-    @Test
-    public void onResume_sideFpsHintShouldBeShown_sideFpsHintShown() {
-        setupGetSecurityView();
-        setupConditionsToEnableSideFpsHint();
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onResume(0);
-
-        verify(mSideFpsController).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).hide(any());
-    }
-
-    @Test
-    public void onResume_sideFpsHintShouldNotBeShown_sideFpsHintHidden() {
-        setupGetSecurityView();
-        setupConditionsToEnableSideFpsHint();
-        setSideFpsHintEnabledFromResources(false);
-        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        reset(mSideFpsController);
-
-        mKeyguardSecurityContainerController.onResume(0);
-
-        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
-        verify(mSideFpsController, never()).show(any());
+    public void onBouncerVisibilityChanged_resetsScale() {
+        mKeyguardSecurityContainerController.onBouncerVisibilityChanged(false);
+        verify(mView).resetScale();
     }
 
     @Test
@@ -475,7 +383,9 @@
                 SecurityMode.SimPin);
 
         // THEN the next security method of PIN is set, and the keyguard is not marked as done
-        verify(mSecurityCallback, never()).finish(anyBoolean(), anyInt());
+
+        verify(mViewMediatorCallback, never()).keyguardDonePending(anyBoolean(), anyInt());
+        verify(mViewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt());
         assertThat(mKeyguardSecurityContainerController.getCurrentSecurityMode())
                 .isEqualTo(SecurityMode.PIN);
     }
@@ -549,17 +459,19 @@
     }
 
     @Test
-    public void onDensityorFontScaleChanged() {
+    public void onDensityOrFontScaleChanged() {
         ArgumentCaptor<ConfigurationController.ConfigurationListener>
                 configurationListenerArgumentCaptor = ArgumentCaptor.forClass(
                 ConfigurationController.ConfigurationListener.class);
         mKeyguardSecurityContainerController.onViewAttached();
         verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture());
+        clearInvocations(mKeyguardSecurityViewFlipperController);
+
         configurationListenerArgumentCaptor.getValue().onDensityOrFontScaleChanged();
 
         verify(mView).onDensityOrFontScaleChanged();
         verify(mKeyguardSecurityViewFlipperController).clearViews();
-        verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
+        verify(mKeyguardSecurityViewFlipperController).getSecurityView(eq(SecurityMode.PIN),
                 any(KeyguardSecurityCallback.class));
     }
 
@@ -570,11 +482,13 @@
                 ConfigurationController.ConfigurationListener.class);
         mKeyguardSecurityContainerController.onViewAttached();
         verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture());
+        clearInvocations(mKeyguardSecurityViewFlipperController);
+
         configurationListenerArgumentCaptor.getValue().onThemeChanged();
 
         verify(mView).reloadColors();
         verify(mKeyguardSecurityViewFlipperController).clearViews();
-        verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
+        verify(mKeyguardSecurityViewFlipperController).getSecurityView(eq(SecurityMode.PIN),
                 any(KeyguardSecurityCallback.class));
     }
 
@@ -585,47 +499,123 @@
                 ConfigurationController.ConfigurationListener.class);
         mKeyguardSecurityContainerController.onViewAttached();
         verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture());
+        clearInvocations(mKeyguardSecurityViewFlipperController);
+
         configurationListenerArgumentCaptor.getValue().onUiModeChanged();
 
         verify(mView).reloadColors();
         verify(mKeyguardSecurityViewFlipperController).clearViews();
+        verify(mKeyguardSecurityViewFlipperController).getSecurityView(eq(SecurityMode.PIN),
+                any(KeyguardSecurityCallback.class));
+    }
+
+    @Test
+    public void testHasDismissActions() {
+        assertFalse("Action not set yet", mKeyguardSecurityContainerController.hasDismissActions());
+        mKeyguardSecurityContainerController.setOnDismissAction(mock(
+                        ActivityStarter.OnDismissAction.class),
+                null /* cancelAction */);
+        assertTrue("Action should exist", mKeyguardSecurityContainerController.hasDismissActions());
+    }
+
+    @Test
+    public void testOnStartingToHide() {
+        mKeyguardSecurityContainerController.onStartingToHide();
+        verify(mInputViewController).onStartingToHide();
+    }
+
+    @Test
+    public void testGravityReappliedOnConfigurationChange() {
+        // Set initial gravity
+        mTestableResources.addOverride(R.integer.keyguard_host_view_gravity,
+                Gravity.CENTER);
+
+        // Kick off the initial pass...
+        mKeyguardSecurityContainerController.onInit();
+        verify(mView).setLayoutParams(argThat(
+                (ArgumentMatcher<FrameLayout.LayoutParams>) argument ->
+                        argument.gravity == Gravity.CENTER));
+        clearInvocations(mView);
+
+        // Now simulate a config change
+        mTestableResources.addOverride(R.integer.keyguard_host_view_gravity,
+                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
+
+        mKeyguardSecurityContainerController.updateResources();
+        verify(mView).setLayoutParams(argThat(
+                (ArgumentMatcher<FrameLayout.LayoutParams>) argument ->
+                        argument.gravity == (Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)));
+    }
+
+    @Test
+    public void testGravityUsesOneHandGravityWhenApplicable() {
+        mTestableResources.addOverride(
+                R.integer.keyguard_host_view_gravity,
+                Gravity.CENTER);
+        mTestableResources.addOverride(
+                R.integer.keyguard_host_view_one_handed_gravity,
+                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
+
+        // Start disabled.
+        mTestableResources.addOverride(
+                R.bool.can_use_one_handed_bouncer, false);
+
+        mKeyguardSecurityContainerController.onInit();
+        verify(mView).setLayoutParams(argThat(
+                (ArgumentMatcher<FrameLayout.LayoutParams>) argument ->
+                        argument.gravity == Gravity.CENTER));
+        clearInvocations(mView);
+
+        // And enable
+        mTestableResources.addOverride(
+                R.bool.can_use_one_handed_bouncer, true);
+
+        mKeyguardSecurityContainerController.updateResources();
+        verify(mView).setLayoutParams(argThat(
+                (ArgumentMatcher<FrameLayout.LayoutParams>) argument ->
+                        argument.gravity == (Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)));
+    }
+
+    @Test
+    public void testUpdateKeyguardPositionDelegatesToSecurityContainer() {
+        mKeyguardSecurityContainerController.updateKeyguardPosition(1.0f);
+        verify(mView).updatePositionByTouchX(1.0f);
+    }
+
+
+    @Test
+    public void testReinflateViewFlipper() {
+        mKeyguardSecurityContainerController.reinflateViewFlipper();
+        verify(mKeyguardSecurityViewFlipperController).clearViews();
         verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
                 any(KeyguardSecurityCallback.class));
     }
 
+    @Test
+    public void testSideFpsControllerShow() {
+        mKeyguardSecurityContainerController.updateSideFpsVisibility(/* isVisible= */ true);
+        verify(mSideFpsController).show(
+                SideFpsUiRequestSource.PRIMARY_BOUNCER,
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD);
+    }
+
+    @Test
+    public void testSideFpsControllerHide() {
+        mKeyguardSecurityContainerController.updateSideFpsVisibility(/* isVisible= */ false);
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+    }
+
     private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() {
         mKeyguardSecurityContainerController.onViewAttached();
         verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture());
         return mSwipeListenerArgumentCaptor.getValue();
     }
 
-    private void setupConditionsToEnableSideFpsHint() {
-        attachView();
-        setSideFpsHintEnabledFromResources(true);
-        setFingerprintDetectionRunning(true);
-        setUnlockingWithFingerprintAllowed(true);
-    }
-
     private void attachView() {
         mKeyguardSecurityContainerController.onViewAttached();
         verify(mKeyguardUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallback.capture());
     }
 
-    private void setFingerprintDetectionRunning(boolean running) {
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(running);
-        mKeyguardUpdateMonitorCallback.getValue().onBiometricRunningStateChanged(running,
-                BiometricSourceType.FINGERPRINT);
-    }
-
-    private void setSideFpsHintEnabledFromResources(boolean enabled) {
-        when(mResources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)).thenReturn(
-                enabled);
-    }
-
-    private void setUnlockingWithFingerprintAllowed(boolean allowed) {
-        when(mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed()).thenReturn(allowed);
-    }
-
     private void setupGetSecurityView() {
         when(mKeyguardSecurityViewFlipperController.getSecurityView(
                 any(), any(KeyguardSecurityCallback.class)))
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 36ed669..1bbc199 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -49,6 +49,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
 
 import androidx.constraintlayout.widget.ConstraintSet;
 import androidx.test.filters.SmallTest;
@@ -357,6 +359,27 @@
         assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(PARENT_ID);
     }
 
+    @Test
+    public void testPlayBackAnimation() {
+        OnBackAnimationCallback backCallback = mKeyguardSecurityContainer.getBackCallback();
+        backCallback.onBackStarted(createBackEvent(0, 0));
+        mKeyguardSecurityContainer.getBackCallback().onBackProgressed(
+                createBackEvent(0, 1));
+        assertThat(mKeyguardSecurityContainer.getScaleX()).isEqualTo(
+                KeyguardSecurityContainer.MIN_BACK_SCALE);
+        assertThat(mKeyguardSecurityContainer.getScaleY()).isEqualTo(
+                KeyguardSecurityContainer.MIN_BACK_SCALE);
+
+        // reset scale
+        mKeyguardSecurityContainer.resetScale();
+        assertThat(mKeyguardSecurityContainer.getScaleX()).isEqualTo(1);
+        assertThat(mKeyguardSecurityContainer.getScaleY()).isEqualTo(1);
+    }
+
+    private BackEvent createBackEvent(float touchX, float progress) {
+        return new BackEvent(0, 0, progress, BackEvent.EDGE_LEFT);
+    }
+
     private Configuration configuration(@Configuration.Orientation int orientation) {
         Configuration config = new Configuration();
         config.orientation = orientation;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
index 06082b6..68dc6c0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
@@ -29,6 +29,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
 
@@ -52,6 +53,7 @@
     private ConfigurationController mConfigurationController;
     @Mock
     private ActivityStarter mActivityStarter;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private DumpManager mDumpManager = new DumpManager();
 
     private KeyguardSliceViewController mController;
@@ -63,7 +65,7 @@
         when(mView.getContext()).thenReturn(mContext);
         mController = new KeyguardSliceViewController(
                 mView, mActivityStarter, mConfigurationController,
-                mTunerService, mDumpManager);
+                mTunerService, mDumpManager, mDisplayTracker);
         mController.setupUri(KeyguardSliceProvider.KEYGUARD_SLICE_URI);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index be4bbdf..7144914 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -24,8 +24,8 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -60,11 +60,11 @@
     @Mock
     DozeParameters mDozeParameters;
     @Mock
-    FeatureFlags mFeatureFlags;
-    @Mock
     ScreenOffAnimationController mScreenOffAnimationController;
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
+    @Mock
+    KeyguardLogger mKeyguardLogger;
 
     private KeyguardStatusViewController mController;
 
@@ -80,8 +80,8 @@
                 mKeyguardUpdateMonitor,
                 mConfigurationController,
                 mDozeParameters,
-                mFeatureFlags,
-                mScreenOffAnimationController);
+                mScreenOffAnimationController,
+                mKeyguardLogger);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 13cd328..3c80dad 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -17,7 +17,7 @@
 package com.android.keyguard;
 
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
 import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
@@ -28,17 +28,20 @@
 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;
 import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyObject;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -53,8 +56,6 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.IStrongAuthTracker;
 import android.app.trust.TrustManager;
@@ -87,7 +88,6 @@
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Handler;
-import android.os.IRemoteCallback;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -116,6 +116,7 @@
 import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.SessionTracker;
@@ -123,6 +124,7 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.settings.SecureSettings;
@@ -142,6 +144,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -191,6 +195,8 @@
     @Mock
     private DevicePolicyManager mDevicePolicyManager;
     @Mock
+    private DevicePostureController mDevicePostureController;
+    @Mock
     private IDreamManager mDreamManager;
     @Mock
     private KeyguardBypassController mKeyguardBypassController;
@@ -223,18 +229,16 @@
     @Mock
     private KeyguardUpdateMonitorLogger mKeyguardUpdateMonitorLogger;
     @Mock
-    private IActivityManager mActivityService;
-    @Mock
     private SessionTracker mSessionTracker;
     @Mock
     private UiEventLogger mUiEventLogger;
     @Mock
-    private PowerManager mPowerManager;
-    @Mock
     private GlobalSettings mGlobalSettings;
     private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
+    @Mock
+    private FingerprintInteractiveToAuthProvider mInteractiveToAuthProvider;
 
-
+    private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
     private final int mCurrentUserId = 100;
     private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0);
 
@@ -261,8 +265,6 @@
     @Before
     public void setup() throws RemoteException {
         MockitoAnnotations.initMocks(this);
-        when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo);
-        when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId);
         when(mFaceManager.isHardwareDetected()).thenReturn(true);
         when(mFaceManager.hasEnrolledTemplates()).thenReturn(true);
         when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
@@ -274,9 +276,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 */,
@@ -285,7 +285,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));
@@ -296,17 +300,19 @@
                 .thenReturn(new ServiceState());
         when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings);
         when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+        when(mDevicePostureController.getDevicePosture()).thenReturn(DEVICE_POSTURE_UNKNOWN);
 
         mMockitoSession = ExtendedMockito.mockitoSession()
                 .spyStatic(SubscriptionManager.class)
-                .spyStatic(ActivityManager.class)
                 .startMocking();
         ExtendedMockito.doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
                 .when(SubscriptionManager::getDefaultSubscriptionId);
         KeyguardUpdateMonitor.setCurrentUser(mCurrentUserId);
         when(mUserTracker.getUserId()).thenReturn(mCurrentUserId);
-        ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService);
 
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.systemui.R.integer.config_face_auth_supported_posture,
+                DEVICE_POSTURE_UNKNOWN);
         mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig(
                 mContext.getResources(),
                 mGlobalSettings,
@@ -590,9 +596,8 @@
         mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
         mTestableLooper.processAllMessages();
 
-        verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
-                anyInt());
-        verify(mFingerprintManager, never()).detectFingerprint(any(), any(), anyInt());
+        verifyFingerprintAuthenticateCall();
+        verifyFingerprintDetectNeverCalled();
     }
 
     @Test
@@ -602,24 +607,51 @@
         mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
         mTestableLooper.processAllMessages();
 
-        verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyInt(), anyInt());
-        verify(mFingerprintManager, never()).detectFingerprint(any(), any(), anyInt());
+        verifyFingerprintAuthenticateNeverCalled();
+        verifyFingerprintDetectNeverCalled();
     }
 
     @Test
     public void testOnlyDetectFingerprint_whenFingerprintUnlockNotAllowed() {
-        // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks)
-        // will trigger updateBiometricListeningState();
-        clearInvocations(mFingerprintManager);
-        mKeyguardUpdateMonitor.resetBiometricListeningState();
+        givenDetectFingerprintWithClearingFingerprintManagerInvocations();
 
-        when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
-        mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
-        mTestableLooper.processAllMessages();
+        verifyFingerprintAuthenticateNeverCalled();
+        verifyFingerprintDetectCall();
+    }
 
-        verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt());
-        verify(mFingerprintManager).detectFingerprint(any(), any(), anyInt());
+    @Test
+    public void whenDetectFingerprint_biometricDetectCallback() {
+        ArgumentCaptor<FingerprintManager.FingerprintDetectionCallback> fpDetectCallbackCaptor =
+                ArgumentCaptor.forClass(FingerprintManager.FingerprintDetectionCallback.class);
+
+        givenDetectFingerprintWithClearingFingerprintManagerInvocations();
+        verify(mFingerprintManager).detectFingerprint(
+                any(), fpDetectCallbackCaptor.capture(), anyInt());
+        fpDetectCallbackCaptor.getValue().onFingerprintDetected(0, 0, true);
+
+        // THEN verify keyguardUpdateMonitorCallback receives a detect callback
+        // and NO authenticate callbacks
+        verify(mTestCallback).onBiometricDetected(
+                eq(0), eq(BiometricSourceType.FINGERPRINT), eq(true));
+        verify(mTestCallback, never()).onBiometricAuthenticated(
+                anyInt(), any(), anyBoolean());
+    }
+
+    @Test
+    public void whenDetectFace_biometricDetectCallback() {
+        ArgumentCaptor<FaceManager.FaceDetectionCallback> faceDetectCallbackCaptor =
+                ArgumentCaptor.forClass(FaceManager.FaceDetectionCallback.class);
+
+        givenDetectFace();
+        verify(mFaceManager).detectFace(any(), faceDetectCallbackCaptor.capture(), anyInt());
+        faceDetectCallbackCaptor.getValue().onFaceDetected(0, 0, false);
+
+        // THEN verify keyguardUpdateMonitorCallback receives a detect callback
+        // and NO authenticate callbacks
+        verify(mTestCallback).onBiometricDetected(
+                eq(0), eq(BiometricSourceType.FACE), eq(false));
+        verify(mTestCallback, never()).onBiometricAuthenticated(
+                anyInt(), any(), anyBoolean());
     }
 
     @Test
@@ -650,7 +682,7 @@
         strongAuthNotRequired();
 
         // WHEN fingerprint is locked out
-        fingerprintErrorLockedOut();
+        fingerprintErrorTemporaryLockedOut();
 
         // THEN unlocking with face is not allowed
         Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
@@ -673,18 +705,42 @@
         strongAuthNotRequired();
 
         // WHEN fingerprint is locked out
-        fingerprintErrorLockedOut();
+        fingerprintErrorTemporaryLockedOut();
 
-        // THEN unlocking with fingeprint is not allowed
+        // THEN unlocking with fingerprint is not allowed
         Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
                 BiometricSourceType.FINGERPRINT));
     }
 
     @Test
+    public void trustAgentHasTrust() {
+        // WHEN user has trust
+        mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null);
+
+        // THEN user is considered as "having trust" and bouncer can be skipped
+        Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+        Assert.assertTrue(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser()));
+    }
+
+    @Test
+    public void trustAgentHasTrust_fingerprintLockout() {
+        // GIVEN user has trust
+        mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null);
+        Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+
+        // WHEN fingerprint is locked out
+        fingerprintErrorTemporaryLockedOut();
+
+        // THEN user is NOT considered as "having trust" and bouncer cannot be skipped
+        Assert.assertFalse(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+        Assert.assertFalse(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser()));
+    }
+
+    @Test
     public void testTriesToAuthenticate_whenBouncer() {
         setKeyguardBouncerVisibility(true);
 
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateCall();
         verify(mFaceManager).isHardwareDetected();
         verify(mFaceManager).hasEnrolledTemplates(anyInt());
     }
@@ -694,8 +750,7 @@
         mKeyguardUpdateMonitor.sendPrimaryBouncerChanged(
                 /* bouncerIsOrWillBeShowing */ true, /* bouncerFullyShown */ false);
 
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -703,7 +758,8 @@
         keyguardIsVisible();
         mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+
+        verifyFaceAuthenticateCall();
         verify(mUiEventLogger).logWithInstanceIdAndPosition(
                 eq(FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP),
                 eq(0),
@@ -719,8 +775,7 @@
         mTestableLooper.processAllMessages();
 
         keyguardIsVisible();
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -731,26 +786,40 @@
         mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
-    public void faceDetect_whenStrongAuthRequiredAndBypass() {
-        // GIVEN bypass is enabled, face detection is supported and strong auth is required
+    public void nofaceDetect_whenStrongAuthRequiredAndBypassUdfpsSupportedAndFpRunning() {
+        // GIVEN bypass is enabled, face detection is supported
         lockscreenBypassIsAllowed();
         supportsFaceDetection();
-        strongAuthRequiredEncrypted();
         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
+        verifyFaceDetectNeverCalled();
+        verifyFaceAuthenticateNeverCalled();
+
+        // THEN biometric help message sent to callback
+        verify(mTestCallback).onBiometricHelp(
+                eq(BIOMETRIC_HELP_FACE_NOT_AVAILABLE), anyString(), eq(BiometricSourceType.FACE));
+    }
+
+    @Test
+    public void faceDetect_whenStrongAuthRequiredAndBypass() {
+        givenDetectFace();
+
         // FACE detect is triggered, not authenticate
-        verify(mFaceManager).detectFace(any(), any(), anyInt());
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceDetectCall();
+        verifyFaceAuthenticateNeverCalled();
 
         // WHEN bouncer becomes visible
         setKeyguardBouncerVisibility(true);
@@ -758,9 +827,8 @@
 
         // THEN face scanning is not run
         mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
-        verify(mFaceManager, never()).detectFace(any(), any(), anyInt());
+        verifyFaceAuthenticateNeverCalled();
+        verifyFaceDetectNeverCalled();
     }
 
     @Test
@@ -775,9 +843,8 @@
         mTestableLooper.processAllMessages();
 
         // FACE detect and authenticate are NOT triggered
-        verify(mFaceManager, never()).detectFace(any(), any(), anyInt());
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceDetectNeverCalled();
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -815,7 +882,31 @@
         mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
         mKeyguardUpdateMonitor.setAssistantVisible(true);
 
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateCall();
+    }
+
+    @Test
+    public void doesNotTryToAuthenticateWhenKeyguardIsNotShowingButOccluded_whenAssistant() {
+        mKeyguardUpdateMonitor.setKeyguardShowing(false, true);
+        mKeyguardUpdateMonitor.setAssistantVisible(true);
+
+        verifyFaceAuthenticateNeverCalled();
+    }
+
+    @Test
+    public void noFpListeningWhenKeyguardIsOccluded_unlessAlternateBouncerShowing() {
+        // GIVEN device is awake but occluded
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mKeyguardUpdateMonitor.setKeyguardShowing(false, true);
+
+        // THEN fingerprint shouldn't listen
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
+        verifyFingerprintAuthenticateNeverCalled();
+        // WHEN alternate bouncer is shown
+        mKeyguardUpdateMonitor.setAlternateBouncerShowing(true);
+
+        // THEN make sure FP listening begins
+        verifyFingerprintAuthenticateCall();
     }
 
     @Test
@@ -827,7 +918,28 @@
                 KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */,
                 new ArrayList<>());
         keyguardIsVisible();
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateCall();
+    }
+
+    @Test
+    public void faceUnlockDoesNotRunWhenDeviceIsGoingToSleepWithAssistantVisible() {
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
+        mKeyguardUpdateMonitor.setAssistantVisible(true);
+
+        verifyFaceAuthenticateCall();
+        mTestableLooper.processAllMessages();
+        clearInvocations(mFaceManager);
+
+        // Device going to sleep while assistant is visible
+        mKeyguardUpdateMonitor.handleStartedGoingToSleep(0);
+        mKeyguardUpdateMonitor.handleFinishedGoingToSleep(0);
+        mTestableLooper.moveTimeForward(DEFAULT_CANCEL_SIGNAL_TIMEOUT);
+        mTestableLooper.processAllMessages();
+
+        mKeyguardUpdateMonitor.handleKeyguardReset();
+
+        assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isFalse();
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -837,8 +949,7 @@
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, true /* newlyUnlocked */,
                 KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>());
         keyguardIsVisible();
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -850,9 +961,8 @@
         keyguardIsVisible();
         mTestableLooper.processAllMessages();
 
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
-        verify(mFaceManager, never()).detectFace(any(), any(), anyInt());
+        verifyFaceAuthenticateNeverCalled();
+        verifyFaceDetectNeverCalled();
     }
 
     @Test
@@ -861,16 +971,14 @@
                 .onAuthenticationError(FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, "");
 
         // THEN doesn't authenticate immediately
-        verify(mFingerprintManager, never()).authenticate(any(),
-                any(), any(), any(), anyInt(), anyInt(), anyInt());
+        verifyFingerprintAuthenticateNeverCalled();
 
         // WHEN all messages (with delays) are processed
         mTestableLooper.moveTimeForward(HAL_POWER_PRESS_TIMEOUT);
         mTestableLooper.processAllMessages();
 
         // THEN fingerprint manager attempts to authenticate again
-        verify(mFingerprintManager).authenticate(any(),
-                any(), any(), any(), anyInt(), anyInt(), anyInt());
+        verifyFingerprintAuthenticateCall();
     }
 
     @Test
@@ -882,8 +990,7 @@
         setKeyguardBouncerVisibility(true);
         mTestableLooper.processAllMessages();
 
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -956,11 +1063,6 @@
 
     @Test
     public void testBiometricsCleared_whenUserSwitches() throws Exception {
-        final IRemoteCallback reply = new IRemoteCallback.Stub() {
-            @Override
-            public void sendResult(Bundle data) {
-            } // do nothing
-        };
         final BiometricAuthenticated dummyAuthentication =
                 new BiometricAuthenticated(true /* authenticated */, true /* strong */);
         mKeyguardUpdateMonitor.mUserFaceAuthenticated.put(0 /* user */, dummyAuthentication);
@@ -968,18 +1070,13 @@
         assertThat(mKeyguardUpdateMonitor.mUserFingerprintAuthenticated.size()).isEqualTo(1);
         assertThat(mKeyguardUpdateMonitor.mUserFaceAuthenticated.size()).isEqualTo(1);
 
-        mKeyguardUpdateMonitor.handleUserSwitching(10 /* user */, reply);
+        mKeyguardUpdateMonitor.handleUserSwitching(10 /* user */, new CountDownLatch(0));
         assertThat(mKeyguardUpdateMonitor.mUserFingerprintAuthenticated.size()).isEqualTo(0);
         assertThat(mKeyguardUpdateMonitor.mUserFaceAuthenticated.size()).isEqualTo(0);
     }
 
     @Test
     public void testMultiUserJankMonitor_whenUserSwitches() throws Exception {
-        final IRemoteCallback reply = new IRemoteCallback.Stub() {
-            @Override
-            public void sendResult(Bundle data) {
-            } // do nothing
-        };
         mKeyguardUpdateMonitor.handleUserSwitchComplete(10 /* user */);
         verify(mInteractionJankMonitor).end(InteractionJankMonitor.CUJ_USER_SWITCH);
         verify(mLatencyTracker).onActionEnd(LatencyTracker.ACTION_USER_SWITCH);
@@ -1010,9 +1107,8 @@
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
-        verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
-                anyInt());
+        verifyFaceAuthenticateCall();
+        verifyFingerprintAuthenticateCall();
 
         when(mFingerprintManager.getLockoutModeForUser(eq(FINGERPRINT_SENSOR_ID), eq(newUser)))
                 .thenReturn(fingerprintLockoutMode);
@@ -1040,8 +1136,8 @@
         assertThat(mKeyguardUpdateMonitor.isFingerprintLockedOut()).isEqualTo(fpLocked);
         assertThat(mKeyguardUpdateMonitor.isFaceLockedOut()).isEqualTo(faceLocked);
 
-        // Fingerprint should be restarted once its cancelled bc on lockout, the device
-        // can still detectFingerprint (and if it's not locked out, fingerprint can listen)
+        // Fingerprint should be cancelled on lockout if going to lockout state, else
+        // restarted if it's not
         assertThat(mKeyguardUpdateMonitor.mFingerprintRunningState)
                 .isEqualTo(BIOMETRIC_STATE_CANCELLING_RESTARTING);
     }
@@ -1250,7 +1346,7 @@
     }
 
     @Test
-    public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled()
+    public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled()
             throws RemoteException {
         // SFPS supported and enrolled
         final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
@@ -1258,12 +1354,8 @@
         when(mAuthController.getSfpsProps()).thenReturn(props);
         when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
 
-        // WHEN require screen on to auth is disabled, and keyguard is not awake
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0);
-        mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
-
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true);
+        // WHEN require interactive to auth is disabled, and keyguard is not awake
+        when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false);
 
         // Preconditions for sfps auth to run
         keyguardNotGoingAway();
@@ -1279,9 +1371,8 @@
         // THEN we should listen for sfps when screen off, because require screen on is disabled
         assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
 
-        // WHEN require screen on to auth is enabled, and keyguard is not awake
-        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1);
-        mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
+        // WHEN require interactive to auth is enabled, and keyguard is not awake
+        when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true);
 
         // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled
         assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
@@ -1295,6 +1386,61 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
     }
 
+    @Test
+    public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled()
+            throws RemoteException {
+        // GIVEN SFPS supported and enrolled
+        final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+        props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON));
+        when(mAuthController.getSfpsProps()).thenReturn(props);
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+
+        // GIVEN Preconditions for sfps auth to run
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        statusBarShadeIsLocked();
+
+        // WHEN require interactive to auth is enabled & keyguard is going to sleep
+        when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true);
+        deviceGoingToSleep();
+
+        mTestableLooper.processAllMessages();
+
+        // THEN we should NOT listen for sfps because device is going to sleep
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
+    }
+
+    @Test
+    public void listeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthDisabled()
+            throws RemoteException {
+        // GIVEN SFPS supported and enrolled
+        final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+        props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON));
+        when(mAuthController.getSfpsProps()).thenReturn(props);
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+
+        // GIVEN Preconditions for sfps auth to run
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        statusBarShadeIsLocked();
+
+        // WHEN require interactive to auth is disabled & keyguard is going to sleep
+        when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false);
+        deviceGoingToSleep();
+
+        mTestableLooper.processAllMessages();
+
+        // THEN we should listen for sfps because screen on to auth is  disabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+    }
 
     private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal(
             @FingerprintSensorProperties.SensorType int sensorType) {
@@ -1409,10 +1555,7 @@
     public void testFaceDoesNotAuth_afterPinAttempt() {
         mTestableLooper.processAllMessages();
         mKeyguardUpdateMonitor.setCredentialAttempted();
-        verify(mFingerprintManager, never()).authenticate(any(), any(), any(),
-                any(), anyInt());
-        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
-                anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -1455,7 +1598,7 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
 
         // Fingerprint is locked out.
-        fingerprintErrorLockedOut();
+        fingerprintErrorTemporaryLockedOut();
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
     }
@@ -1610,7 +1753,7 @@
     }
 
     @Test
-    public void testShouldListenForFace_whenOccludingAppRequestsFaceAuth_returnsTrue()
+    public void shouldListenForFace_secureCameraLaunchedButAlternateBouncerIsLaunched_returnsTrue()
             throws RemoteException {
         // Face auth should run when the following is true.
         keyguardNotGoingAway();
@@ -1626,7 +1769,7 @@
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
 
-        occludingAppRequestsFaceAuth();
+        alternateBouncerVisible();
         mTestableLooper.processAllMessages();
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
@@ -1716,7 +1859,7 @@
     }
 
     @Test
-    public void testShouldListenForFace_whenUdfpsBouncerIsShowing_returnsTrue()
+    public void testShouldListenForFace_whenAlternateBouncerIsShowing_returnsTrue()
             throws RemoteException {
         // Preconditions for face auth to run
         keyguardNotGoingAway();
@@ -1728,13 +1871,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();
@@ -1744,7 +1887,7 @@
         biometricsEnabledForCurrentUser();
         userNotCurrentlySwitching();
         deviceNotGoingToSleep();
-        mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true);
+        alternateBouncerVisible();
         mTestableLooper.processAllMessages();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
 
@@ -1754,6 +1897,10 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
     }
 
+    private void alternateBouncerVisible() {
+        mKeyguardUpdateMonitor.setAlternateBouncerShowing(true);
+    }
+
     @Test
     public void testShouldListenForFace_whenFaceIsLockedOut_returnsTrue()
             throws RemoteException {
@@ -1764,7 +1911,7 @@
         biometricsNotDisabledThroughDevicePolicyManager();
         biometricsEnabledForCurrentUser();
         userNotCurrentlySwitching();
-        mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true);
+        mKeyguardUpdateMonitor.setAlternateBouncerShowing(true);
         mTestableLooper.processAllMessages();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
 
@@ -1783,9 +1930,8 @@
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
-        verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
-                anyInt());
+        verifyFaceAuthenticateCall();
+        verifyFingerprintAuthenticateCall();
 
         mKeyguardUpdateMonitor.onFaceAuthenticated(0, false);
         // Make sure keyguard is going away after face auth attempt, and that it calls
@@ -1807,34 +1953,11 @@
     }
 
     @Test
-    public void testFingerAcquired_wakesUpPowerManager() {
-        cleanupKeyguardUpdateMonitor();
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.kg_wake_on_acquire_start, true);
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
-        fingerprintAcquireStart();
-
-        verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
-    }
-
-    @Test
-    public void testFingerAcquired_doesNotWakeUpPowerManager() {
-        cleanupKeyguardUpdateMonitor();
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.kg_wake_on_acquire_start, false);
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
-        fingerprintAcquireStart();
-
-        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
-    }
-
-    @Test
     public void testDreamingStopped_faceDoesNotRun() {
         mKeyguardUpdateMonitor.dispatchDreamingStopped();
         mTestableLooper.processAllMessages();
 
-        verify(mFaceManager, never()).authenticate(
-                any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
     }
 
     @Test
@@ -1847,15 +1970,14 @@
         mTestableLooper.processAllMessages();
 
         // THEN face auth isn't triggered
-        verify(mFaceManager, never()).authenticate(
-                any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateNeverCalled();
 
         // WHEN device wakes up from the power button
         mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
 
         // THEN face auth is triggered
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateCall();
     }
 
 
@@ -2026,7 +2148,7 @@
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateCall();
         verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
                 anyInt());
 
@@ -2059,7 +2181,7 @@
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
-        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verifyFaceAuthenticateCall();
 
         final CancellationSignal faceCancel = spy(mKeyguardUpdateMonitor.mFaceCancelSignal);
         mKeyguardUpdateMonitor.mFaceCancelSignal = faceCancel;
@@ -2106,7 +2228,7 @@
 
         // GIVEN active unlock triggers on biometric failures
         when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
                 .thenReturn(true);
 
         // WHEN fingerprint fails
@@ -2129,7 +2251,7 @@
 
         // GIVEN active unlock triggers on biometric failures
         when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
                 .thenReturn(true);
 
         // WHEN face fails & bypass is not allowed
@@ -2153,7 +2275,7 @@
 
         // GIVEN active unlock triggers on biometric failures
         when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
                 .thenReturn(true);
 
         // WHEN face fails & bypass is not allowed
@@ -2175,7 +2297,7 @@
 
         // GIVEN active unlock triggers on biometric failures
         when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
-                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
                 .thenReturn(true);
 
         // WHEN face fails & on the bouncer
@@ -2187,6 +2309,162 @@
                 eq(true));
     }
 
+    @Test
+    public void testShouldListenForFace_withAuthSupportPostureConfig_returnsTrue()
+            throws RemoteException {
+        mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED;
+        keyguardNotGoingAway();
+        bouncerFullyVisibleAndNotGoingToSleep();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        supportsFaceDetection();
+
+        deviceInPostureStateOpened();
+        mTestableLooper.processAllMessages();
+        // Should not listen for face when posture state in DEVICE_POSTURE_OPENED
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
+
+        deviceInPostureStateClosed();
+        mTestableLooper.processAllMessages();
+        // Should listen for face when posture state in DEVICE_POSTURE_CLOSED
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+    }
+
+    @Test
+    public void testShouldListenForFace_withoutAuthSupportPostureConfig_returnsTrue()
+            throws RemoteException {
+        mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_UNKNOWN;
+        keyguardNotGoingAway();
+        bouncerFullyVisibleAndNotGoingToSleep();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        supportsFaceDetection();
+
+        deviceInPostureStateClosed();
+        mTestableLooper.processAllMessages();
+        // Whether device in any posture state, always listen for face
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+
+        deviceInPostureStateOpened();
+        mTestableLooper.processAllMessages();
+        // Whether device in any posture state, always listen for face
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+    }
+
+    @Test
+    public void unfoldWakeup_requestActiveUnlock_forceDismissKeyguard()
+            throws RemoteException {
+        // GIVEN shouldTriggerActiveUnlock
+        keyguardIsVisible();
+        when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true);
+
+        // GIVEN active unlock triggers on wakeup
+        when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
+                .thenReturn(true);
+
+        // GIVEN an unfold should force dismiss the keyguard
+        when(mActiveUnlockConfig.shouldWakeupForceDismissKeyguard(
+                PowerManager.WAKE_REASON_UNFOLD_DEVICE)).thenReturn(true);
+
+        // WHEN device wakes up from an unfold
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNFOLD_DEVICE);
+        mTestableLooper.processAllMessages();
+
+        // THEN request unlock with a keyguard dismissal
+        verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()),
+                eq(true));
+    }
+
+    @Test
+    public void unfoldWakeup_requestActiveUnlock_noDismissKeyguard()
+            throws RemoteException {
+        // GIVEN shouldTriggerActiveUnlock on wake from UNFOLD_DEVICE
+        keyguardIsVisible();
+        when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true);
+
+        // GIVEN active unlock triggers on wakeup
+        when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
+                .thenReturn(true);
+
+        // GIVEN an unfold should NOT force dismiss the keyguard
+        when(mActiveUnlockConfig.shouldWakeupForceDismissKeyguard(
+                PowerManager.WAKE_REASON_UNFOLD_DEVICE)).thenReturn(false);
+
+        // WHEN device wakes up from an unfold
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNFOLD_DEVICE);
+        mTestableLooper.processAllMessages();
+
+        // THEN request unlock WITHOUT a keyguard dismissal
+        verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()),
+                eq(false));
+    }
+
+    @Test
+    public void detectFingerprint_onTemporaryLockoutReset_authenticateFingerprint() {
+        ArgumentCaptor<FingerprintManager.LockoutResetCallback> fpLockoutResetCallbackCaptor =
+                ArgumentCaptor.forClass(FingerprintManager.LockoutResetCallback.class);
+        verify(mFingerprintManager).addLockoutResetCallback(fpLockoutResetCallbackCaptor.capture());
+
+        // GIVEN device is locked out
+        fingerprintErrorTemporaryLockedOut();
+
+        // GIVEN FP detection is running
+        givenDetectFingerprintWithClearingFingerprintManagerInvocations();
+        verifyFingerprintDetectCall();
+        verifyFingerprintAuthenticateNeverCalled();
+
+        // WHEN temporary lockout resets
+        fpLockoutResetCallbackCaptor.getValue().onLockoutReset(0);
+        mTestableLooper.processAllMessages();
+
+        // THEN fingerprint detect state should cancel & then restart (for authenticate call)
+        assertThat(mKeyguardUpdateMonitor.mFingerprintRunningState)
+                .isEqualTo(BIOMETRIC_STATE_CANCELLING_RESTARTING);
+    }
+
+    private void verifyFingerprintAuthenticateNeverCalled() {
+        verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
+                anyInt(), anyInt());
+    }
+
+    private void verifyFingerprintAuthenticateCall() {
+        verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
+                anyInt());
+    }
+
+    private void verifyFingerprintDetectNeverCalled() {
+        verify(mFingerprintManager, never()).detectFingerprint(any(), any(), anyInt());
+    }
+
+    private void verifyFingerprintDetectCall() {
+        verify(mFingerprintManager).detectFingerprint(any(), any(), anyInt());
+    }
+
+    private void verifyFaceAuthenticateNeverCalled() {
+        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
+                anyBoolean());
+    }
+
+    private void verifyFaceAuthenticateCall() {
+        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+    }
+
+    private void verifyFaceDetectNeverCalled() {
+        verify(mFaceManager, never()).detectFace(any(), any(), anyInt());
+    }
+
+    private void verifyFaceDetectCall() {
+        verify(mFaceManager).detectFace(any(), any(), anyInt());
+    }
+
     private void userDeviceLockDown() {
         when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
         when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId))
@@ -2256,14 +2534,17 @@
         mKeyguardUpdateMonitor.setSwitchingUser(true);
     }
 
-    private void fingerprintErrorLockedOut() {
+    private void fingerprintErrorTemporaryLockedOut() {
         mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
                 .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT, "Fingerprint locked out");
     }
 
-    private void fingerprintAcquireStart() {
-        mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
-                .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START);
+    private void deviceInPostureStateOpened() {
+        mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_OPENED);
+    }
+
+    private void deviceInPostureStateClosed() {
+        mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_CLOSED);
     }
 
     private void successfulFingerprintAuth() {
@@ -2314,6 +2595,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);
@@ -2368,6 +2654,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,
@@ -2382,6 +2674,30 @@
         receiver.setPendingResult(pendingResult);
     }
 
+    private void givenDetectFingerprintWithClearingFingerprintManagerInvocations() {
+        // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks)
+        // will trigger updateBiometricListeningState();
+        clearInvocations(mFingerprintManager);
+        mKeyguardUpdateMonitor.resetBiometricListeningState();
+
+        when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
+        mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
+        mTestableLooper.processAllMessages();
+    }
+
+    private void givenDetectFace() {
+        // 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);
+        mTestableLooper.processAllMessages();
+    }
+
     private Intent putPhoneInfo(Intent intent, Bundle data, Boolean simInited) {
         int subscription = simInited
                 ? 1/* mock subid=1 */ : SubscriptionManager.PLACEHOLDER_SUBSCRIPTION_ID_BASE;
@@ -2404,10 +2720,11 @@
                     mAuthController, mTelephonyListenerManager,
                     mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
                     mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
-                    mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
+                    mTrustManager, mSubscriptionManager, mUserManager,
                     mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
                     mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager,
-                    mFaceWakeUpTriggersConfig);
+                    mFaceWakeUpTriggersConfig, mDevicePostureController,
+                    Optional.of(mInteractiveToAuthProvider));
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index e4c41a7..456702b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -43,12 +43,14 @@
 import com.android.systemui.doze.util.BurnInHelperKt;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -91,6 +93,7 @@
     protected @Mock AuthRippleController mAuthRippleController;
     protected @Mock FeatureFlags mFeatureFlags;
     protected @Mock KeyguardTransitionRepository mTransitionRepository;
+    protected @Mock CommandQueue mCommandQueue;
     protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
 
     protected LockIconViewController mUnderTest;
@@ -157,7 +160,12 @@
                 mAuthRippleController,
                 mResources,
                 new KeyguardTransitionInteractor(mTransitionRepository),
-                new KeyguardInteractor(new FakeKeyguardRepository()),
+                new KeyguardInteractor(
+                        new FakeKeyguardRepository(),
+                        mCommandQueue,
+                        mFeatureFlags,
+                        new FakeKeyguardBouncerRepository()
+                ),
                 mFeatureFlags
         );
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
index b69491e..b7d0059 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -284,4 +284,26 @@
         // THEN the lock icon is shown
         verify(mLockIconView).setContentDescription(LOCKED_LABEL);
     }
+
+    @Test
+    public void lockIconShows_afterUnlockStateChanges() {
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureKeyguardStateCallback();
+        captureKeyguardUpdateMonitorCallback();
+
+        // GIVEN user has unlocked with a biometric auth (ie: face auth)
+        // and biometric running state changes
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+                BiometricSourceType.FACE);
+        reset(mLockIconView);
+
+        // WHEN the unlocked state changes
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(false);
+        mKeyguardStateCallback.onUnlockedChanged();
+
+        // THEN the lock icon is shown
+        verify(mLockIconView).setContentDescription(LOCKED_LABEL);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt
new file mode 100644
index 0000000..9fcb9c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.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.keyguard
+
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NumPadAnimatorTest : SysuiTestCase() {
+    @Mock lateinit var background: GradientDrawable
+    @Mock lateinit var buttonImage: Drawable
+    private lateinit var underTest: NumPadAnimator
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = NumPadAnimator(context, background, 0, buttonImage)
+    }
+
+    @Test
+    fun testOnLayout() {
+        underTest.onLayout(100)
+        verify(background).cornerRadius = 50f
+        reset(background)
+        underTest.onLayout(100)
+        verify(background, never()).cornerRadius = anyFloat()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
index e9a2789..9fe32f1 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
@@ -32,6 +32,7 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import java.util.Optional
@@ -83,6 +84,33 @@
     }
 
     @Test
+    fun testTasksReady_onScreenTurningOnAndTurnedOnEventsCalledTogether_callsDrawnCallback() {
+        screenOnCoordinator.onScreenTurningOn(runnable)
+        screenOnCoordinator.onScreenTurnedOn()
+
+        onUnfoldOverlayReady()
+        onFoldAodReady()
+        waitHandlerIdle(testHandler)
+
+        // Should be called when both unfold overlay and keyguard drawn ready
+        verify(runnable).run()
+    }
+
+    @Test
+    fun testTasksReady_onScreenTurnedOnAndTurnedOffBeforeCompletion_doesNotCallDrawnCallback() {
+        screenOnCoordinator.onScreenTurningOn(runnable)
+        screenOnCoordinator.onScreenTurnedOn()
+        screenOnCoordinator.onScreenTurnedOff()
+
+        onUnfoldOverlayReady()
+        onFoldAodReady()
+        waitHandlerIdle(testHandler)
+
+        // Should not be called because this screen turning on call is not valid anymore
+        verify(runnable, never()).run()
+    }
+
+    @Test
     fun testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback() {
         // Recreate with empty unfoldComponent
         screenOnCoordinator = ScreenOnCoordinator(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
index 81d0034..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,8 +148,8 @@
         )
 
         // Act
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
-        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+        setFlagMock(true)
+        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
 
         // Assert
         verify(mockPackageManager, times(2)).setComponentEnabledSetting(
@@ -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,8 +175,8 @@
         )
 
         // Act
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
-        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+        setFlagMock(false)
+        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
 
         // Assert
         verify(mockPackageManager, times(2)).setComponentEnabledSetting(
@@ -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,14 +198,18 @@
         clearInvocations(mockPackageManager)
 
         // Act
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
-        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id + 1))
+        flagListener.value.onFlagChanged(TestFlagEvent("other flag"))
 
         // Assert
         verifyZeroInteractions(mockPackageManager)
     }
 
-    private class TestFlagEvent(override val flagId: Int) : FlagListenable.FlagEvent {
+    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/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 0627fc6..4cf5a4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -21,6 +21,7 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.systemui.dump.LogBufferHelperKt.logcatLogBuffer;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -33,8 +34,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -88,7 +91,9 @@
 import com.android.systemui.decor.PrivacyDotCornerDecorProviderImpl;
 import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
 import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
 import com.android.systemui.tuner.TunerService;
@@ -101,8 +106,12 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -116,7 +125,8 @@
     private WindowManager mWindowManager;
     private DisplayManager mDisplayManager;
     private SecureSettings mSecureSettings;
-    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+    private FakeExecutor mExecutor;
+    private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private FakeThreadFactory mThreadFactory;
     private ArrayList<DecorProvider> mPrivacyDecorProviders;
     private ArrayList<DecorProvider> mFaceScanningProviders;
@@ -157,6 +167,8 @@
     private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener;
     @Mock
     private CutoutDecorProviderFactory mCutoutFactory;
+    @Captor
+    private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback;
     private List<DecorProvider> mMockCutoutList;
 
     @Before
@@ -165,6 +177,7 @@
 
         Handler mainHandler = new Handler(TestableLooper.get(this).getLooper());
         mSecureSettings = new FakeSettings();
+        mExecutor = new FakeExecutor(new FakeSystemClock());
         mThreadFactory = new FakeThreadFactory(mExecutor);
         mThreadFactory.setHandler(mainHandler);
 
@@ -217,11 +230,14 @@
                 mAuthController,
                 mStatusBarStateController,
                 mKeyguardUpdateMonitor,
-                mExecutor));
+                mExecutor,
+                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
 
         mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
-                mTunerService, mUserTracker, mDotViewController, mThreadFactory,
-                mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) {
+                mTunerService, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
+                mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
+                mAuthController) {
             @Override
             public void start() {
                 super.start();
@@ -1159,6 +1175,44 @@
     }
 
     @Test
+    public void faceSensorLocationChangesReloadsFaceScanningOverlay() {
+        mFaceScanningProviders = new ArrayList<>();
+        mFaceScanningProviders.add(mFaceScanningDecorProvider);
+        when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
+        when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
+        ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
+                mSecureSettings, mTunerService, mUserTracker, mDisplayTracker, mDotViewController,
+                mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
+        screenDecorations.start();
+        verify(mAuthController).addCallback(mAuthControllerCallback.capture());
+        when(mContext.getDisplay()).thenReturn(mDisplay);
+        when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                DisplayInfo displayInfo = invocation.getArgument(0);
+                int modeId = 1;
+                displayInfo.modeId = modeId;
+                displayInfo.supportedModes = new Display.Mode[]{new Display.Mode(modeId, 1024, 1024,
+                        90)};
+                return false;
+            }
+        });
+        mExecutor.runAllReady();
+        clearInvocations(mFaceScanningDecorProvider);
+
+        AuthController.Callback callback = mAuthControllerCallback.getValue();
+        callback.onFaceSensorLocationChanged();
+        mExecutor.runAllReady();
+
+        verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                any());
+    }
+
+    @Test
     public void testPrivacyDotShowingListenerWorkWellWithNullParameter() {
         mPrivacyDotShowingListener.onPrivacyDotShown(null);
         mPrivacyDotShowingListener.onPrivacyDotHidden(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
index 7aa4763..4a5c1be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
@@ -22,14 +22,16 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import android.os.UserHandle;
+import android.app.ActivityManager;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -42,11 +44,14 @@
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
 public class AccessibilityButtonModeObserverTest extends SysuiTestCase {
+    private static final int MY_USER_ID = ActivityManager.getCurrentUser();
 
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private AccessibilityButtonModeObserver.ModeChangedListener mListener;
 
     private AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
@@ -56,10 +61,12 @@
 
     @Before
     public void setUp() {
+        when(mUserTracker.getUserId()).thenReturn(MY_USER_ID);
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
-        mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext);
+                Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, MY_USER_ID);
+        mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext,
+                mUserTracker);
     }
 
     @Test
@@ -67,7 +74,7 @@
         mAccessibilityButtonModeObserver.addListener(mListener);
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, TEST_A11Y_BTN_MODE_VALUE,
-                UserHandle.USER_CURRENT);
+                MY_USER_ID);
 
         mAccessibilityButtonModeObserver.mContentObserver.onChange(false);
 
@@ -80,7 +87,7 @@
         mAccessibilityButtonModeObserver.removeListener(mListener);
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, TEST_A11Y_BTN_MODE_VALUE,
-                UserHandle.USER_CURRENT);
+                MY_USER_ID);
 
         mAccessibilityButtonModeObserver.mContentObserver.onChange(false);
 
@@ -91,7 +98,7 @@
     public void getCurrentAccessibilityButtonMode_expectedValue() {
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, TEST_A11Y_BTN_MODE_VALUE,
-                UserHandle.USER_CURRENT);
+                MY_USER_ID);
 
         final int actualValue =
                 mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
index 4145437..a5a7a4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
@@ -21,14 +21,16 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import android.os.UserHandle;
+import android.app.ActivityManager;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -42,11 +44,14 @@
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
 public class AccessibilityButtonTargetsObserverTest extends SysuiTestCase {
+    private static final int MY_USER_ID = ActivityManager.getCurrentUser();
 
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private AccessibilityButtonTargetsObserver.TargetsChangedListener mListener;
 
     private AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
@@ -55,7 +60,9 @@
 
     @Before
     public void setUp() {
-        mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext);
+        when(mUserTracker.getUserId()).thenReturn(MY_USER_ID);
+        mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext,
+                mUserTracker);
     }
 
     @Test
@@ -63,7 +70,7 @@
         mAccessibilityButtonTargetsObserver.addListener(mListener);
         Settings.Secure.putStringForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                UserHandle.USER_CURRENT);
+                MY_USER_ID);
 
         mAccessibilityButtonTargetsObserver.mContentObserver.onChange(false);
 
@@ -76,7 +83,7 @@
         mAccessibilityButtonTargetsObserver.removeListener(mListener);
         Settings.Secure.putStringForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                UserHandle.USER_CURRENT);
+                MY_USER_ID);
 
         mAccessibilityButtonTargetsObserver.mContentObserver.onChange(false);
 
@@ -87,7 +94,7 @@
     public void getCurrentAccessibilityButtonTargets_expectedValue() {
         Settings.Secure.putStringForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                UserHandle.USER_CURRENT);
+                MY_USER_ID);
 
         final String actualValue =
                 mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
index 58b4af4..da419d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
@@ -39,6 +39,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.CommandQueue;
 
 import org.junit.Before;
@@ -76,6 +77,7 @@
 
     private IWindowMagnificationConnection mIWindowMagnificationConnection;
     private WindowMagnification mWindowMagnification;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Before
     public void setUp() throws Exception {
@@ -88,7 +90,7 @@
                 any(IWindowMagnificationConnection.class));
         mWindowMagnification = new WindowMagnification(getContext(),
                 getContext().getMainThreadHandler(), mCommandQueue,
-                mModeSwitchesController, mSysUiState, mOverviewProxyService);
+                mModeSwitchesController, mSysUiState, mOverviewProxyService, mDisplayTracker);
         mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class));
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
index 41fd2b3..9c601a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
@@ -18,18 +18,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.ActivityManager;
 import android.content.Context;
-import android.os.UserHandle;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 /** Test for {@link SecureSettingsContentObserver}. */
 @RunWith(AndroidTestingRunner.class)
@@ -40,7 +42,9 @@
 
     @Before
     public void setUpObserver() {
-        mTestObserver = new FakeSecureSettingsContentObserver(mContext,
+        UserTracker userTracker = Mockito.mock(UserTracker.class);
+        Mockito.when(userTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+        mTestObserver = new FakeSecureSettingsContentObserver(mContext, userTracker,
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
     }
 
@@ -57,7 +61,7 @@
     @Test
     public void checkValue() {
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 1, UserHandle.USER_CURRENT);
+                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 1, ActivityManager.getCurrentUser());
 
         assertThat(mTestObserver.getSettingsValue()).isEqualTo("1");
     }
@@ -66,9 +70,9 @@
     private static class FakeSecureSettingsContentObserver extends
             SecureSettingsContentObserver<Object> {
 
-        protected FakeSecureSettingsContentObserver(Context context,
+        protected FakeSecureSettingsContentObserver(Context context, UserTracker userTracker,
                 String secureSettingsKey) {
-            super(context, secureSettingsKey);
+            super(context, userTracker, secureSettingsKey);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index b7d3459..f4505f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -32,8 +32,11 @@
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.AdditionalAnswers.returnsSecondArg;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
@@ -75,7 +78,9 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.util.leak.ReferenceTestUtils;
+import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.utils.os.FakeHandler;
 
 import org.junit.After;
@@ -111,14 +116,17 @@
     IRemoteMagnificationAnimationCallback mAnimationCallback;
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+    @Mock
+    private SecureSettings mSecureSettings;
 
     private Handler mHandler;
     private TestableWindowManager mWindowManager;
-    private SysUiState mSysUiState = new SysUiState();
+    private SysUiState mSysUiState;
     private Resources mResources;
     private WindowMagnificationAnimationController mWindowMagnificationAnimationController;
     private WindowMagnificationController mWindowMagnificationController;
     private Instrumentation mInstrumentation;
+    private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0);
 
     @Before
@@ -137,7 +145,12 @@
             return null;
         }).when(mSfVsyncFrameProvider).postFrameCallback(
                 any(FrameCallback.class));
+        mSysUiState = new SysUiState(mDisplayTracker);
         mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class));
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then(
+                returnsSecondArg());
+        when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then(
+                returnsSecondArg());
 
         mResources = getContext().getOrCreateTestableResources().getResources();
         mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
index ccf2f8b..f75dc03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
@@ -45,6 +45,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.CommandQueue;
 
 import org.junit.Before;
@@ -74,6 +75,8 @@
     private CommandQueue mCommandQueue;
     private WindowMagnification mWindowMagnification;
     private OverviewProxyListener mOverviewProxyListener;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -87,10 +90,10 @@
 
         when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
 
-        mCommandQueue = new CommandQueue(getContext());
+        mCommandQueue = new CommandQueue(getContext(), new FakeDisplayTracker(getContext()));
         mWindowMagnification = new WindowMagnification(getContext(),
                 getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController,
-                mSysUiState, mOverviewProxyService);
+                mSysUiState, mOverviewProxyService, mDisplayTracker);
         mWindowMagnification.start();
 
         final ArgumentCaptor<OverviewProxyListener> listenerArgumentCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 8ca17b9..f34a36f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.os.UserHandle;
@@ -40,6 +41,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.After;
 import org.junit.Before;
@@ -48,6 +50,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -71,8 +75,12 @@
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardCallbackCaptor;
     private KeyguardUpdateMonitorCallback mKeyguardCallback;
 
+    @Mock
+    private SecureSettings mSecureSettings;
+
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mContextWrapper = new ContextWrapper(mContext) {
             @Override
             public Context createContextAsUser(UserHandle user, int flags) {
@@ -128,7 +136,7 @@
     public void onKeyguardVisibilityChanged_showing_destroyWidget() {
         enableAccessibilityFloatingMenuConfig();
         mController = setUpController();
-        mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper);
+        mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
         captureKeyguardUpdateMonitorCallback();
         mKeyguardCallback.onUserUnlocked();
 
@@ -154,7 +162,7 @@
         final int fakeUserId = 1;
         enableAccessibilityFloatingMenuConfig();
         mController = setUpController();
-        mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper);
+        mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
         captureKeyguardUpdateMonitorCallback();
 
         mKeyguardCallback.onUserSwitching(fakeUserId);
@@ -167,7 +175,7 @@
         final int fakeUserId = 1;
         enableAccessibilityFloatingMenuConfig();
         mController = setUpController();
-        mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper);
+        mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
         captureKeyguardUpdateMonitorCallback();
         mKeyguardCallback.onUserUnlocked();
         mKeyguardCallback.onKeyguardVisibilityChanged(true);
@@ -197,7 +205,7 @@
     public void onAccessibilityButtonModeChanged_floatingModeAndHasButtonTargets_showWidget() {
         Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                UserHandle.USER_CURRENT);
+                ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
@@ -208,7 +216,7 @@
     @Test
     public void onAccessibilityButtonModeChanged_floatingModeAndNoButtonTargets_destroyWidget() {
         Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
@@ -220,7 +228,7 @@
     public void onAccessibilityButtonModeChanged_navBarModeAndHasButtonTargets_destroyWidget() {
         Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                UserHandle.USER_CURRENT);
+                ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
@@ -231,7 +239,7 @@
     @Test
     public void onAccessibilityButtonModeChanged_navBarModeAndNoButtonTargets_destroyWidget() {
         Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
@@ -243,7 +251,7 @@
     public void onAccessibilityButtonTargetsChanged_floatingModeAndHasButtonTargets_showWidget() {
         Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                UserHandle.USER_CURRENT);
+                ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -255,7 +263,7 @@
     public void onAccessibilityButtonTargetsChanged_floatingModeAndNoButtonTargets_destroyWidget() {
         Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                UserHandle.USER_CURRENT);
+                ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged("");
@@ -267,7 +275,7 @@
     public void onAccessibilityButtonTargetsChanged_navBarModeAndHasButtonTargets_destroyWidget() {
         Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
-                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
+                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -279,7 +287,7 @@
     public void onAccessibilityButtonTargetsChanged_navBarModeAndNoButtonTargets_destroyWidget() {
         Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
-                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
+                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser());
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged("");
@@ -293,7 +301,7 @@
         mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
         final AccessibilityFloatingMenuController controller =
                 new AccessibilityFloatingMenuController(mContextWrapper, mTargetsObserver,
-                        mModeObserver, mKeyguardUpdateMonitor);
+                        mModeObserver, mKeyguardUpdateMonitor, mSecureSettings);
         controller.init();
 
         return controller;
@@ -302,10 +310,10 @@
     private void enableAccessibilityFloatingMenuConfig() {
         Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                UserHandle.USER_CURRENT);
+                ActivityManager.getCurrentUser());
         Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                UserHandle.USER_CURRENT);
+                ActivityManager.getCurrentUser());
     }
 
     private void captureKeyguardUpdateMonitorCallback() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
index 558261b..04345fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
@@ -31,6 +31,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.After;
 import org.junit.Before;
@@ -55,6 +56,8 @@
 
     @Mock
     private AccessibilityManager mAccessibilityManager;
+    @Mock
+    private SecureSettings mSecureSettings;
 
     private AccessibilityFloatingMenuView mMenuView;
     private AccessibilityFloatingMenu mMenu;
@@ -69,7 +72,7 @@
 
         final Position position = new Position(0, 0);
         mMenuView = new AccessibilityFloatingMenuView(mContext, position);
-        mMenu = new AccessibilityFloatingMenu(mContext, mMenuView);
+        mMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings, mMenuView);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
new file mode 100644
index 0000000..ca6f426
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.accessibility.fontscaling
+
+import android.os.Handler
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.ViewGroup
+import android.widget.SeekBar
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.SystemSettings
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [FontScalingDialog]. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class FontScalingDialogTest : SysuiTestCase() {
+    private lateinit var fontScalingDialog: FontScalingDialog
+    private lateinit var systemSettings: SystemSettings
+    private val fontSizeValueArray: Array<String> =
+        mContext
+            .getResources()
+            .getStringArray(com.android.settingslib.R.array.entryvalues_font_size)
+
+    @Before
+    fun setUp() {
+        val mainHandler = Handler(TestableLooper.get(this).getLooper())
+        systemSettings = FakeSettings()
+        fontScalingDialog = FontScalingDialog(mContext, systemSettings as FakeSettings)
+    }
+
+    @Test
+    fun showTheDialog_seekbarIsShowingCorrectProgress() {
+        fontScalingDialog.show()
+
+        val seekBar: SeekBar = fontScalingDialog.findViewById<SeekBar>(R.id.seekbar)!!
+        val progress: Int = seekBar.getProgress()
+        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def = */ 1.0f)
+
+        assertThat(currentScale).isEqualTo(fontSizeValueArray[progress].toFloat())
+
+        fontScalingDialog.dismiss()
+    }
+
+    @Test
+    fun progressIsZero_clickIconEnd_seekBarProgressIncreaseOne_fontSizeScaled() {
+        fontScalingDialog.show()
+
+        val iconEndFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_end_frame)!!
+        val seekBarWithIconButtonsView: SeekBarWithIconButtonsView =
+            fontScalingDialog.findViewById(R.id.font_scaling_slider)!!
+        val seekBar: SeekBar = fontScalingDialog.findViewById(R.id.seekbar)!!
+
+        seekBarWithIconButtonsView.setProgress(0)
+
+        iconEndFrame.performClick()
+
+        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def = */ 1.0f)
+        assertThat(seekBar.getProgress()).isEqualTo(1)
+        assertThat(currentScale).isEqualTo(fontSizeValueArray[1].toFloat())
+
+        fontScalingDialog.dismiss()
+    }
+
+    @Test
+    fun progressIsMax_clickIconStart_seekBarProgressDecreaseOne_fontSizeScaled() {
+        fontScalingDialog.show()
+
+        val iconStartFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_start_frame)!!
+        val seekBarWithIconButtonsView: SeekBarWithIconButtonsView =
+            fontScalingDialog.findViewById(R.id.font_scaling_slider)!!
+        val seekBar: SeekBar = fontScalingDialog.findViewById(R.id.seekbar)!!
+
+        seekBarWithIconButtonsView.setProgress(fontSizeValueArray.size - 1)
+
+        iconStartFrame.performClick()
+
+        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def = */ 1.0f)
+        assertThat(seekBar.getProgress()).isEqualTo(fontSizeValueArray.size - 2)
+        assertThat(currentScale)
+            .isEqualTo(fontSizeValueArray[fontSizeValueArray.size - 2].toFloat())
+
+        fontScalingDialog.dismiss()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index a61cd23..578e1d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -15,6 +15,7 @@
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -26,6 +27,7 @@
 import junit.framework.AssertionFailedError
 import kotlin.concurrent.thread
 import org.junit.After
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -195,6 +197,13 @@
         verify(controller).onLaunchAnimationStart(anyBoolean())
     }
 
+    @Test
+    fun creatingControllerFromNormalViewThrows() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ActivityLaunchAnimator.Controller.fromView(FrameLayout(mContext))
+        }
+    }
+
     private fun fakeWindow(): RemoteAnimationTarget {
         val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
         val taskInfo = ActivityManager.RunningTaskInfo()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index cac4a0e..316de59 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -26,6 +26,7 @@
 import junit.framework.Assert.assertTrue
 import org.junit.After
 import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -260,6 +261,19 @@
         assertThat(touchSurface.visibility).isEqualTo(View.GONE)
     }
 
+    @Test
+    fun creatingControllerFromNormalViewThrows() {
+        assertThrows(IllegalArgumentException::class.java) {
+            DialogLaunchAnimator.Controller.fromView(FrameLayout(mContext))
+        }
+    }
+
+    @Test
+    fun showFromDialogDoesNotCrashWhenShownFromRandomDialog() {
+        val dialog = createDialogAndShowFromDialog(animateFrom = TestDialog(context))
+        dialog.dismiss()
+    }
+
     private fun createAndShowDialog(
         animator: DialogLaunchAnimator = dialogLaunchAnimator,
     ): TestDialog {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
index 3696ec5..0798d73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
@@ -16,58 +16,34 @@
 
 package com.android.systemui.animation
 
-import android.graphics.drawable.Drawable
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewParent
+import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
-import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.SysuiTestCase
-import org.junit.Before
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
-    @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock lateinit var view: View
-    @Mock lateinit var rootView: ViewGroup
-    @Mock lateinit var viewParent: ViewParent
-    @Mock lateinit var drawable: Drawable
-    lateinit var controller: GhostedViewLaunchAnimatorController
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        whenever(view.rootView).thenReturn(rootView)
-        whenever(view.background).thenReturn(drawable)
-        whenever(view.height).thenReturn(0)
-        whenever(view.width).thenReturn(0)
-        whenever(view.parent).thenReturn(viewParent)
-        whenever(view.visibility).thenReturn(View.VISIBLE)
-        whenever(view.invalidate()).then { /* NO-OP */ }
-        whenever(view.getLocationOnScreen(any())).then { /* NO-OP */ }
-        whenever(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
-        whenever(interactionJankMonitor.end(anyInt())).thenReturn(true)
-        controller = GhostedViewLaunchAnimatorController(view, 0, interactionJankMonitor)
-    }
-
     @Test
     fun animatingOrphanViewDoesNotCrash() {
         val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
 
+        val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext))
         controller.onIntentStarted(willAnimate = true)
         controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
         controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
         controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
     }
+
+    @Test
+    fun creatingControllerFromNormalViewThrows() {
+        assertThrows(IllegalArgumentException::class.java) {
+            GhostedViewLaunchAnimatorController(FrameLayout(mContext))
+        }
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
index ed0cd7e..31d0d12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
@@ -19,6 +19,7 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.graphics.Typeface
+import android.graphics.fonts.FontVariationAxis
 import android.testing.AndroidTestingRunner
 import android.text.Layout
 import android.text.StaticLayout
@@ -178,4 +179,71 @@
 
         assertThat(paint.typeface).isSameInstanceAs(prevTypeface)
     }
+
+    @Test
+    fun testSetTextStyle_addWeight() {
+        testWeightChange("", 100, FontVariationAxis.fromFontVariationSettings("'wght' 100")!!)
+    }
+
+    @Test
+    fun testSetTextStyle_changeWeight() {
+        testWeightChange(
+                "'wght' 500",
+                100,
+                FontVariationAxis.fromFontVariationSettings("'wght' 100")!!
+        )
+    }
+
+    @Test
+    fun testSetTextStyle_addWeightWithOtherAxis() {
+        testWeightChange(
+                "'wdth' 100",
+                100,
+                FontVariationAxis.fromFontVariationSettings("'wght' 100, 'wdth' 100")!!
+        )
+    }
+
+    @Test
+    fun testSetTextStyle_changeWeightWithOtherAxis() {
+        testWeightChange(
+                "'wght' 500, 'wdth' 100",
+                100,
+                FontVariationAxis.fromFontVariationSettings("'wght' 100, 'wdth' 100")!!
+        )
+    }
+
+    private fun testWeightChange(
+            initialFontVariationSettings: String,
+            weight: Int,
+            expectedFontVariationSettings: Array<FontVariationAxis>
+    ) {
+        val layout = makeLayout("Hello, World", PAINT)
+        val valueAnimator = mock(ValueAnimator::class.java)
+        val textInterpolator = mock(TextInterpolator::class.java)
+        val paint =
+                TextPaint().apply {
+                    typeface = Typeface.createFromFile("/system/fonts/Roboto-Regular.ttf")
+                    fontVariationSettings = initialFontVariationSettings
+                }
+        `when`(textInterpolator.targetPaint).thenReturn(paint)
+
+        val textAnimator =
+                TextAnimator(layout, {}).apply {
+                    this.textInterpolator = textInterpolator
+                    this.animator = valueAnimator
+                }
+        textAnimator.setTextStyle(weight = weight, animate = false)
+
+        val resultFontVariationList =
+                FontVariationAxis.fromFontVariationSettings(
+                        textInterpolator.targetPaint.fontVariationSettings
+                )
+        expectedFontVariationSettings.forEach { expectedAxis ->
+            val resultAxis = resultFontVariationList?.filter { it.tag == expectedAxis.tag }?.get(0)
+            assertThat(resultAxis).isNotNull()
+            if (resultAxis != null) {
+                assertThat(resultAxis.styleValue).isEqualTo(expectedAxis.styleValue)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
new file mode 100644
index 0000000..3bdbf97
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -0,0 +1,87 @@
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private data class BackInput(val progressX: Float, val progressY: Float, val edge: Int)
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BackAnimationSpecTest : SysuiTestCase() {
+    private var displayMetrics =
+        DisplayMetrics().apply {
+            widthPixels = 100
+            heightPixels = 200
+            density = 3f
+        }
+
+    @Test
+    fun sysUi_floatingSystemSurfaces_animationValues() {
+        val maxX = 14.0f
+        val maxY = 4.0f
+        val minScale = 0.8f
+
+        val backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics)
+
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = 0f, translateY = 0f, scale = 1f),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = -maxX, translateY = 0f, scale = minScale),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_RIGHT),
+            expected = BackTransformation(translateX = maxX, translateY = 0f, scale = minScale),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = -maxX, translateY = -maxY, scale = minScale),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = 0f, translateY = -maxY, scale = 1f),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = -1f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = 0f, translateY = maxY, scale = 1f),
+        )
+    }
+}
+
+private fun assertBackTransformation(
+    backAnimationSpec: BackAnimationSpec,
+    backInput: BackInput,
+    expected: BackTransformation,
+) {
+    val actual = BackTransformation()
+    backAnimationSpec.getBackTransformation(
+        backEvent =
+            BackEvent(
+                /* touchX = */ 0f,
+                /* touchY = */ 0f,
+                /* progress = */ backInput.progressX,
+                /* swipeEdge = */ backInput.edge,
+            ),
+        progressY = backInput.progressY,
+        result = actual
+    )
+
+    val tolerance = 0f
+    assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
+    assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+    assertThat(actual.scale).isWithin(tolerance).of(expected.scale)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
new file mode 100644
index 0000000..190b3d2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
@@ -0,0 +1,80 @@
+package com.android.systemui.animation.back
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BackTransformationTest : SysuiTestCase() {
+    private val targetView: View = mock()
+
+    @Test
+    fun defaultValue_noTransformation() {
+        val transformation = BackTransformation()
+
+        assertThat(transformation.translateX).isNaN()
+        assertThat(transformation.translateY).isNaN()
+        assertThat(transformation.scale).isNaN()
+    }
+
+    @Test
+    fun applyTo_targetView_translateX_Y_Scale() {
+        val transformation = BackTransformation(translateX = 0f, translateY = 0f, scale = 1f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).translationX = 0f
+        verify(targetView).translationY = 0f
+        verify(targetView).scaleX = 1f
+        verify(targetView).scaleY = 1f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_translateX() {
+        val transformation = BackTransformation(translateX = 1f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).translationX = 1f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_translateY() {
+        val transformation = BackTransformation(translateY = 2f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).translationY = 2f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_scale() {
+        val transformation = BackTransformation(scale = 3f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).scaleX = 3f
+        verify(targetView).scaleY = 3f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_noTransformation() {
+        val transformation = BackTransformation()
+
+        transformation.applyTo(targetView = targetView)
+
+        verifyNoMoreInteractions(targetView)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
new file mode 100644
index 0000000..921f9a8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
@@ -0,0 +1,63 @@
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class OnBackAnimationCallbackExtensionTest : SysuiTestCase() {
+    private val onBackProgress: (BackTransformation) -> Unit = mock()
+    private val onBackStart: (BackEvent) -> Unit = mock()
+    private val onBackInvoke: () -> Unit = mock()
+    private val onBackCancel: () -> Unit = mock()
+
+    private val displayMetrics =
+        DisplayMetrics().apply {
+            widthPixels = 100
+            heightPixels = 100
+            density = 1f
+        }
+
+    private val onBackAnimationCallback =
+        onBackAnimationCallbackFrom(
+            backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics),
+            displayMetrics = displayMetrics,
+            onBackProgressed = onBackProgress,
+            onBackStarted = onBackStart,
+            onBackInvoked = onBackInvoke,
+            onBackCancelled = onBackCancel,
+        )
+
+    @Test
+    fun onBackProgressed_shouldInvoke_onBackProgress() {
+        val backEvent = BackEvent(0f, 0f, 0f, BackEvent.EDGE_LEFT)
+        onBackAnimationCallback.onBackStarted(backEvent)
+
+        onBackAnimationCallback.onBackProgressed(backEvent)
+
+        verify(onBackProgress).invoke(BackTransformation(0f, 0f, 1f))
+    }
+
+    @Test
+    fun onBackStarted_shouldInvoke_onBackStart() {
+        val backEvent = BackEvent(0f, 0f, 0f, BackEvent.EDGE_LEFT)
+
+        onBackAnimationCallback.onBackStarted(backEvent)
+
+        verify(onBackStart).invoke(backEvent)
+    }
+
+    @Test
+    fun onBackInvoked_shouldInvoke_onBackInvoke() {
+        onBackAnimationCallback.onBackInvoked()
+
+        verify(onBackInvoke).invoke()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
index bce98cf..0574838 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
@@ -15,27 +15,49 @@
  */
 package com.android.systemui.biometrics
 
+import android.content.Context
 import android.hardware.biometrics.BiometricAuthenticator
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.biometrics.SensorProperties
+import android.hardware.display.DisplayManagerGlobal
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.os.Bundle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
+import android.view.Display
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import android.view.Surface
 import android.view.View
+import android.view.ViewGroup
 import androidx.test.filters.SmallTest
+import com.airbnb.lottie.LottieAnimationView
 import com.android.systemui.R
+import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.SysuiTestableContext
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenEver
+
+private const val DISPLAY_ID = 2
+private const val SENSOR_ID = 1
 
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
@@ -50,9 +72,22 @@
     private lateinit var callback: AuthBiometricView.Callback
 
     @Mock
+    private lateinit var fingerprintManager: FingerprintManager
+
+    @Mock
+    private lateinit var iconView: LottieAnimationView
+
+    @Mock
+    private lateinit var iconViewOverlay: LottieAnimationView
+
+    @Mock
+    private lateinit var iconLayoutParamSize: Pair<Int, Int>
+
+    @Mock
     private lateinit var panelController: AuthPanelController
 
     private lateinit var biometricView: AuthBiometricView
+    private lateinit var iconController: AuthBiometricFingerprintIconController
 
     private fun createView(allowDeviceCredential: Boolean = false): AuthBiometricFingerprintView {
         val view: AuthBiometricFingerprintView =
@@ -277,5 +312,186 @@
         verify(callback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL)
     }
 
+    private fun testWithSfpsDisplay(
+        isReverseDefaultRotation: Boolean = false,
+        inRearDisplayMode: Boolean = false,
+        isFolded: Boolean = false,
+        initInfo: DisplayInfo.() -> Unit = {},
+        block: () -> Unit
+    ) {
+        val displayInfo = DisplayInfo()
+        displayInfo.initInfo()
+
+        val dmGlobal = mock(DisplayManagerGlobal::class.java)
+        val display = Display(dmGlobal, DISPLAY_ID, displayInfo,
+            DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS)
+
+        whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo)
+
+        val iconControllerContext = context.createDisplayContext(display) as SysuiTestableContext
+        iconControllerContext.orCreateTestableResources.addOverride(
+            com.android.internal.R.bool.config_reverseDefaultRotation,
+            isReverseDefaultRotation
+        )
+
+        val rearDisplayDeviceStates = if (inRearDisplayMode) intArrayOf(3) else intArrayOf()
+        iconControllerContext.orCreateTestableResources.addOverride(
+            com.android.internal.R.array.config_rearDisplayDeviceStates,
+            rearDisplayDeviceStates
+        )
+
+        val layoutParams = mock(ViewGroup.LayoutParams::class.java)
+        whenEver(iconView.layoutParams).thenReturn(layoutParams)
+        whenEver(iconViewOverlay.layoutParams).thenReturn(layoutParams)
+
+        var locations = listOf(SensorLocationInternal("", 2500, 0, 0))
+        whenEver(fingerprintManager.sensorPropertiesInternal)
+            .thenReturn(
+                listOf(
+                    FingerprintSensorPropertiesInternal(
+                        SENSOR_ID,
+                        SensorProperties.STRENGTH_STRONG,
+                        5 /* maxEnrollmentsPerUser */,
+                        listOf() /* componentInfo */,
+                        FingerprintSensorProperties.TYPE_POWER_BUTTON,
+                        true /* halControlsIllumination */,
+                        true /* resetLockoutRequiresHardwareAuthToken */,
+                        locations
+                    )
+                )
+            )
+        iconControllerContext.addMockSystemService(Context.FINGERPRINT_SERVICE, fingerprintManager)
+
+        iconController = AuthBiometricFingerprintIconController(
+            iconControllerContext,
+            iconView,
+            iconViewOverlay
+        )
+        iconController.onFoldUpdated(isFolded)
+
+        biometricView.mIconController = iconController
+        block()
+    }
+
+    @Test
+    fun sfpsRearDisplay_showsCorrectAnimationAssetsAcrossRotations() {
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = true,
+            isFolded = false,
+            { rotation = Surface.ROTATION_0 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = true,
+            isFolded = false,
+            { rotation = Surface.ROTATION_90 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = true,
+            isFolded = false,
+            { rotation = Surface.ROTATION_180 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = true,
+            isFolded = false,
+            { rotation = Surface.ROTATION_270 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        val expectedLottieAssetOrder: List<Int> = listOf(
+            R.raw.biometricprompt_rear_landscape_base,
+            R.raw.biometricprompt_rear_portrait_reverse_base,
+            R.raw.biometricprompt_rear_landscape_base,
+            R.raw.biometricprompt_rear_portrait_base,
+        )
+
+        val lottieAssetCaptor: ArgumentCaptor<Int> = ArgumentCaptor.forClass(Int::class.java)
+        verify(iconView, times(4)).setAnimation(lottieAssetCaptor.capture())
+        val observedLottieAssetOrder: List<Int> = lottieAssetCaptor.getAllValues()
+        assertThat(observedLottieAssetOrder).containsExactlyElementsIn(expectedLottieAssetOrder)
+                .inOrder()
+    }
+
+    @Test
+    fun sfpsDefaultDisplayFolded_showsAnimationsAssetsCorrectlyAcrossRotations() {
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = true,
+            { rotation = Surface.ROTATION_0 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+            testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = true,
+            { rotation = Surface.ROTATION_90 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN); }
+            testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = true,
+            { rotation = Surface.ROTATION_180 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN); }
+            testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = true,
+            { rotation = Surface.ROTATION_270 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN); }
+        val expectedLottieAssetOrder: List<Int> = listOf(
+            R.raw.biometricprompt_folded_base_default,
+            R.raw.biometricprompt_folded_base_topleft,
+            R.raw.biometricprompt_folded_base_default,
+            R.raw.biometricprompt_folded_base_bottomright,
+        )
+
+        val lottieAssetCaptor: ArgumentCaptor<Int> = ArgumentCaptor.forClass(Int::class.java)
+        verify(iconView, times(4)).setAnimation(lottieAssetCaptor.capture())
+        val observedLottieAssetOrder: List<Int> = lottieAssetCaptor.getAllValues()
+        assertThat(observedLottieAssetOrder).containsExactlyElementsIn(expectedLottieAssetOrder)
+                .inOrder()
+    }
+
+    @Test
+    fun sfpsDefaultDisplayUnfolded_showsAnimationsAssetsCorrectlyAcrossRotations() {
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = false,
+            { rotation = Surface.ROTATION_0 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = false,
+            { rotation = Surface.ROTATION_90 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = false,
+            { rotation = Surface.ROTATION_180 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        testWithSfpsDisplay(
+            isReverseDefaultRotation = false,
+            inRearDisplayMode = false,
+            isFolded = false,
+            { rotation = Surface.ROTATION_270 }
+        ) { biometricView.updateState(STATE_AUTHENTICATING_ANIMATING_IN) }
+        val expectedLottieAssetOrder: List<Int> = listOf(
+            R.raw.biometricprompt_landscape_base,
+            R.raw.biometricprompt_portrait_base_topleft,
+            R.raw.biometricprompt_landscape_base,
+            R.raw.biometricprompt_portrait_base_bottomright,
+        )
+
+        val lottieAssetCaptor: ArgumentCaptor<Int> = ArgumentCaptor.forClass(Int::class.java)
+        verify(iconView, times(4)).setAnimation(lottieAssetCaptor.capture())
+        val observedLottieAssetOrder: List<Int> = lottieAssetCaptor.getAllValues()
+        assertThat(observedLottieAssetOrder).containsExactlyElementsIn(expectedLottieAssetOrder)
+                .inOrder()
+    }
+
     override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages()
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 83bf183..489efd71 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -25,6 +25,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
 import static junit.framework.Assert.assertNull;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -33,6 +34,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -49,6 +51,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Point;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
@@ -166,6 +169,8 @@
     private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
     @Captor
     private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefullnessObserverCaptor;
+    @Mock
+    private Resources mResources;
 
     private TestableContext mContextSpy;
     private Execution mExecution;
@@ -739,7 +744,7 @@
     public void testForwardsDozeEvents() throws RemoteException {
         when(mStatusBarStateController.isDozing()).thenReturn(true);
         when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE);
-        mAuthController.setBiometicContextListener(mContextListener);
+        mAuthController.setBiometricContextListener(mContextListener);
 
         mStatusBarStateListenerCaptor.getValue().onDozingChanged(true);
         mStatusBarStateListenerCaptor.getValue().onDozingChanged(false);
@@ -754,7 +759,7 @@
     public void testForwardsWakeEvents() throws RemoteException {
         when(mStatusBarStateController.isDozing()).thenReturn(false);
         when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE);
-        mAuthController.setBiometicContextListener(mContextListener);
+        mAuthController.setBiometricContextListener(mContextListener);
 
         mWakefullnessObserverCaptor.getValue().onStartedGoingToSleep();
         mWakefullnessObserverCaptor.getValue().onFinishedGoingToSleep();
@@ -879,6 +884,25 @@
         );
     }
 
+    @Test
+    public void testUpdateFingerprintLocation_defaultPointChanges_whenConfigChanges() {
+        when(mContextSpy.getResources()).thenReturn(mResources);
+
+        doReturn(500).when(mResources)
+                .getDimensionPixelSize(eq(com.android.systemui.R.dimen
+                        .physical_fingerprint_sensor_center_screen_location_y));
+        mAuthController.onConfigurationChanged(null /* newConfig */);
+
+        final Point firstFpLocation = mAuthController.getFingerprintSensorLocation();
+
+        doReturn(1000).when(mResources)
+                .getDimensionPixelSize(eq(com.android.systemui.R.dimen
+                        .physical_fingerprint_sensor_center_screen_location_y));
+        mAuthController.onConfigurationChanged(null /* newConfig */);
+
+        assertNotSame(firstFpLocation, mAuthController.getFingerprintSensorLocation());
+    }
+
     private void showDialog(int[] sensorIds, boolean credentialAllowed) {
         mAuthController.showAuthenticationDialog(createTestPromptInfo(),
                 mReceiver /* receiver */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index 3c40835..6333a68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -51,13 +51,23 @@
 import android.view.WindowMetrics
 import androidx.test.filters.SmallTest
 import com.airbnb.lottie.LottieAnimationView
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.SysuiTestableContext
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.MODERN_ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -88,6 +98,7 @@
 
     @JvmField @Rule var rule = MockitoJUnit.rule()
 
+    @Mock lateinit var keyguardStateController: KeyguardStateController
     @Mock lateinit var layoutInflater: LayoutInflater
     @Mock lateinit var fingerprintManager: FingerprintManager
     @Mock lateinit var windowManager: WindowManager
@@ -100,14 +111,16 @@
     @Captor lateinit var overlayCaptor: ArgumentCaptor<View>
     @Captor lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
+    private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
+    private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+    private val featureFlags = FakeFeatureFlags()
     private val executor = FakeExecutor(FakeSystemClock())
     private lateinit var overlayController: ISidefpsController
     private lateinit var sideFpsController: SideFpsController
 
     enum class DeviceConfig {
         X_ALIGNED,
-        Y_ALIGNED_UNFOLDED,
-        Y_ALIGNED_FOLDED
+        Y_ALIGNED,
     }
 
     private lateinit var deviceConfig: DeviceConfig
@@ -121,6 +134,19 @@
 
     @Before
     fun setup() {
+        featureFlags.set(MODERN_ALTERNATE_BOUNCER, true)
+        keyguardBouncerRepository = FakeKeyguardBouncerRepository()
+        alternateBouncerInteractor =
+            AlternateBouncerInteractor(
+                keyguardStateController,
+                keyguardBouncerRepository,
+                FakeBiometricSettingsRepository(),
+                FakeDeviceEntryFingerprintAuthRepository(),
+                FakeSystemClock(),
+                mock(KeyguardUpdateMonitor::class.java),
+                featureFlags,
+            )
+
         context.addMockSystemService(DisplayManager::class.java, displayManager)
         context.addMockSystemService(WindowManager::class.java, windowManager)
 
@@ -143,35 +169,31 @@
 
     private fun testWithDisplay(
         deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
+        isReverseDefaultRotation: Boolean = false,
         initInfo: DisplayInfo.() -> Unit = {},
         windowInsets: WindowInsets = insetsForSmallNavbar(),
+        inRearDisplayMode: Boolean = false,
         block: () -> Unit
     ) {
         this.deviceConfig = deviceConfig
 
         when (deviceConfig) {
             DeviceConfig.X_ALIGNED -> {
-                displayWidth = 2560
-                displayHeight = 1600
-                sensorLocation = SensorLocationInternal("", 2325, 0, 0)
-                boundsWidth = 160
-                boundsHeight = 84
+                displayWidth = 3000
+                displayHeight = 1500
+                sensorLocation = SensorLocationInternal("", 2500, 0, 0)
+                boundsWidth = 200
+                boundsHeight = 100
             }
-            DeviceConfig.Y_ALIGNED_UNFOLDED -> {
-                displayWidth = 2208
-                displayHeight = 1840
-                sensorLocation = SensorLocationInternal("", 0, 510, 0)
-                boundsWidth = 110
-                boundsHeight = 210
-            }
-            DeviceConfig.Y_ALIGNED_FOLDED -> {
-                displayWidth = 1080
-                displayHeight = 2100
-                sensorLocation = SensorLocationInternal("", 0, 590, 0)
-                boundsWidth = 110
-                boundsHeight = 210
+            DeviceConfig.Y_ALIGNED -> {
+                displayWidth = 2500
+                displayHeight = 2000
+                sensorLocation = SensorLocationInternal("", 0, 300, 0)
+                boundsWidth = 100
+                boundsHeight = 200
             }
         }
+
         indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
         displayBounds = Rect(0, 0, displayWidth, displayHeight)
         var locations = listOf(sensorLocation)
@@ -194,8 +216,10 @@
 
         val displayInfo = DisplayInfo()
         displayInfo.initInfo()
+
         val dmGlobal = mock(DisplayManagerGlobal::class.java)
         val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS)
+
         whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo)
         whenEver(windowManager.defaultDisplay).thenReturn(display)
         whenEver(windowManager.maximumWindowMetrics)
@@ -203,9 +227,21 @@
         whenEver(windowManager.currentWindowMetrics)
             .thenReturn(WindowMetrics(displayBounds, windowInsets))
 
+        val sideFpsControllerContext = context.createDisplayContext(display) as SysuiTestableContext
+        sideFpsControllerContext.orCreateTestableResources.addOverride(
+            com.android.internal.R.bool.config_reverseDefaultRotation,
+            isReverseDefaultRotation
+        )
+
+        val rearDisplayDeviceStates = if (inRearDisplayMode) intArrayOf(3) else intArrayOf()
+        sideFpsControllerContext.orCreateTestableResources.addOverride(
+            com.android.internal.R.array.config_rearDisplayDeviceStates,
+            rearDisplayDeviceStates
+        )
+
         sideFpsController =
             SideFpsController(
-                context.createDisplayContext(display),
+                sideFpsControllerContext,
                 layoutInflater,
                 fingerprintManager,
                 windowManager,
@@ -214,7 +250,10 @@
                 displayManager,
                 executor,
                 handler,
-                dumpManager
+                alternateBouncerInteractor,
+                TestCoroutineScope(),
+                featureFlags,
+                dumpManager,
             )
 
         overlayController =
@@ -238,6 +277,17 @@
     }
 
     @Test
+    fun testShowOverlayReasonWhenDisplayChanged() = testWithDisplay {
+        sideFpsController.show(SideFpsUiRequestSource.AUTO_SHOW, REASON_AUTH_KEYGUARD)
+        executor.runAllReady()
+        sideFpsController.orientationListener.onDisplayChanged(1 /* displayId */)
+        executor.runAllReady()
+
+        assertThat(sideFpsController.orientationReasonListener.reason)
+            .isEqualTo(REASON_AUTH_KEYGUARD)
+    }
+
+    @Test
     fun testShowsAndHides() = testWithDisplay {
         overlayController.show(SENSOR_ID, REASON_UNKNOWN)
         executor.runAllReady()
@@ -299,82 +349,287 @@
     }
 
     @Test
-    fun showsWithTaskbar() =
-        testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_0 }) {
-            hidesWithTaskbar(visible = true)
-        }
-
-    @Test
-    fun showsWithTaskbarOnY() =
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_0() =
         testWithDisplay(
-            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
             { rotation = Surface.ROTATION_0 }
-        ) { hidesWithTaskbar(visible = true) }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
 
     @Test
-    fun showsWithTaskbar90() =
-        testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_90 }) {
-            hidesWithTaskbar(visible = true)
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_90 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_180 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_180 },
+            windowInsets = insetsForSmallNavbar()
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_180 },
+            windowInsets = insetsForLargeNavbar()
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_270 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_0 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_90 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_InReverseDefaultRotation_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_90 },
+            windowInsets = insetsForSmallNavbar()
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_InReverseDefaultRotation_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_90 },
+            windowInsets = insetsForLargeNavbar()
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_180 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_270 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_0 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_90 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_180 },
+        ) {
+            verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
         }
 
     @Test
-    fun showsWithTaskbar90OnY() =
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_270() =
         testWithDisplay(
-            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-            { rotation = Surface.ROTATION_90 }
-        ) { hidesWithTaskbar(visible = true) }
-
-    @Test
-    fun showsWithTaskbar180() =
-        testWithDisplay(
-            deviceConfig = DeviceConfig.X_ALIGNED,
-            { rotation = Surface.ROTATION_180 }
-        ) { hidesWithTaskbar(visible = true) }
-
-    @Test
-    fun showsWithTaskbar270OnY() =
-        testWithDisplay(
-            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
             { rotation = Surface.ROTATION_270 }
-        ) { hidesWithTaskbar(visible = true) }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
 
     @Test
-    fun showsWithTaskbarCollapsedDown() =
+    fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_270() =
         testWithDisplay(
-            deviceConfig = DeviceConfig.X_ALIGNED,
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
             { rotation = Surface.ROTATION_270 },
             windowInsets = insetsForSmallNavbar()
-        ) { hidesWithTaskbar(visible = true) }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
 
     @Test
-    fun showsWithTaskbarCollapsedDownOnY() =
+    fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_270() =
         testWithDisplay(
-            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-            { rotation = Surface.ROTATION_180 },
-            windowInsets = insetsForSmallNavbar()
-        ) { hidesWithTaskbar(visible = true) }
-
-    @Test
-    fun hidesWithTaskbarDown() =
-        testWithDisplay(
-            deviceConfig = DeviceConfig.X_ALIGNED,
-            { rotation = Surface.ROTATION_180 },
-            windowInsets = insetsForLargeNavbar()
-        ) { hidesWithTaskbar(visible = false) }
-
-    @Test
-    fun hidesWithTaskbarDownOnY() =
-        testWithDisplay(
-            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
             { rotation = Surface.ROTATION_270 },
             windowInsets = insetsForLargeNavbar()
-        ) { hidesWithTaskbar(visible = true) }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_0 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_90 },
+        ) {
+            verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
+        }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_180 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_InReverseDefaultRotation_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_180 },
+            windowInsets = insetsForSmallNavbar()
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_InReverseDefaultRotation_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_180 },
+            windowInsets = insetsForLargeNavbar()
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+    @Test
+    fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_270 }
+        ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+    @Test
+    fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_0 },
+            inRearDisplayMode = true,
+        ) {
+            verifySfpsIndicator_notAdded_InRearDisplayMode()
+        }
+
+    @Test
+    fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_90() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_90 },
+            inRearDisplayMode = true,
+        ) {
+            verifySfpsIndicator_notAdded_InRearDisplayMode()
+        }
+
+    @Test
+    fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_180 },
+            inRearDisplayMode = true,
+        ) {
+            verifySfpsIndicator_notAdded_InRearDisplayMode()
+        }
+
+    @Test
+    fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_270 },
+            inRearDisplayMode = true,
+        ) {
+            verifySfpsIndicator_notAdded_InRearDisplayMode()
+        }
+
+    private fun verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible: Boolean) {
+        sideFpsController.overlayOffsets = sensorLocation
+    }
+
+    private fun verifySfpsIndicator_notAdded_InRearDisplayMode() {
+        sideFpsController.overlayOffsets = sensorLocation
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    fun alternateBouncerVisibility_showAndHideSideFpsUI() = testWithDisplay {
+        // WHEN alternate bouncer is visible
+        keyguardBouncerRepository.setAlternateVisible(true)
+        executor.runAllReady()
+
+        // THEN side fps shows UI
+        verify(windowManager).addView(any(), any())
+        verify(windowManager, never()).removeView(any())
+
+        // WHEN alternate bouncer is no longer visible
+        keyguardBouncerRepository.setAlternateVisible(false)
+        executor.runAllReady()
+
+        // THEN side fps UI is hidden
+        verify(windowManager).removeView(any())
+    }
 
     private fun hidesWithTaskbar(visible: Boolean) {
         overlayController.show(SENSOR_ID, REASON_UNKNOWN)
         executor.runAllReady()
 
-        sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(visible, false)
+        sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(true, false)
         executor.runAllReady()
 
         verify(windowManager).addView(any(), any())
@@ -382,25 +637,98 @@
         verify(sideFpsView).visibility = if (visible) View.VISIBLE else View.GONE
     }
 
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+     * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+     * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+     * in other rotations have been omitted.
+     */
     @Test
-    fun testIndicatorPlacementForXAlignedSensor() =
-        testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED) {
-            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+    fun verifiesIndicatorPlacementForXAlignedSensor_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_0 }
+        ) {
             sideFpsController.overlayOffsets = sensorLocation
+
             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
             executor.runAllReady()
 
             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
-
             assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
             assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
         }
 
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+     * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+     * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+     * correctly, tests for indicator placement in other rotations have been omitted.
+     */
     @Test
-    fun testIndicatorPlacementForYAlignedSensor() =
-        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+    fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_270 }
+        ) {
             sideFpsController.overlayOffsets = sensorLocation
+
             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+        }
+
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+     * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+     * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+     * in other rotations have been omitted.
+     */
+    @Test
+    fun verifiesIndicatorPlacementForYAlignedSensor_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_0 }
+        ) {
+            sideFpsController.overlayOffsets = sensorLocation
+
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+        }
+
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+     * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+     * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+     * correctly, tests for indicator placement in other rotations have been omitted.
+     */
+    @Test
+    fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_270 }
+        ) {
+            sideFpsController.overlayOffsets = sensorLocation
+
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
             executor.runAllReady()
 
@@ -412,7 +740,6 @@
     @Test
     fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
         // By default all those tests assume the side fps sensor is available.
-
         assertThat(fingerprintManager.hasSideFpsSensor()).isTrue()
     }
 
@@ -425,7 +752,7 @@
 
     @Test
     fun testLayoutParams_isKeyguardDialogType() =
-        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
             sideFpsController.overlayOffsets = sensorLocation
             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
@@ -440,7 +767,7 @@
 
     @Test
     fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
-        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
             sideFpsController.overlayOffsets = sensorLocation
             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
@@ -455,7 +782,7 @@
 
     @Test
     fun testLayoutParams_hasTrustedOverlayWindowFlag() =
-        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
             sideFpsController.overlayOffsets = sensorLocation
             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 53bc2c2..0690d1b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
@@ -52,7 +53,7 @@
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
@@ -95,18 +96,19 @@
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var transitionController: LockscreenShadeTransitionController
     @Mock private lateinit var configurationController: ConfigurationController
-    @Mock private lateinit var systemClock: SystemClock
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var unlockedScreenOffAnimationController:
             UnlockedScreenOffAnimationController
     @Mock private lateinit var udfpsDisplayMode: UdfpsDisplayModeProvider
+    @Mock private lateinit var secureSettings: SecureSettings
     @Mock private lateinit var controllerCallback: IUdfpsOverlayControllerCallback
     @Mock private lateinit var udfpsController: UdfpsController
     @Mock private lateinit var udfpsView: UdfpsView
     @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView
     @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
     @Mock private lateinit var featureFlags: FeatureFlags
-    @Mock private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
+    @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+    @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -138,10 +140,10 @@
             context, fingerprintManager, inflater, windowManager, accessibilityManager,
             statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
-            configurationController, systemClock, keyguardStateController,
-            unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
-            controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
-            mPrimaryBouncerInteractor, isDebuggable
+            configurationController, keyguardStateController,
+            unlockedScreenOffAnimationController, udfpsDisplayMode, secureSettings, REQUEST_ID,
+            reason, controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
+            primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable,
         )
         block()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index b061eb3..232daad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -81,6 +81,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -95,6 +96,7 @@
 import com.android.systemui.util.concurrency.Execution;
 import com.android.systemui.util.concurrency.FakeExecution;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
 
@@ -202,6 +204,10 @@
     private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     @Mock
     private SinglePointerTouchProcessor mSinglePointerTouchProcessor;
+    @Mock
+    private AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @Mock
+    private SecureSettings mSecureSettings;
 
     // Capture listeners so that they can be used to send events
     @Captor
@@ -292,7 +298,8 @@
                 mDisplayManager, mHandler, mConfigurationController, mSystemClock,
                 mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker,
                 mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor,
-                mPrimaryBouncerInteractor, mSinglePointerTouchProcessor);
+                mPrimaryBouncerInteractor, mSinglePointerTouchProcessor,
+                mAlternateBouncerInteractor, mSecureSettings);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
@@ -406,7 +413,7 @@
         // GIVEN overlay was showing and the udfps bouncer is showing
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
 
         // WHEN the overlay is hidden
         mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
index 3c61382..dbbc266 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
@@ -30,20 +30,19 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionListener;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.mockito.ArgumentCaptor;
@@ -71,11 +70,10 @@
     protected @Mock SystemUIDialogManager mDialogManager;
     protected @Mock UdfpsController mUdfpsController;
     protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
-    protected @Mock KeyguardBouncer mBouncer;
     protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
 
     protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
-    protected FakeSystemClock mSystemClock = new FakeSystemClock();
 
     protected UdfpsKeyguardViewController mController;
 
@@ -86,10 +84,6 @@
     private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
     protected List<ShadeExpansionListener> mExpansionListeners;
 
-    private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateBouncer>
-            mAlternateBouncerCaptor;
-    protected StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
-
     private @Captor ArgumentCaptor<KeyguardStateController.Callback>
             mKeyguardStateControllerCallbackCaptor;
     protected KeyguardStateController.Callback mKeyguardStateControllerCallback;
@@ -135,12 +129,6 @@
         }
     }
 
-    protected void captureAltAuthInterceptor() {
-        verify(mStatusBarKeyguardViewManager).setAlternateBouncer(
-                mAlternateBouncerCaptor.capture());
-        mAlternateBouncer = mAlternateBouncerCaptor.getValue();
-    }
-
     protected void captureKeyguardStateControllerCallback() {
         verify(mKeyguardStateController).addCallback(
                 mKeyguardStateControllerCallbackCaptor.capture());
@@ -159,10 +147,8 @@
 
     protected UdfpsKeyguardViewController createUdfpsKeyguardViewController(
             boolean useModernBouncer, boolean useExpandedOverlay) {
-        mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer);
+        mFeatureFlags.set(Flags.MODERN_ALTERNATE_BOUNCER, useModernBouncer);
         mFeatureFlags.set(Flags.UDFPS_NEW_TOUCH_DETECTION, useExpandedOverlay);
-        when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn(
-                useModernBouncer ? null : mBouncer);
         UdfpsKeyguardViewController controller = new UdfpsKeyguardViewController(
                 mView,
                 mStatusBarStateController,
@@ -172,14 +158,14 @@
                 mDumpManager,
                 mLockscreenShadeTransitionController,
                 mConfigurationController,
-                mSystemClock,
                 mKeyguardStateController,
                 mUnlockedScreenOffAnimationController,
                 mDialogManager,
                 mUdfpsController,
                 mActivityLaunchAnimator,
                 mFeatureFlags,
-                mPrimaryBouncerInteractor);
+                mPrimaryBouncerInteractor,
+                mAlternateBouncerInteractor);
         return controller;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index babe533..f437a8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -19,39 +19,30 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
+import android.testing.TestableLooper;
 import android.view.MotionEvent;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.shade.ShadeExpansionListener;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper
-public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest {
-    private @Captor ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback>
-            mBouncerExpansionCallbackCaptor;
-    private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
 
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest {
     @Override
     public UdfpsKeyguardViewController createUdfpsKeyguardViewController() {
         return createUdfpsKeyguardViewController(/* useModernBouncer */ false,
@@ -64,16 +55,12 @@
         captureStatusBarStateListeners();
         sendStatusBarStateChanged(StatusBarState.KEYGUARD);
 
-        captureBouncerExpansionCallback();
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
         when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true);
-        mBouncerExpansionCallback.onVisibilityChanged(true);
-
+        when(mView.getUnpausedAlpha()).thenReturn(0);
         assertTrue(mController.shouldPauseAuth());
     }
 
-
-
     @Test
     public void testRegistersExpansionChangedListenerOnAttached() {
         mController.onViewAttached();
@@ -237,85 +224,9 @@
     public void testOverrideShouldPauseAuthOnShadeLocked() {
         mController.onViewAttached();
         captureStatusBarStateListeners();
-        captureAltAuthInterceptor();
 
         sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED);
         assertTrue(mController.shouldPauseAuth());
-
-        mAlternateBouncer.showAlternateBouncer(); // force show
-        assertFalse(mController.shouldPauseAuth());
-        assertTrue(mAlternateBouncer.isShowingAlternateBouncer());
-
-        mAlternateBouncer.hideAlternateBouncer(); // stop force show
-        assertTrue(mController.shouldPauseAuth());
-        assertFalse(mAlternateBouncer.isShowingAlternateBouncer());
-    }
-
-    @Test
-    public void testOnDetachedStateReset() {
-        // GIVEN view is attached
-        mController.onViewAttached();
-        captureAltAuthInterceptor();
-
-        // WHEN view is detached
-        mController.onViewDetached();
-
-        // THEN remove alternate auth interceptor
-        verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAlternateBouncer);
-    }
-
-    @Test
-    public void testHiddenUdfpsBouncerOnTouchOutside_nothingHappens() {
-        // GIVEN view is attached
-        mController.onViewAttached();
-        captureAltAuthInterceptor();
-
-        // GIVEN udfps bouncer isn't showing
-        mAlternateBouncer.hideAlternateBouncer();
-
-        // WHEN touch is observed outside the view
-        mController.onTouchOutsideView();
-
-        // THEN bouncer / alt auth methods are never called
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
-        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
-        verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean());
-    }
-
-    @Test
-    public void testShowingUdfpsBouncerOnTouchOutsideWithinThreshold_nothingHappens() {
-        // GIVEN view is attached
-        mController.onViewAttached();
-        captureAltAuthInterceptor();
-
-        // GIVEN udfps bouncer is showing
-        mAlternateBouncer.showAlternateBouncer();
-
-        // WHEN touch is observed outside the view 200ms later (just within threshold)
-        mSystemClock.advanceTime(200);
-        mController.onTouchOutsideView();
-
-        // THEN bouncer / alt auth methods are never called because not enough time has passed
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
-        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
-        verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean());
-    }
-
-    @Test
-    public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showPrimaryBouncer() {
-        // GIVEN view is attached
-        mController.onViewAttached();
-        captureAltAuthInterceptor();
-
-        // GIVEN udfps bouncer is showing
-        mAlternateBouncer.showAlternateBouncer();
-
-        // WHEN touch is observed outside the view 205ms later
-        mSystemClock.advanceTime(205);
-        mController.onTouchOutsideView();
-
-        // THEN show the bouncer
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(eq(true));
     }
 
     @Test
@@ -334,25 +245,6 @@
     }
 
     @Test
-    public void testShowUdfpsBouncer() {
-        // GIVEN view is attached and status bar expansion is 0
-        mController.onViewAttached();
-        captureStatusBarExpansionListeners();
-        captureKeyguardStateControllerCallback();
-        captureAltAuthInterceptor();
-        updateStatusBarExpansion(0, true);
-        reset(mView);
-        when(mView.getContext()).thenReturn(mResourceContext);
-        when(mResourceContext.getString(anyInt())).thenReturn("test string");
-
-        // WHEN status bar expansion is 0 but udfps bouncer is requested
-        mAlternateBouncer.showAlternateBouncer();
-
-        // THEN alpha is 255
-        verify(mView).setUnpausedAlpha(255);
-    }
-
-    @Test
     public void testTransitionToFullShadeProgress() {
         // GIVEN view is attached and status bar expansion is 1f
         mController.onViewAttached();
@@ -370,24 +262,6 @@
     }
 
     @Test
-    public void testShowUdfpsBouncer_transitionToFullShadeProgress() {
-        // GIVEN view is attached and status bar expansion is 1f
-        mController.onViewAttached();
-        captureStatusBarExpansionListeners();
-        captureKeyguardStateControllerCallback();
-        captureAltAuthInterceptor();
-        updateStatusBarExpansion(1f, true);
-        mAlternateBouncer.showAlternateBouncer();
-        reset(mView);
-
-        // WHEN we're transitioning to the full shade
-        mController.setTransitionToFullShadeProgress(1.0f);
-
-        // THEN alpha is 255 (b/c udfps bouncer is requested)
-        verify(mView).setUnpausedAlpha(255);
-    }
-
-    @Test
     public void testUpdatePanelExpansion_pauseAuth() {
         // GIVEN view is attached + on the keyguard
         mController.onViewAttached();
@@ -421,11 +295,6 @@
         verify(mView, atLeastOnce()).setPauseAuth(false);
     }
 
-    private void captureBouncerExpansionCallback() {
-        verify(mBouncer).addBouncerExpansionCallback(mBouncerExpansionCallbackCaptor.capture());
-        mBouncerExpansionCallback = mBouncerExpansionCallbackCaptor.getValue();
-    }
-
     @Test
     // TODO(b/259264861): Tracking Bug
     public void testUdfpsExpandedOverlayOn() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
index 2d412dc..86fb279 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
@@ -21,26 +21,38 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.phone.KeyguardBouncer
 import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.SystemClock
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.TestCoroutineScope
 import kotlinx.coroutines.yield
+import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
 import org.mockito.Mock
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -55,8 +67,9 @@
         allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
         MockitoAnnotations.initMocks(this)
         keyguardBouncerRepository =
-            KeyguardBouncerRepository(
+            KeyguardBouncerRepositoryImpl(
                 mock(com.android.keyguard.ViewMediatorCallback::class.java),
+                FakeSystemClock(),
                 TestCoroutineScope(),
                 bouncerLogger,
             )
@@ -74,8 +87,19 @@
                 mock(PrimaryBouncerCallbackInteractor::class.java),
                 mock(FalsingCollector::class.java),
                 mock(DismissCallbackRegistry::class.java),
+                context,
+                mKeyguardUpdateMonitor,
                 mock(KeyguardBypassController::class.java),
-                mKeyguardUpdateMonitor
+            )
+        mAlternateBouncerInteractor =
+            AlternateBouncerInteractor(
+                mock(KeyguardStateController::class.java),
+                keyguardBouncerRepository,
+                mock(BiometricSettingsRepository::class.java),
+                mock(DeviceEntryFingerprintAuthRepository::class.java),
+                mock(SystemClock::class.java),
+                mock(KeyguardUpdateMonitor::class.java),
+                mock(FeatureFlags::class.java)
             )
         return createUdfpsKeyguardViewController(
             /* useModernBouncer */ true, /* useExpandedOverlay */
@@ -83,9 +107,29 @@
         )
     }
 
-    /** After migration, replaces LockIconViewControllerTest version */
     @Test
-    fun testShouldPauseAuthBouncerShowing() =
+    fun shadeLocked_showAlternateBouncer_unpauseAuth() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN view is attached + on the SHADE_LOCKED (udfps view not showing)
+            mController.onViewAttached()
+            captureStatusBarStateListeners()
+            sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED)
+
+            // WHEN alternate bouncer is requested
+            val job = mController.listenForAlternateBouncerVisibility(this)
+            keyguardBouncerRepository.setAlternateVisible(true)
+            yield()
+
+            // THEN udfps view will animate in & pause auth is updated to NOT pause
+            verify(mView).animateInUdfpsBouncer(any())
+            assertFalse(mController.shouldPauseAuth())
+
+            job.cancel()
+        }
+
+    /** After migration to MODERN_BOUNCER, replaces UdfpsKeyguardViewControllerTest version */
+    @Test
+    fun shouldPauseAuthBouncerShowing() =
         runBlocking(IMMEDIATE) {
             // GIVEN view attached and we're on the keyguard
             mController.onViewAttached()
@@ -95,7 +139,7 @@
             // WHEN the bouncer expansion is VISIBLE
             val job = mController.listenForBouncerExpansion(this)
             keyguardBouncerRepository.setPrimaryVisible(true)
-            keyguardBouncerRepository.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+            keyguardBouncerRepository.setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
             yield()
 
             // THEN UDFPS shouldPauseAuth == true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index d550b92..8255a14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -80,15 +80,6 @@
     }
 
     @Test
-    fun forwardsEvents() {
-        view.dozeTimeTick()
-        verify(animationViewController).dozeTimeTick()
-
-        view.onTouchOutsideView()
-        verify(animationViewController).onTouchOutsideView()
-    }
-
-    @Test
     fun layoutSizeFitsSensor() {
         val params = withArgCaptor<RectF> {
             verify(animationViewController).onSensorRectUpdated(capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
new file mode 100644
index 0000000..af46d9b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.biometrics.udfps
+
+import android.graphics.Point
+import android.graphics.Rect
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when` as whenEver
+
+@SmallTest
+@RunWith(Parameterized::class)
+class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() {
+    val underTest = spy(EllipseOverlapDetector(neededPoints = 1))
+
+    @Before
+    fun setUp() {
+        // Use one single center point for testing, required or total number of points may change
+        whenEver(underTest.calculateSensorPoints(SENSOR))
+            .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY())))
+    }
+
+    @Test
+    fun isGoodOverlap() {
+        val touchData =
+            TOUCH_DATA.copy(
+                x = testCase.x.toFloat(),
+                y = testCase.y.toFloat(),
+                minor = testCase.minor,
+                major = testCase.major
+            )
+        val actual = underTest.isGoodOverlap(touchData, SENSOR)
+
+        assertThat(actual).isEqualTo(testCase.expected)
+    }
+
+    data class TestCase(
+        val x: Int,
+        val y: Int,
+        val minor: Float,
+        val major: Float,
+        val expected: Boolean
+    )
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun data(): List<TestCase> =
+            listOf(
+                    genTestCases(
+                        innerXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()),
+                        innerYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()),
+                        outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
+                        outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
+                        minor = 300f,
+                        major = 300f,
+                        expected = true
+                    ),
+                    genTestCases(
+                        innerXs = listOf(SENSOR.left, SENSOR.right),
+                        innerYs = listOf(SENSOR.top, SENSOR.bottom),
+                        outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
+                        outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
+                        minor = 100f,
+                        major = 100f,
+                        expected = false
+                    )
+                )
+                .flatten()
+    }
+}
+
+/* Placeholder touch parameters. */
+private const val POINTER_ID = 42
+private const val NATIVE_MINOR = 2.71828f
+private const val NATIVE_MAJOR = 3.14f
+private const val ORIENTATION = 0f // used for perfect circles
+private const val TIME = 12345699L
+private const val GESTURE_START = 12345600L
+
+/* Template [NormalizedTouchData]. */
+private val TOUCH_DATA =
+    NormalizedTouchData(
+        POINTER_ID,
+        x = 0f,
+        y = 0f,
+        NATIVE_MINOR,
+        NATIVE_MAJOR,
+        ORIENTATION,
+        TIME,
+        GESTURE_START
+    )
+
+private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */)
+
+private fun genTestCases(
+    innerXs: List<Int>,
+    innerYs: List<Int>,
+    outerXs: List<Int>,
+    outerYs: List<Int>,
+    minor: Float,
+    major: Float,
+    expected: Boolean
+): List<EllipseOverlapDetectorTest.TestCase> {
+    return (innerXs + outerXs).flatMap { x ->
+        (innerYs + outerYs).map { y ->
+            EllipseOverlapDetectorTest.TestCase(x, y, minor, major, expected)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
index 95c53b4..8e20303 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
@@ -39,7 +39,8 @@
 
     @Test
     fun processTouch() {
-        overlapDetector.shouldReturn = testCase.isGoodOverlap
+        overlapDetector.shouldReturn =
+            testCase.currentPointers.associate { pointer -> pointer.id to pointer.onSensor }
 
         val actual =
             underTest.processTouch(
@@ -56,7 +57,7 @@
 
     data class TestCase(
         val event: MotionEvent,
-        val isGoodOverlap: Boolean,
+        val currentPointers: List<TestPointer>,
         val previousPointerOnSensorId: Int,
         val overlayParams: UdfpsOverlayParams,
         val expected: TouchProcessorResult,
@@ -91,28 +92,43 @@
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_DOWN,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = true,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.DOWN,
-                        expectedPointerOnSensorId = POINTER_ID,
-                    ),
-                    genPositiveTestCases(
-                        motionEventAction = MotionEvent.ACTION_DOWN,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = true,
-                        expectedInteractionEvent = InteractionEvent.DOWN,
-                        expectedPointerOnSensorId = POINTER_ID,
+                        expectedPointerOnSensorId = POINTER_ID_1,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_DOWN,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = false,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.UNCHANGED,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_DOWN,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = false,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+                        expectedInteractionEvent = InteractionEvent.UP,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID,
+                    ),
+                    // MotionEvent.ACTION_HOVER_ENTER
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_ENTER,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+                        expectedInteractionEvent = InteractionEvent.DOWN,
+                        expectedPointerOnSensorId = POINTER_ID_1,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_ENTER,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_ENTER,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.UP,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
@@ -120,28 +136,79 @@
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_MOVE,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = true,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.DOWN,
-                        expectedPointerOnSensorId = POINTER_ID,
+                        expectedPointerOnSensorId = POINTER_ID_1,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_MOVE,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = true,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.UNCHANGED,
-                        expectedPointerOnSensorId = POINTER_ID,
+                        expectedPointerOnSensorId = POINTER_ID_1,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_MOVE,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = false,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.UNCHANGED,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_MOVE,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = false,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+                        expectedInteractionEvent = InteractionEvent.UP,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_MOVE,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = false),
+                                TestPointer(id = POINTER_ID_2, onSensor = true)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.DOWN,
+                        expectedPointerOnSensorId = POINTER_ID_2,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_MOVE,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = false),
+                                TestPointer(id = POINTER_ID_2, onSensor = true)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = POINTER_ID_2,
+                    ),
+                    // MotionEvent.ACTION_HOVER_MOVE
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+                        expectedInteractionEvent = InteractionEvent.DOWN,
+                        expectedPointerOnSensorId = POINTER_ID_1,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = POINTER_ID_1,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.UP,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
@@ -149,78 +216,197 @@
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_UP,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = true,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.UP,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_UP,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = true,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.UP,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_UP,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = false,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.UNCHANGED,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
+                    // MotionEvent.ACTION_HOVER_EXIT
                     genPositiveTestCases(
-                        motionEventAction = MotionEvent.ACTION_UP,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = false,
+                        motionEventAction = MotionEvent.ACTION_HOVER_EXIT,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.UP,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_EXIT,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+                        expectedInteractionEvent = InteractionEvent.UP,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_HOVER_EXIT,
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID,
+                    ),
                     // MotionEvent.ACTION_CANCEL
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_CANCEL,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = true,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.CANCEL,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_CANCEL,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = true,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
                         expectedInteractionEvent = InteractionEvent.CANCEL,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_CANCEL,
                         previousPointerOnSensorId = INVALID_POINTER_ID,
-                        isGoodOverlap = false,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.CANCEL,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
                     genPositiveTestCases(
                         motionEventAction = MotionEvent.ACTION_CANCEL,
-                        previousPointerOnSensorId = POINTER_ID,
-                        isGoodOverlap = false,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
                         expectedInteractionEvent = InteractionEvent.CANCEL,
                         expectedPointerOnSensorId = INVALID_POINTER_ID,
                     ),
-                )
-                .flatten() +
-                listOf(
-                        // Unsupported MotionEvent actions.
-                        genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_DOWN),
-                        genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_UP),
-                        genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_ENTER),
-                        genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_MOVE),
-                        genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_EXIT),
+                    // MotionEvent.ACTION_POINTER_DOWN
+                    genPositiveTestCases(
+                        motionEventAction =
+                            MotionEvent.ACTION_POINTER_DOWN +
+                                (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = true),
+                                TestPointer(id = POINTER_ID_2, onSensor = false)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.DOWN,
+                        expectedPointerOnSensorId = POINTER_ID_1,
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction =
+                            MotionEvent.ACTION_POINTER_DOWN +
+                                (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = false),
+                                TestPointer(id = POINTER_ID_2, onSensor = true)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.DOWN,
+                        expectedPointerOnSensorId = POINTER_ID_2
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction =
+                            MotionEvent.ACTION_POINTER_DOWN +
+                                (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = true),
+                                TestPointer(id = POINTER_ID_2, onSensor = false)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = POINTER_ID_1,
+                    ),
+                    // MotionEvent.ACTION_POINTER_UP
+                    genPositiveTestCases(
+                        motionEventAction =
+                            MotionEvent.ACTION_POINTER_UP +
+                                (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+                        previousPointerOnSensorId = INVALID_POINTER_ID,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = false),
+                                TestPointer(id = POINTER_ID_2, onSensor = false)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction =
+                            MotionEvent.ACTION_POINTER_UP +
+                                (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+                        previousPointerOnSensorId = POINTER_ID_2,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = false),
+                                TestPointer(id = POINTER_ID_2, onSensor = true)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UP,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_POINTER_UP,
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = true),
+                                TestPointer(id = POINTER_ID_2, onSensor = false)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UP,
+                        expectedPointerOnSensorId = INVALID_POINTER_ID
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction =
+                            MotionEvent.ACTION_POINTER_UP +
+                                (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+                        previousPointerOnSensorId = POINTER_ID_1,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = true),
+                                TestPointer(id = POINTER_ID_2, onSensor = false)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = POINTER_ID_1
+                    ),
+                    genPositiveTestCases(
+                        motionEventAction = MotionEvent.ACTION_POINTER_UP,
+                        previousPointerOnSensorId = POINTER_ID_2,
+                        currentPointers =
+                            listOf(
+                                TestPointer(id = POINTER_ID_1, onSensor = false),
+                                TestPointer(id = POINTER_ID_2, onSensor = true)
+                            ),
+                        expectedInteractionEvent = InteractionEvent.UNCHANGED,
+                        expectedPointerOnSensorId = POINTER_ID_2
                     )
-                    .flatten()
+                )
+                .flatten()
     }
 }
 
+data class TestPointer(val id: Int, val onSensor: Boolean)
+
 /* Display dimensions in native resolution and natural orientation. */
 private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400
 private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600
 
+/* Placeholder touch parameters. */
+private const val POINTER_ID_1 = 42
+private const val POINTER_ID_2 = 43
+private const val NATIVE_MINOR = 2.71828f
+private const val NATIVE_MAJOR = 3.14f
+private const val ORIENTATION = 1.2345f
+private const val TIME = 12345699L
+private const val GESTURE_START = 12345600L
+
 /*
  * ROTATION_0 map:
  * _ _ _ _
@@ -244,6 +430,7 @@
 private val ROTATION_0_INPUTS =
     OrientationBasedInputs(
         rotation = Surface.ROTATION_0,
+        nativeOrientation = ORIENTATION,
         nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(),
         nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(),
         nativeXOutsideSensor = 250f,
@@ -271,6 +458,7 @@
 private val ROTATION_90_INPUTS =
     OrientationBasedInputs(
         rotation = Surface.ROTATION_90,
+        nativeOrientation = (ORIENTATION - Math.PI.toFloat() / 2),
         nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(),
         nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(),
         nativeXOutsideSensor = 150f,
@@ -304,25 +492,18 @@
 private val ROTATION_270_INPUTS =
     OrientationBasedInputs(
         rotation = Surface.ROTATION_270,
+        nativeOrientation = (ORIENTATION + Math.PI.toFloat() / 2),
         nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(),
         nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(),
         nativeXOutsideSensor = 450f,
         nativeYOutsideSensor = 250f,
     )
 
-/* Placeholder touch parameters. */
-private const val POINTER_ID = 42
-private const val NATIVE_MINOR = 2.71828f
-private const val NATIVE_MAJOR = 3.14f
-private const val ORIENTATION = 1.23f
-private const val TIME = 12345699L
-private const val GESTURE_START = 12345600L
-
 /* Template [MotionEvent]. */
 private val MOTION_EVENT =
     obtainMotionEvent(
         action = 0,
-        pointerId = POINTER_ID,
+        pointerId = POINTER_ID_1,
         x = 0f,
         y = 0f,
         minor = 0f,
@@ -335,7 +516,7 @@
 /* Template [NormalizedTouchData]. */
 private val NORMALIZED_TOUCH_DATA =
     NormalizedTouchData(
-        POINTER_ID,
+        POINTER_ID_1,
         x = 0f,
         y = 0f,
         NATIVE_MINOR,
@@ -352,6 +533,7 @@
  */
 private data class OrientationBasedInputs(
     @Rotation val rotation: Int,
+    val nativeOrientation: Float,
     val nativeXWithinSensor: Float,
     val nativeYWithinSensor: Float,
     val nativeXOutsideSensor: Float,
@@ -380,7 +562,7 @@
 private fun genPositiveTestCases(
     motionEventAction: Int,
     previousPointerOnSensorId: Int,
-    isGoodOverlap: Boolean,
+    currentPointers: List<TestPointer>,
     expectedInteractionEvent: InteractionEvent,
     expectedPointerOnSensorId: Int
 ): List<SinglePointerTouchProcessorTest.TestCase> {
@@ -395,21 +577,47 @@
     return scaleFactors.flatMap { scaleFactor ->
         orientations.map { orientation ->
             val overlayParams = orientation.toOverlayParams(scaleFactor)
-            val nativeX = orientation.getNativeX(isGoodOverlap)
-            val nativeY = orientation.getNativeY(isGoodOverlap)
+
+            val pointerProperties =
+                currentPointers
+                    .map { pointer ->
+                        val pp = MotionEvent.PointerProperties()
+                        pp.id = pointer.id
+                        pp
+                    }
+                    .toList()
+
+            val pointerCoords =
+                currentPointers
+                    .map { pointer ->
+                        val pc = MotionEvent.PointerCoords()
+                        pc.x = orientation.getNativeX(pointer.onSensor) * scaleFactor
+                        pc.y = orientation.getNativeY(pointer.onSensor) * scaleFactor
+                        pc.touchMinor = NATIVE_MINOR * scaleFactor
+                        pc.touchMajor = NATIVE_MAJOR * scaleFactor
+                        pc.orientation = orientation.nativeOrientation
+                        pc
+                    }
+                    .toList()
+
             val event =
                 MOTION_EVENT.copy(
                     action = motionEventAction,
-                    x = nativeX * scaleFactor,
-                    y = nativeY * scaleFactor,
-                    minor = NATIVE_MINOR * scaleFactor,
-                    major = NATIVE_MAJOR * scaleFactor,
+                    pointerProperties = pointerProperties,
+                    pointerCoords = pointerCoords
                 )
+
             val expectedTouchData =
-                NORMALIZED_TOUCH_DATA.copy(
-                    x = ROTATION_0_INPUTS.getNativeX(isGoodOverlap),
-                    y = ROTATION_0_INPUTS.getNativeY(isGoodOverlap),
-                )
+                if (expectedPointerOnSensorId != INVALID_POINTER_ID) {
+                    NORMALIZED_TOUCH_DATA.copy(
+                        pointerId = expectedPointerOnSensorId,
+                        x = ROTATION_0_INPUTS.getNativeX(isWithinSensor = true),
+                        y = ROTATION_0_INPUTS.getNativeY(isWithinSensor = true)
+                    )
+                } else {
+                    NormalizedTouchData()
+                }
+
             val expected =
                 TouchProcessorResult.ProcessedTouch(
                     event = expectedInteractionEvent,
@@ -418,7 +626,7 @@
                 )
             SinglePointerTouchProcessorTest.TestCase(
                 event = event,
-                isGoodOverlap = isGoodOverlap,
+                currentPointers = currentPointers,
                 previousPointerOnSensorId = previousPointerOnSensorId,
                 overlayParams = overlayParams,
                 expected = expected,
@@ -431,7 +639,7 @@
     motionEventAction: Int
 ): List<SinglePointerTouchProcessorTest.TestCase> {
     val isGoodOverlap = true
-    val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID)
+    val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID_1)
     return previousPointerOnSensorIds.map { previousPointerOnSensorId ->
         val overlayParams = ROTATION_0_INPUTS.toOverlayParams(scaleFactor = 1f)
         val nativeX = ROTATION_0_INPUTS.getNativeX(isGoodOverlap)
@@ -446,7 +654,7 @@
             )
         SinglePointerTouchProcessorTest.TestCase(
             event = event,
-            isGoodOverlap = isGoodOverlap,
+            currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = isGoodOverlap)),
             previousPointerOnSensorId = previousPointerOnSensorId,
             overlayParams = overlayParams,
             expected = TouchProcessorResult.Failure(),
@@ -473,13 +681,23 @@
     pc.touchMinor = minor
     pc.touchMajor = major
     pc.orientation = orientation
+    return obtainMotionEvent(action, arrayOf(pp), arrayOf(pc), time, gestureStart)
+}
+
+private fun obtainMotionEvent(
+    action: Int,
+    pointerProperties: Array<MotionEvent.PointerProperties>,
+    pointerCoords: Array<MotionEvent.PointerCoords>,
+    time: Long,
+    gestureStart: Long,
+): MotionEvent {
     return MotionEvent.obtain(
         gestureStart /* downTime */,
         time /* eventTime */,
         action /* action */,
-        1 /* pointerCount */,
-        arrayOf(pp) /* pointerProperties */,
-        arrayOf(pc) /* pointerCoords */,
+        pointerCoords.size /* pointerCount */,
+        pointerProperties /* pointerProperties */,
+        pointerCoords /* pointerCoords */,
         0 /* metaState */,
         0 /* buttonState */,
         1f /* xPrecision */,
@@ -503,4 +721,19 @@
     gestureStart: Long = this.downTime,
 ) = obtainMotionEvent(action, pointerId, x, y, minor, major, orientation, time, gestureStart)
 
+private fun MotionEvent.copy(
+    action: Int = this.action,
+    pointerProperties: List<MotionEvent.PointerProperties>,
+    pointerCoords: List<MotionEvent.PointerCoords>,
+    time: Long = this.eventTime,
+    gestureStart: Long = this.downTime
+) =
+    obtainMotionEvent(
+        action,
+        pointerProperties.toTypedArray(),
+        pointerCoords.toTypedArray(),
+        time,
+        gestureStart
+    )
+
 private fun Rect.scaled(scaleFactor: Float) = Rect(this).apply { scale(scaleFactor) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java
index a61cecb..3503902 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java
@@ -20,14 +20,14 @@
 
 import static org.mockito.Mockito.mock;
 
-import androidx.test.filters.SmallTest;
-
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
+import androidx.test.filters.SmallTest;
+
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -44,18 +44,21 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class BroadcastDialogTest extends SysuiTestCase {
 
-    private static final String SWITCH_APP = "Music";
+    private static final String CURRENT_BROADCAST_APP = "Music";
+    private static final String SWITCH_APP = "Files by Google";
     private static final String TEST_PACKAGE = "com.google.android.apps.nbu.files";
     private BroadcastDialog mBroadcastDialog;
     private View mDialogView;
+    private TextView mTitle;
     private TextView mSubTitle;
+    private Button mSwitchBroadcastAppButton;
     private Button mChangeOutputButton;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mBroadcastDialog = new BroadcastDialog(mContext, mock(MediaOutputDialogFactory.class),
-                SWITCH_APP, TEST_PACKAGE, mock(UiEventLogger.class));
+                CURRENT_BROADCAST_APP, TEST_PACKAGE, mock(UiEventLogger.class));
         mBroadcastDialog.show();
         mDialogView = mBroadcastDialog.mDialogView;
     }
@@ -66,7 +69,15 @@
     }
 
     @Test
-    public void onCreate_withCurrentApp_checkSwitchAppContent() {
+    public void onCreate_withCurrentApp_titleIsCurrentAppName() {
+        mTitle = mDialogView.requireViewById(R.id.dialog_title);
+
+        assertThat(mTitle.getText().toString()).isEqualTo(mContext.getString(
+                R.string.bt_le_audio_broadcast_dialog_title, CURRENT_BROADCAST_APP));
+    }
+
+    @Test
+    public void onCreate_withCurrentApp_subTitleIsSwitchAppName() {
         mSubTitle = mDialogView.requireViewById(R.id.dialog_subtitle);
 
         assertThat(mSubTitle.getText()).isEqualTo(
@@ -74,6 +85,14 @@
     }
 
     @Test
+    public void onCreate_withCurrentApp_switchBtnIsSwitchAppName() {
+        mSwitchBroadcastAppButton = mDialogView.requireViewById(R.id.switch_broadcast);
+
+        assertThat(mSwitchBroadcastAppButton.getText().toString()).isEqualTo(
+                mContext.getString(R.string.bt_le_audio_broadcast_dialog_switch_app, SWITCH_APP));
+    }
+
+    @Test
     public void onClick_withChangeOutput_dismissBroadcastDialog() {
         mChangeOutputButton = mDialogView.requireViewById(R.id.change_output);
         mChangeOutputButton.performClick();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
index 262b4b8..80c3e5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.ActivityIntentHelper
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -69,6 +70,8 @@
     lateinit var cameraIntents: CameraIntentsWrapper
     @Mock
     lateinit var contentResolver: ContentResolver
+    @Mock
+    lateinit var userTracker: UserTracker
 
     private lateinit var underTest: CameraGestureHelper
 
@@ -96,6 +99,7 @@
             cameraIntents = cameraIntents,
             contentResolver = contentResolver,
             uiExecutor = MoreExecutors.directExecutor(),
+            userTracker = userTracker,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
index d159714..d6cafcb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
@@ -16,18 +16,23 @@
 
 package com.android.systemui.charging
 
+import android.graphics.Rect
 import android.testing.AndroidTestingRunner
+import android.view.Surface
 import android.view.View
 import android.view.WindowManager
+import android.view.WindowMetrics
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.surfaceeffects.ripple.RippleView
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.surfaceeffects.ripple.RippleView
+import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import org.junit.Before
 import org.junit.Test
@@ -35,12 +40,12 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers
 import org.mockito.Mock
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -54,6 +59,7 @@
     @Mock private lateinit var rippleView: RippleView
     @Mock private lateinit var windowManager: WindowManager
     @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var windowMetrics: WindowMetrics
     private val systemClock = FakeSystemClock()
 
     @Before
@@ -66,6 +72,9 @@
         rippleView.setupShader()
         controller.rippleView = rippleView // Replace the real ripple view with a mock instance
         controller.registerCallbacks()
+
+        `when`(windowMetrics.bounds).thenReturn(Rect(0, 0, 100, 100))
+        `when`(windowManager.currentWindowMetrics).thenReturn(windowMetrics)
     }
 
     @Test
@@ -164,4 +173,63 @@
         verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture())
         verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>())
     }
+
+    @Test
+    fun testRipple_layoutsCorrectly() {
+        // Sets the correct ripple size.
+        val width = 100
+        val height = 200
+        whenever(windowMetrics.bounds).thenReturn(Rect(0, 0, width, height))
+
+        // Trigger ripple.
+        val captor = ArgumentCaptor
+                .forClass(BatteryController.BatteryStateChangeCallback::class.java)
+        verify(batteryController).addCallback(captor.capture())
+
+        captor.value.onBatteryLevelChanged(
+                /* unusedBatteryLevel= */ 0,
+                /* plugged in= */ true,
+                /* charging= */ false)
+
+        val attachListenerCaptor =
+                ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
+        verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
+        verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>())
+
+        val runnableCaptor =
+                ArgumentCaptor.forClass(Runnable::class.java)
+        attachListenerCaptor.value.onViewAttachedToWindow(rippleView)
+        verify(rippleView).startRipple(runnableCaptor.capture())
+
+        // Verify size and center position.
+        val maxSize = 400f // Double the max value between width and height.
+        verify(rippleView).setMaxSize(maxWidth = maxSize, maxHeight = maxSize)
+
+        val normalizedPortPosX =
+                context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_x)
+        val normalizedPortPosY =
+                context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_y)
+        val expectedCenterX: Float
+        val expectedCenterY: Float
+        when (context.display.rotation) {
+            Surface.ROTATION_90 -> {
+                expectedCenterX = width * normalizedPortPosY
+                expectedCenterY = height * (1 - normalizedPortPosX)
+            }
+            Surface.ROTATION_180 -> {
+                expectedCenterX = width * (1 - normalizedPortPosX)
+                expectedCenterY = height * (1 - normalizedPortPosY)
+            }
+            Surface.ROTATION_270 -> {
+                expectedCenterX = width * (1 - normalizedPortPosY)
+                expectedCenterY = height * normalizedPortPosX
+            }
+            else -> { // Surface.ROTATION_0
+                expectedCenterX = width * normalizedPortPosX
+                expectedCenterY = height * normalizedPortPosY
+            }
+        }
+
+        verify(rippleView).setCenter(expectedCenterX, expectedCenterY)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
index 0fadc13..8cb9130 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
@@ -106,6 +106,7 @@
         mClassifiers.add(mClassifierB);
         when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mFalsingDataProvider.isUnfolded()).thenReturn(false);
         mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider,
                 mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier,
                 mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController,
@@ -121,6 +122,7 @@
         mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue();
         mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true);
         mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true);
+        mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
index 4281ee0..315774a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
@@ -89,25 +89,27 @@
         mClassifiers.add(mClassifierA);
         when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mFalsingDataProvider.isUnfolded()).thenReturn(false);
         mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider,
                 mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier,
                 mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController,
                 mAccessibilityManager, false, mFakeFeatureFlags);
         mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true);
+        mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true);
     }
 
     @Test
     public void testA11yDisablesGesture() {
-        assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue();
+        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
-        assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
+        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
     }
 
     @Test
     public void testA11yDisablesTap() {
-        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
+        assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue();
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
-        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
+        assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
     }
 
 
@@ -179,4 +181,11 @@
         when(mFalsingDataProvider.isA11yAction()).thenReturn(true);
         assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
     }
+
+    @Test
+    public void testSkipUnfolded() {
+        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
+        when(mFalsingDataProvider.isUnfolded()).thenReturn(true);
+        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
index 5fa7214..94cf384 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.classifier;
 
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.util.DisplayMetrics;
 import android.view.MotionEvent;
 
@@ -38,6 +39,7 @@
     private float mOffsetY = 0;
     @Mock
     private BatteryController mBatteryController;
+    private FoldStateListener mFoldStateListener = new FoldStateListener(mContext);
     private final DockManagerFake mDockManager = new DockManagerFake();
 
     public void setup() {
@@ -47,7 +49,8 @@
         displayMetrics.ydpi = 100;
         displayMetrics.widthPixels = 1000;
         displayMetrics.heightPixels = 1000;
-        mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager);
+        mDataProvider = new FalsingDataProvider(
+                displayMetrics, mBatteryController, mFoldStateListener, mDockManager);
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
index d315c2d..2edc3d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.testing.AndroidTestingRunner;
 import android.util.DisplayMetrics;
 import android.view.MotionEvent;
@@ -50,6 +51,8 @@
     private FalsingDataProvider mDataProvider;
     @Mock
     private BatteryController mBatteryController;
+    @Mock
+    private FoldStateListener mFoldStateListener;
     private final DockManagerFake mDockManager = new DockManagerFake();
 
     @Before
@@ -61,7 +64,8 @@
         displayMetrics.ydpi = 100;
         displayMetrics.widthPixels = 1000;
         displayMetrics.heightPixels = 1000;
-        mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager);
+        mDataProvider = new FalsingDataProvider(
+                displayMetrics, mBatteryController, mFoldStateListener, mDockManager);
     }
 
     @After
@@ -316,4 +320,22 @@
         mDataProvider.onA11yAction();
         assertThat(mDataProvider.isA11yAction()).isTrue();
     }
+
+    @Test
+    public void test_FoldedState_Folded() {
+        when(mFoldStateListener.getFolded()).thenReturn(true);
+        assertThat(mDataProvider.isUnfolded()).isFalse();
+    }
+
+    @Test
+    public void test_FoldedState_Unfolded() {
+        when(mFoldStateListener.getFolded()).thenReturn(false);
+        assertThat(mDataProvider.isUnfolded()).isTrue();
+    }
+
+    @Test
+    public void test_FoldedState_NotFoldable() {
+        when(mFoldStateListener.getFolded()).thenReturn(null);
+        assertThat(mDataProvider.isUnfolded()).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index bdd496e..fd6e31b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.clipboardoverlay;
 
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
 
 import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
 
@@ -33,7 +33,6 @@
 import android.content.ClipDescription;
 import android.content.ClipboardManager;
 import android.os.PersistableBundle;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 
 import androidx.test.filters.SmallTest;
@@ -41,9 +40,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.util.DeviceConfigProxyFake;
+import com.android.systemui.flags.FakeFeatureFlags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -54,6 +51,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
+import java.util.ArrayList;
+
 import javax.inject.Provider;
 
 @SmallTest
@@ -63,18 +62,12 @@
     @Mock
     private ClipboardManager mClipboardManager;
     @Mock
-    private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory;
-    @Mock
-    private ClipboardOverlayControllerLegacy mOverlayControllerLegacy;
-    @Mock
     private ClipboardOverlayController mOverlayController;
     @Mock
     private ClipboardToast mClipboardToast;
+    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     @Mock
     private UiEventLogger mUiEventLogger;
-    @Mock
-    private FeatureFlags mFeatureFlags;
-    private DeviceConfigProxyFake mDeviceConfigProxy;
 
     private ClipData mSampleClipData;
     private String mSampleSource = "Example source";
@@ -97,8 +90,6 @@
         mOverlayControllerProvider = () -> mOverlayController;
 
         MockitoAnnotations.initMocks(this);
-        when(mClipboardOverlayControllerLegacyFactory.create(any()))
-                .thenReturn(mOverlayControllerLegacy);
         when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
         Settings.Secure.putInt(
                 mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1);
@@ -108,26 +99,15 @@
         when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
         when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
 
-        mDeviceConfigProxy = new DeviceConfigProxyFake();
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true);
 
-        mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy,
-                mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
-                mClipboardToast, mClipboardManager, mUiEventLogger, mFeatureFlags);
+        mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
+                mClipboardToast, mClipboardManager, mFeatureFlags, mUiEventLogger);
     }
 
-    @Test
-    public void test_disabled() {
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
-                "false", false);
-        mClipboardListener.start();
-        verifyZeroInteractions(mClipboardManager);
-        verifyZeroInteractions(mUiEventLogger);
-    }
 
     @Test
-    public void test_enabled() {
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
-                "true", false);
+    public void test_initialization() {
         mClipboardListener.start();
         verify(mClipboardManager).addPrimaryClipChangedListener(any());
         verifyZeroInteractions(mUiEventLogger);
@@ -135,45 +115,6 @@
 
     @Test
     public void test_consecutiveCopies() {
-        when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
-
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
-                "true", false);
-        mClipboardListener.start();
-        mClipboardListener.onPrimaryClipChanged();
-
-        verify(mClipboardOverlayControllerLegacyFactory).create(any());
-
-        verify(mOverlayControllerLegacy).setClipData(
-                mClipDataCaptor.capture(), mStringCaptor.capture());
-
-        assertEquals(mSampleClipData, mClipDataCaptor.getValue());
-        assertEquals(mSampleSource, mStringCaptor.getValue());
-
-        verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture());
-
-        // Should clear the overlay controller
-        mRunnableCaptor.getValue().run();
-
-        mClipboardListener.onPrimaryClipChanged();
-
-        verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
-
-        // Not calling the runnable here, just change the clip again and verify that the overlay is
-        // NOT recreated.
-
-        mClipboardListener.onPrimaryClipChanged();
-
-        verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
-        verifyZeroInteractions(mOverlayControllerProvider);
-    }
-
-    @Test
-    public void test_consecutiveCopies_new() {
-        when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
-
-        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
-                "true", false);
         mClipboardListener.start();
         mClipboardListener.onPrimaryClipChanged();
 
@@ -200,7 +141,6 @@
         mClipboardListener.onPrimaryClipChanged();
 
         verify(mOverlayControllerProvider, times(2)).get();
-        verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
     }
 
     @Test
@@ -231,23 +171,6 @@
 
     @Test
     public void test_logging_enterAndReenter() {
-        when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
-
-        mClipboardListener.start();
-
-        mClipboardListener.onPrimaryClipChanged();
-        mClipboardListener.onPrimaryClipChanged();
-
-        verify(mUiEventLogger, times(1)).log(
-                ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
-        verify(mUiEventLogger, times(1)).log(
-                ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
-    }
-
-    @Test
-    public void test_logging_enterAndReenter_new() {
-        when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
-
         mClipboardListener.start();
 
         mClipboardListener.onPrimaryClipChanged();
@@ -271,6 +194,62 @@
                 ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource);
         verify(mClipboardToast, times(1)).showCopiedToast();
         verifyZeroInteractions(mOverlayControllerProvider);
-        verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
+    }
+
+    @Test
+    public void test_nullClipData_showsNothing() {
+        when(mClipboardManager.getPrimaryClip()).thenReturn(null);
+
+        mClipboardListener.start();
+        mClipboardListener.onPrimaryClipChanged();
+
+        verifyZeroInteractions(mUiEventLogger);
+        verifyZeroInteractions(mClipboardToast);
+        verifyZeroInteractions(mOverlayControllerProvider);
+    }
+
+    @Test
+    public void test_emptyClipData_showsToast() {
+        ClipDescription description = new ClipDescription("Test", new String[0]);
+        ClipData noItems = new ClipData(description, new ArrayList<>());
+        when(mClipboardManager.getPrimaryClip()).thenReturn(noItems);
+
+        mClipboardListener.start();
+        mClipboardListener.onPrimaryClipChanged();
+
+        verify(mUiEventLogger, times(1)).log(
+                ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource);
+        verify(mClipboardToast, times(1)).showCopiedToast();
+        verifyZeroInteractions(mOverlayControllerProvider);
+    }
+
+    @Test
+    public void test_minimizedLayoutFlagOff_usesLegacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+        mClipboardListener.start();
+        mClipboardListener.onPrimaryClipChanged();
+
+        verify(mOverlayControllerProvider).get();
+
+        verify(mOverlayController).setClipDataLegacy(
+                mClipDataCaptor.capture(), mStringCaptor.capture());
+
+        assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+        assertEquals(mSampleSource, mStringCaptor.getValue());
+    }
+
+    @Test
+    public void test_minimizedLayoutFlagOn_usesNew() {
+        mClipboardListener.start();
+        mClipboardListener.onPrimaryClipChanged();
+
+        verify(mOverlayControllerProvider).get();
+
+        verify(mOverlayController).setClipData(
+                mClipDataCaptor.capture(), mStringCaptor.capture());
+
+        assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+        assertEquals(mSampleSource, mStringCaptor.getValue());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
new file mode 100644
index 0000000..c0dada4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.clipboardoverlay
+
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.ContentResolver
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.PersistableBundle
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.whenever
+import java.io.IOException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ClipboardModelTest : SysuiTestCase() {
+    @Mock private lateinit var mClipboardUtils: ClipboardOverlayUtils
+    @Mock private lateinit var mMockContext: Context
+    @Mock private lateinit var mMockContentResolver: ContentResolver
+    private lateinit var mSampleClipData: ClipData
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mSampleClipData = ClipData("Test", arrayOf("text/plain"), ClipData.Item("Test Item"))
+    }
+
+    @Test
+    fun test_textClipData() {
+        val source = "test source"
+        val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, source)
+        assertEquals(mSampleClipData, model.clipData)
+        assertEquals(source, model.source)
+        assertEquals(ClipboardModel.Type.TEXT, model.type)
+        assertEquals(mSampleClipData.getItemAt(0).text, model.text)
+        assertEquals(mSampleClipData.getItemAt(0).textLinks, model.textLinks)
+        assertEquals(mSampleClipData.getItemAt(0).uri, model.uri)
+        assertFalse(model.isSensitive)
+        assertFalse(model.isRemote)
+        assertNull(model.loadThumbnail(mContext))
+    }
+
+    @Test
+    fun test_sensitiveExtra() {
+        val description = mSampleClipData.description
+        val b = PersistableBundle()
+        b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
+        description.extras = b
+        val data = ClipData(description, mSampleClipData.getItemAt(0))
+        val (_, _, _, _, _, _, sensitive) =
+            ClipboardModel.fromClipData(mContext, mClipboardUtils, data, "")
+        assertTrue(sensitive)
+    }
+
+    @Test
+    fun test_remoteExtra() {
+        whenever(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true)
+        val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, "")
+        assertTrue(model.isRemote)
+    }
+
+    @Test
+    @Throws(IOException::class)
+    fun test_imageClipData() {
+        val testBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
+        whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
+        whenever(mMockContext.resources).thenReturn(mContext.resources)
+        whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenReturn(testBitmap)
+        whenever(mMockContentResolver.getType(any())).thenReturn("image")
+        val imageClipData =
+            ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
+        val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
+        assertEquals(ClipboardModel.Type.IMAGE, model.type)
+        assertEquals(testBitmap, model.loadThumbnail(mMockContext))
+    }
+
+    @Test
+    @Throws(IOException::class)
+    fun test_imageClipData_loadFailure() {
+        whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
+        whenever(mMockContext.resources).thenReturn(mContext.resources)
+        whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenThrow(IOException())
+        whenever(mMockContentResolver.getType(any())).thenReturn("image")
+        val imageClipData =
+            ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
+        val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
+        assertEquals(ClipboardModel.Type.IMAGE, model.type)
+        assertNull(model.loadThumbnail(mMockContext))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index b4e85c0..2099281 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -16,13 +16,17 @@
 
 package com.android.systemui.clipboardoverlay;
 
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
 import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -35,8 +39,11 @@
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.PersistableBundle;
+import android.view.WindowInsets;
 import android.view.textclassifier.TextLinks;
 
 import androidx.test.filters.SmallTest;
@@ -47,6 +54,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.screenshot.TimeoutHandler;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -81,6 +89,7 @@
     private ClipboardOverlayUtils mClipboardUtils;
     @Mock
     private UiEventLogger mUiEventLogger;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock
@@ -100,11 +109,14 @@
 
         when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
         when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+        when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
+                getImeInsets(new Rect(0, 0, 0, 0)));
 
         mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
                 new ClipData.Item("Test Item"));
 
         mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, false);
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true); // turned off for legacy tests
 
         mOverlayController = new ClipboardOverlayController(
                 mContext,
@@ -127,16 +139,178 @@
     }
 
     @Test
-    public void test_setClipData_nullData() {
-        ClipData clipData = null;
-        mOverlayController.setClipData(clipData, "");
+    public void test_setClipData_invalidImageData_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        ClipData clipData = new ClipData("", new String[]{"image/png"},
+                new ClipData.Item(Uri.parse("")));
+
+        mOverlayController.setClipDataLegacy(clipData, "");
 
         verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
-        verify(mClipboardOverlayView, times(0)).showShareChip();
+        verify(mClipboardOverlayView, times(1)).showShareChip();
         verify(mClipboardOverlayView, times(1)).getEnterAnimation();
     }
 
     @Test
+    public void test_setClipData_nonImageUri_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        ClipData clipData = new ClipData("", new String[]{"resource/png"},
+                new ClipData.Item(Uri.parse("")));
+
+        mOverlayController.setClipDataLegacy(clipData, "");
+
+        verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+        verify(mClipboardOverlayView, times(1)).showShareChip();
+        verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+    }
+
+    @Test
+    public void test_setClipData_textData_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
+        verify(mClipboardOverlayView, times(1)).showShareChip();
+        verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+    }
+
+    @Test
+    public void test_setClipData_sensitiveTextData_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+        ClipDescription description = mSampleClipData.getDescription();
+        PersistableBundle b = new PersistableBundle();
+        b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
+        description.setExtras(b);
+        ClipData data = new ClipData(description, mSampleClipData.getItemAt(0));
+        mOverlayController.setClipDataLegacy(data, "");
+
+        verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
+        verify(mClipboardOverlayView, times(1)).showShareChip();
+        verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+    }
+
+    @Test
+    public void test_setClipData_repeatedCalls_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        when(mAnimator.isRunning()).thenReturn(true);
+
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+    }
+
+    @Test
+    public void test_viewCallbacks_onShareTapped_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        mCallbacks.onShareButtonTapped();
+
+        verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
+        verify(mClipboardOverlayView, times(1)).getExitAnimation();
+    }
+
+    @Test
+    public void test_viewCallbacks_onDismissTapped_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        mCallbacks.onDismissButtonTapped();
+
+        verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "");
+        verify(mClipboardOverlayView, times(1)).getExitAnimation();
+    }
+
+    @Test
+    public void test_multipleDismissals_dismissesOnce_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+        mCallbacks.onSwipeDismissInitiated(mAnimator);
+        mCallbacks.onDismissButtonTapped();
+        mCallbacks.onSwipeDismissInitiated(mAnimator);
+        mCallbacks.onDismissButtonTapped();
+
+        verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED, 0, null);
+        verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+    }
+
+    @Test
+    public void test_remoteCopy_withFlagOn_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+        when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        verify(mTimeoutHandler, never()).resetTimeout();
+    }
+
+    @Test
+    public void test_remoteCopy_withFlagOff_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        verify(mTimeoutHandler).resetTimeout();
+    }
+
+    @Test
+    public void test_nonRemoteCopy_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+        when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false);
+
+        mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+        verify(mTimeoutHandler).resetTimeout();
+    }
+
+    @Test
+    public void test_logsUseLastClipSource_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+        mOverlayController.setClipDataLegacy(mSampleClipData, "first.package");
+        mCallbacks.onDismissButtonTapped();
+        mOverlayController.setClipDataLegacy(mSampleClipData, "second.package");
+        mCallbacks.onDismissButtonTapped();
+
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "first.package");
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "second.package");
+        verifyNoMoreInteractions(mUiEventLogger);
+    }
+
+    @Test
+    public void test_logOnClipboardActionsShown_legacy() {
+        mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+        ClipData.Item item = mSampleClipData.getItemAt(0);
+        item.setTextLinks(Mockito.mock(TextLinks.class));
+        mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+        when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString()))
+                .thenReturn(true);
+        when(mClipboardUtils.getAction(any(ClipData.Item.class), anyString()))
+                .thenReturn(Optional.of(Mockito.mock(RemoteAction.class)));
+        when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                ((Runnable) invocation.getArgument(0)).run();
+                return null;
+            }
+        });
+
+        mOverlayController.setClipDataLegacy(
+                new ClipData(mSampleClipData.getDescription(), item), "actionShownSource");
+        mExecutor.runAllReady();
+
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
+        verifyNoMoreInteractions(mUiEventLogger);
+    }
+
+    // start of refactored setClipData tests
+    @Test
     public void test_setClipData_invalidImageData() {
         ClipData clipData = new ClipData("", new String[]{"image/png"},
                 new ClipData.Item(Uri.parse("")));
@@ -144,7 +318,19 @@
         mOverlayController.setClipData(clipData, "");
 
         verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
-        verify(mClipboardOverlayView, times(0)).showShareChip();
+        verify(mClipboardOverlayView, times(1)).showShareChip();
+        verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+    }
+
+    @Test
+    public void test_setClipData_nonImageUri() {
+        ClipData clipData = new ClipData("", new String[]{"resource/png"},
+                new ClipData.Item(Uri.parse("")));
+
+        mOverlayController.setClipData(clipData, "");
+
+        verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+        verify(mClipboardOverlayView, times(1)).showShareChip();
         verify(mClipboardOverlayView, times(1)).getEnterAnimation();
     }
 
@@ -260,7 +446,7 @@
         mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
         when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString()))
                 .thenReturn(true);
-        when(mClipboardUtils.getAction(any(ClipData.Item.class), anyString()))
+        when(mClipboardUtils.getAction(any(CharSequence.class), any(TextLinks.class), anyString()))
                 .thenReturn(Optional.of(Mockito.mock(RemoteAction.class)));
         when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() {
             @Override
@@ -277,4 +463,43 @@
         verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
         verifyNoMoreInteractions(mUiEventLogger);
     }
+
+    @Test
+    public void test_noInsets_showsExpanded() {
+        mOverlayController.setClipData(mSampleClipData, "");
+
+        verify(mClipboardOverlayView, never()).setMinimized(true);
+        verify(mClipboardOverlayView).setMinimized(false);
+        verify(mClipboardOverlayView).showTextPreview("Test Item", false);
+    }
+
+    @Test
+    public void test_insets_showsMinimized() {
+        when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
+                getImeInsets(new Rect(0, 0, 0, 1)));
+        mOverlayController.setClipData(mSampleClipData, "");
+
+        verify(mClipboardOverlayView).setMinimized(true);
+        verify(mClipboardOverlayView, never()).setMinimized(false);
+        verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean());
+
+        mCallbacks.onMinimizedViewTapped();
+
+        verify(mClipboardOverlayView).setMinimized(false);
+        verify(mClipboardOverlayView).showTextPreview("Test Item", false);
+    }
+
+    @Test
+    public void test_insetsChanged_minimizes() {
+        mOverlayController.setClipData(mSampleClipData, "");
+        verify(mClipboardOverlayView, never()).setMinimized(true);
+
+        WindowInsets insetsWithKeyboard = getImeInsets(new Rect(0, 0, 0, 1));
+        mOverlayController.onInsetsChanged(insetsWithKeyboard, ORIENTATION_PORTRAIT);
+        verify(mClipboardOverlayView).setMinimized(true);
+    }
+
+    private static WindowInsets getImeInsets(Rect r) {
+        return new WindowInsets.Builder().setInsets(WindowInsets.Type.ime(), Insets.of(r)).build();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt
new file mode 100644
index 0000000..d552c9d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.common.coroutine
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** atest SystemUITests:CoroutineResultTest */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class CoroutineResultTest : SysuiTestCase() {
+
+    @Test
+    fun suspendRunCatching_shouldReturnSuccess() = runTest {
+        val actual = suspendRunCatching { "Placeholder" }
+        assertThat(actual.isSuccess).isTrue()
+        assertThat(actual.getOrNull()).isEqualTo("Placeholder")
+    }
+
+    @Test
+    fun suspendRunCatching_whenExceptionThrow_shouldResumeWithException() = runTest {
+        val actual = suspendRunCatching { throw Exception() }
+        assertThat(actual.isFailure).isTrue()
+        assertThat(actual.exceptionOrNull()).isInstanceOf(Exception::class.java)
+    }
+
+    @Test(expected = CancellationException::class)
+    fun suspendRunCatching_whenCancelled_shouldResumeWithException() = runTest {
+        suspendRunCatching { cancel() }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
new file mode 100644
index 0000000..fe352fd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.common.ui.view
+
+import android.view.ViewConfiguration
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Down
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Move
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Up
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class LongPressHandlingViewInteractionHandlerTest : SysuiTestCase() {
+
+    @Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
+    @Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
+    @Mock private lateinit var onSingleTapDetected: () -> Unit
+
+    private lateinit var underTest: LongPressHandlingViewInteractionHandler
+
+    private var isAttachedToWindow: Boolean = true
+    private var delayedRunnable: Runnable? = null
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(postDelayed.invoke(any(), any())).thenAnswer { invocation ->
+            delayedRunnable = invocation.arguments[0] as Runnable
+            DisposableHandle { delayedRunnable = null }
+        }
+
+        underTest =
+            LongPressHandlingViewInteractionHandler(
+                postDelayed = postDelayed,
+                isAttachedToWindow = { isAttachedToWindow },
+                onLongPressDetected = onLongPressDetected,
+                onSingleTapDetected = onSingleTapDetected,
+            )
+        underTest.isLongPressHandlingEnabled = true
+    }
+
+    @Test
+    fun `long-press`() = runTest {
+        val downX = 123
+        val downY = 456
+        dispatchTouchEvents(
+            Down(
+                x = downX,
+                y = downY,
+            ),
+            Move(
+                distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f,
+            ),
+        )
+        delayedRunnable?.run()
+
+        verify(onLongPressDetected).invoke(downX, downY)
+        verify(onSingleTapDetected, never()).invoke()
+    }
+
+    @Test
+    fun `long-press but feature not enabled`() = runTest {
+        underTest.isLongPressHandlingEnabled = false
+        dispatchTouchEvents(
+            Down(
+                x = 123,
+                y = 456,
+            ),
+        )
+
+        assertThat(delayedRunnable).isNull()
+        verify(onLongPressDetected, never()).invoke(any(), any())
+        verify(onSingleTapDetected, never()).invoke()
+    }
+
+    @Test
+    fun `long-press but view not attached`() = runTest {
+        isAttachedToWindow = false
+        dispatchTouchEvents(
+            Down(
+                x = 123,
+                y = 456,
+            ),
+        )
+        delayedRunnable?.run()
+
+        verify(onLongPressDetected, never()).invoke(any(), any())
+        verify(onSingleTapDetected, never()).invoke()
+    }
+
+    @Test
+    fun `dragged too far to be considered a long-press`() = runTest {
+        dispatchTouchEvents(
+            Down(
+                x = 123,
+                y = 456,
+            ),
+            Move(
+                distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f,
+            ),
+        )
+
+        assertThat(delayedRunnable).isNull()
+        verify(onLongPressDetected, never()).invoke(any(), any())
+        verify(onSingleTapDetected, never()).invoke()
+    }
+
+    @Test
+    fun `held down too briefly to be considered a long-press`() = runTest {
+        dispatchTouchEvents(
+            Down(
+                x = 123,
+                y = 456,
+            ),
+            Up(
+                distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
+                gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
+            ),
+        )
+
+        assertThat(delayedRunnable).isNull()
+        verify(onLongPressDetected, never()).invoke(any(), any())
+        verify(onSingleTapDetected).invoke()
+    }
+
+    private fun dispatchTouchEvents(
+        vararg models: MotionEventModel,
+    ) {
+        models.forEach { model -> underTest.onTouchEvent(model) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java
new file mode 100644
index 0000000..eafe727
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.common.ui.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link SeekBarWithIconButtonsView}
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class SeekBarWithIconButtonsViewTest extends SysuiTestCase {
+
+    private ImageView mIconStart;
+    private ImageView mIconEnd;
+    private ViewGroup mIconStartFrame;
+    private ViewGroup mIconEndFrame;
+    private SeekBar mSeekbar;
+    private SeekBarWithIconButtonsView mIconDiscreteSliderLinearLayout;
+
+    @Before
+    public void setUp() {
+        mIconDiscreteSliderLinearLayout = new SeekBarWithIconButtonsView(mContext);
+        mIconStart = mIconDiscreteSliderLinearLayout.findViewById(R.id.icon_start);
+        mIconEnd = mIconDiscreteSliderLinearLayout.findViewById(R.id.icon_end);
+        mIconStartFrame = mIconDiscreteSliderLinearLayout.findViewById(R.id.icon_start_frame);
+        mIconEndFrame = mIconDiscreteSliderLinearLayout.findViewById(R.id.icon_end_frame);
+        mSeekbar = mIconDiscreteSliderLinearLayout.findViewById(R.id.seekbar);
+    }
+
+    @Test
+    public void setSeekBarProgressZero_startIconAndFrameDisabled() {
+        mIconDiscreteSliderLinearLayout.setProgress(0);
+
+        assertThat(mIconStart.isEnabled()).isFalse();
+        assertThat(mIconEnd.isEnabled()).isTrue();
+        assertThat(mIconStartFrame.isEnabled()).isFalse();
+        assertThat(mIconEndFrame.isEnabled()).isTrue();
+    }
+
+    @Test
+    public void setSeekBarProgressMax_endIconAndFrameDisabled() {
+        mIconDiscreteSliderLinearLayout.setProgress(mSeekbar.getMax());
+
+        assertThat(mIconEnd.isEnabled()).isFalse();
+        assertThat(mIconStart.isEnabled()).isTrue();
+        assertThat(mIconEndFrame.isEnabled()).isFalse();
+        assertThat(mIconStartFrame.isEnabled()).isTrue();
+    }
+
+    @Test
+    public void setSeekBarProgressMax_allIconsAndFramesEnabled() {
+        // We are using the default value for the max of seekbar.
+        // Therefore, the max value will be DEFAULT_SEEKBAR_MAX = 6.
+        mIconDiscreteSliderLinearLayout.setProgress(1);
+
+        assertThat(mIconStart.isEnabled()).isTrue();
+        assertThat(mIconEnd.isEnabled()).isTrue();
+        assertThat(mIconStartFrame.isEnabled()).isTrue();
+        assertThat(mIconEndFrame.isEnabled()).isTrue();
+    }
+
+    @Test
+    public void clickIconEnd_currentProgressIsOneToMax_reachesMax() {
+        mIconDiscreteSliderLinearLayout.setProgress(mSeekbar.getMax() - 1);
+
+        mIconEndFrame.performClick();
+
+        assertThat(mSeekbar.getProgress()).isEqualTo(mSeekbar.getMax());
+    }
+
+    @Test
+    public void clickIconStart_currentProgressIsOne_reachesZero() {
+        mIconDiscreteSliderLinearLayout.setProgress(1);
+
+        mIconStartFrame.performClick();
+
+        assertThat(mSeekbar.getProgress()).isEqualTo(0);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt
new file mode 100644
index 0000000..3e6cc3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.compose
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.ViewUtils
+import android.widget.FrameLayout
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ComposeInitializerTest : SysuiTestCase() {
+    @Test
+    fun testCanAddComposeViewInInitializedWindow() {
+        if (!ComposeFacade.isComposeAvailable()) {
+            return
+        }
+
+        val root = TestWindowRoot(context)
+        try {
+            runOnMainThreadAndWaitForIdleSync { ViewUtils.attachView(root) }
+            assertThat(root.isAttachedToWindow).isTrue()
+
+            runOnMainThreadAndWaitForIdleSync { root.addView(ComposeView(context)) }
+        } finally {
+            runOnMainThreadAndWaitForIdleSync { ViewUtils.detachView(root) }
+        }
+    }
+
+    private fun runOnMainThreadAndWaitForIdleSync(f: () -> Unit) {
+        mContext.mainExecutor.execute(f)
+        waitForIdleSync()
+    }
+
+    class TestWindowRoot(context: Context) : FrameLayout(context) {
+        override fun onAttachedToWindow() {
+            super.onAttachedToWindow()
+            ComposeFacade.composeInitializer().onAttachedToWindow(this)
+        }
+
+        override fun onDetachedFromWindow() {
+            super.onDetachedFromWindow()
+            ComposeFacade.composeInitializer().onDetachedFromWindow(this)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
index 0a81c38..ebbe096 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
@@ -269,6 +269,14 @@
     }
 
     @Test
+    fun testBindServiceForPanel() {
+        controller.bindServiceForPanel(TEST_COMPONENT_NAME_1)
+        executor.runAllReady()
+
+        verify(providers[0]).bindServiceForPanel()
+    }
+
+    @Test
     fun testSubscribe() {
         val controlInfo1 = ControlInfo("id_1", "", "", DeviceTypes.TYPE_UNKNOWN)
         val controlInfo2 = ControlInfo("id_2", "", "", DeviceTypes.TYPE_UNKNOWN)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 1b34706..28e80057 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -33,16 +33,15 @@
 import com.android.systemui.controls.ControlStatus
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.Optional
-import java.util.function.Consumer
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -57,7 +56,9 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
@@ -65,8 +66,10 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
+import java.io.File
+import java.util.*
+import java.util.function.Consumer
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -88,6 +91,8 @@
     private lateinit var userTracker: UserTracker
     @Mock
     private lateinit var userFileManager: UserFileManager
+    @Mock
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
 
     @Captor
     private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
@@ -100,8 +105,6 @@
             ArgumentCaptor<ControlsBindingController.LoadCallback>
 
     @Captor
-    private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
-    @Captor
     private lateinit var listingCallbackCaptor:
             ArgumentCaptor<ControlsListingController.ControlsListingCallback>
 
@@ -144,6 +147,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
         `when`(userTracker.userHandle).thenReturn(UserHandle.of(user))
 
         delayableExecutor = FakeExecutor(FakeSystemClock())
@@ -168,15 +172,12 @@
                 listingController,
                 userFileManager,
                 userTracker,
+                authorizedPanelsRepository,
                 Optional.of(persistenceWrapper),
                 mock(DumpManager::class.java)
         )
         controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper
 
-        verify(userTracker).addCallback(
-            capture(userTrackerCallbackCaptor), any()
-        )
-
         verify(listingController).addCallback(capture(listingCallbackCaptor))
     }
 
@@ -224,6 +225,7 @@
                 listingController,
                 userFileManager,
                 userTracker,
+                authorizedPanelsRepository,
                 Optional.of(persistenceWrapper),
                 mock(DumpManager::class.java)
         )
@@ -231,6 +233,26 @@
     }
 
     @Test
+    fun testAddAuthorizedPackagesFromSavedFavoritesOnStart() {
+        clearInvocations(authorizedPanelsRepository)
+        `when`(persistenceWrapper.readFavorites()).thenReturn(listOf(TEST_STRUCTURE_INFO))
+        ControlsControllerImpl(
+                mContext,
+                delayableExecutor,
+                uiController,
+                bindingController,
+                listingController,
+                userFileManager,
+                userTracker,
+                authorizedPanelsRepository,
+                Optional.of(persistenceWrapper),
+                mock(DumpManager::class.java)
+        )
+        verify(authorizedPanelsRepository)
+                .addAuthorizedPanels(setOf(TEST_STRUCTURE_INFO.componentName.packageName))
+    }
+
+    @Test
     fun testOnActionResponse() {
         controller.onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID, ControlAction.RESPONSE_OK)
 
@@ -513,7 +535,7 @@
 
         reset(persistenceWrapper)
 
-        userTrackerCallbackCaptor.value.onUserChanged(otherUser, mContext)
+        controller.changeUser(UserHandle.of(otherUser))
 
         verify(persistenceWrapper).changeFileAndBackupManager(any(), any())
         verify(persistenceWrapper).readFavorites()
@@ -919,6 +941,34 @@
             .getFile(ControlsFavoritePersistenceWrapper.FILE_NAME, context.user.identifier)
         assertThat(userStructure.file).isNotNull()
     }
+
+    @Test
+    fun testBindForPanel() {
+        controller.bindComponentForPanel(TEST_COMPONENT)
+        verify(bindingController).bindServiceForPanel(TEST_COMPONENT)
+    }
+
+    @Test
+    fun testRemoveFavoriteRemovesFavorite() {
+        val componentName = ComponentName(context, "test.Cls")
+        controller.addFavorite(
+                componentName,
+                "test structure",
+                ControlInfo(
+                        controlId = "testId",
+                        controlTitle = "Test Control",
+                        controlSubtitle = "test control subtitle",
+                        deviceType = DeviceTypes.TYPE_LIGHT,
+                ),
+        )
+
+        controller.removeFavorites(componentName)
+        delayableExecutor.runAllReady()
+
+        verify(authorizedPanelsRepository)
+                .removeAuthorizedPanels(eq(setOf(componentName.packageName)))
+        assertThat(controller.getFavorites()).isEmpty()
+    }
 }
 
 private class DidRunRunnable() : Runnable {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
index af3f24a..da548f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
@@ -105,6 +105,22 @@
     }
 
     @Test
+    fun testBindForPanel() {
+        manager.bindServiceForPanel()
+        executor.runAllReady()
+        assertTrue(context.isBound(componentName))
+    }
+
+    @Test
+    fun testUnbindPanelIsUnbound() {
+        manager.bindServiceForPanel()
+        executor.runAllReady()
+        manager.unbindService()
+        executor.runAllReady()
+        assertFalse(context.isBound(componentName))
+    }
+
+    @Test
     fun testNullBinding() {
         val mockContext = mock(Context::class.java)
         lateinit var serviceConnection: ServiceConnection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt
index 4439586..2283746 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt
@@ -18,9 +18,13 @@
 
 import android.app.job.JobParameters
 import android.content.Context
+import android.os.PersistableBundle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper.DeletionJobService.Companion.USER
+import com.android.systemui.util.mockito.whenever
+import java.util.concurrent.TimeUnit
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -28,18 +32,15 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import java.util.concurrent.TimeUnit
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class DeletionJobServiceTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var context: Context
+    @Mock private lateinit var context: Context
 
     private lateinit var service: AuxiliaryPersistenceWrapper.DeletionJobService
 
@@ -53,6 +54,10 @@
 
     @Test
     fun testOnStartJob() {
+        val bundle = PersistableBundle().also { it.putInt(USER, 0) }
+        val params = mock(JobParameters::class.java)
+        whenever(params.getExtras()).thenReturn(bundle)
+
         // false means job is terminated
         assertFalse(service.onStartJob(mock(JobParameters::class.java)))
         verify(context).deleteFile(AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
@@ -67,13 +72,17 @@
     @Test
     fun testJobHasRightParameters() {
         val userId = 10
-        `when`(context.userId).thenReturn(userId)
-        `when`(context.packageName).thenReturn(mContext.packageName)
+        whenever(context.userId).thenReturn(userId)
+        whenever(context.packageName).thenReturn(mContext.packageName)
 
-        val jobInfo = AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
+        val jobInfo =
+            AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context, userId)
         assertEquals(
-            AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId, jobInfo.id)
+            AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId,
+            jobInfo.id
+        )
         assertTrue(jobInfo.isPersisted)
+        assertEquals(userId, jobInfo.getExtras().getInt(USER))
         assertEquals(TimeUnit.DAYS.toMillis(7), jobInfo.minLatencyMillis)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
index 765c4c0..226ef3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
@@ -21,12 +21,15 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.LayoutInflater
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.settingslib.core.lifecycle.Lifecycle
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -36,8 +39,10 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import java.text.Collator
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -49,25 +54,18 @@
     @Mock lateinit var lifecycle: Lifecycle
     @Mock lateinit var controlsListingController: ControlsListingController
     @Mock lateinit var layoutInflater: LayoutInflater
-    @Mock lateinit var onAppSelected: (ComponentName?) -> Unit
+    @Mock lateinit var onAppSelected: (ControlsServiceInfo) -> Unit
     @Mock lateinit var favoritesRenderer: FavoritesRenderer
     val resources: Resources = context.resources
     lateinit var adapter: AppAdapter
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        adapter = AppAdapter(backgroundExecutor,
-            uiExecutor,
-            lifecycle,
-            controlsListingController,
-            layoutInflater,
-            onAppSelected,
-            favoritesRenderer,
-            resources)
     }
 
     @Test
     fun testOnServicesUpdated_nullLoadLabel() {
+        adapter = createAdapterWithAuthorizedPanels(emptySet())
         val captor = ArgumentCaptor
             .forClass(ControlsListingController.ControlsListingCallback::class.java)
         val controlsServiceInfo = mock<ControlsServiceInfo>()
@@ -76,14 +74,14 @@
         verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
 
         captor.value.onServicesUpdated(serviceInfo)
-        backgroundExecutor.runAllReady()
-        uiExecutor.runAllReady()
+        FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
 
         assertThat(adapter.itemCount).isEqualTo(serviceInfo.size)
     }
 
     @Test
-    fun testOnServicesUpdatedDoesntHavePanels() {
+    fun testOnServicesUpdated_showsNotAuthorizedPanels() {
+        adapter = createAdapterWithAuthorizedPanels(emptySet())
         val captor = ArgumentCaptor
                 .forClass(ControlsListingController.ControlsListingCallback::class.java)
         val serviceInfo = listOf(
@@ -93,20 +91,88 @@
         verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
 
         captor.value.onServicesUpdated(serviceInfo)
-        backgroundExecutor.runAllReady()
-        uiExecutor.runAllReady()
+        FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+
+        assertThat(adapter.itemCount).isEqualTo(2)
+    }
+
+    @Test
+    fun testOnServicesUpdated_doesntShowAuthorizedPanels() {
+        adapter = createAdapterWithAuthorizedPanels(setOf(TEST_PACKAGE))
+
+        val captor = ArgumentCaptor
+                .forClass(ControlsListingController.ControlsListingCallback::class.java)
+        val serviceInfo = listOf(
+                ControlsServiceInfo("no panel", null),
+                ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls"))
+        )
+        verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
+
+        captor.value.onServicesUpdated(serviceInfo)
+        FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
 
         assertThat(adapter.itemCount).isEqualTo(1)
     }
 
-    fun ControlsServiceInfo(
-        label: CharSequence,
-        panelComponentName: ComponentName? = null
-    ): ControlsServiceInfo {
-        return mock {
-            `when`(this.loadLabel()).thenReturn(label)
-            `when`(this.panelActivity).thenReturn(panelComponentName)
-            `when`(this.loadIcon()).thenReturn(mock())
+    @Test
+    fun testOnBindSetsClickListenerToCallOnAppSelected() {
+        adapter = createAdapterWithAuthorizedPanels(emptySet())
+
+        val captor = ArgumentCaptor
+                .forClass(ControlsListingController.ControlsListingCallback::class.java)
+        val serviceInfo = listOf(
+                ControlsServiceInfo("no panel", null),
+                ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls"))
+        )
+        verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
+
+        captor.value.onServicesUpdated(serviceInfo)
+        FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+
+        val sorted = serviceInfo.sortedWith(
+                compareBy(Collator.getInstance(resources.configuration.locales[0])) {
+                    it.loadLabel() ?: ""
+                })
+
+        sorted.forEachIndexed { index, info ->
+            val fakeView: View = mock()
+            val fakeHolder: AppAdapter.Holder = mock()
+            `when`(fakeHolder.view).thenReturn(fakeView)
+
+            clearInvocations(onAppSelected)
+            adapter.onBindViewHolder(fakeHolder, index)
+            val listenerCaptor: ArgumentCaptor<View.OnClickListener> = argumentCaptor()
+            verify(fakeView).setOnClickListener(capture(listenerCaptor))
+            listenerCaptor.value.onClick(fakeView)
+
+            verify(onAppSelected).invoke(info)
         }
     }
+
+    private fun createAdapterWithAuthorizedPanels(packages: Set<String>): AppAdapter {
+        return AppAdapter(backgroundExecutor,
+                uiExecutor,
+                lifecycle,
+                controlsListingController,
+                layoutInflater,
+                onAppSelected,
+                favoritesRenderer,
+                resources,
+                packages)
+    }
+
+    companion object {
+        private fun ControlsServiceInfo(
+                label: CharSequence,
+                panelComponentName: ComponentName? = null
+        ): ControlsServiceInfo {
+            return mock {
+                `when`(loadLabel()).thenReturn(label)
+                `when`(panelActivity).thenReturn(panelComponentName)
+                `when`(loadIcon()).thenReturn(mock())
+            }
+        }
+
+        private const val TEST_PACKAGE = "package"
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
index 56c3efe..8dfd223 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
@@ -16,7 +16,13 @@
 
 package com.android.systemui.controls.management
 
+import android.app.Dialog
+import android.content.ComponentName
 import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
+import android.os.Bundle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.window.OnBackInvokedCallback
@@ -25,14 +31,23 @@
 import androidx.test.rule.ActivityTestRule
 import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
-import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -41,7 +56,11 @@
 import org.mockito.ArgumentMatchers
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -58,9 +77,10 @@
 
     @Mock lateinit var userTracker: UserTracker
 
-    @Mock lateinit var uiController: ControlsUiController
+    @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
 
-    private lateinit var controlsProviderSelectorActivity: ControlsProviderSelectorActivity_Factory
+    @Mock lateinit var dialogFactory: PanelConfirmationDialogFactory
+
     private var latch: CountDownLatch = CountDownLatch(1)
 
     @Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
@@ -81,7 +101,8 @@
                         listingController,
                         controlsController,
                         userTracker,
-                        uiController,
+                        authorizedPanelsRepository,
+                        dialogFactory,
                         mockDispatcher,
                         latch
                     )
@@ -113,13 +134,99 @@
         verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value)
     }
 
-    public class TestableControlsProviderSelectorActivity(
+    @Test
+    fun testOnAppSelectedForNonPanelStartsFavoritingActivity() {
+        val info = ControlsServiceInfo(ComponentName("test_pkg", "service"), "", null)
+        activityRule.activity.onAppSelected(info)
+
+        verifyNoMoreInteractions(dialogFactory)
+
+        assertThat(activityRule.activity.lastStartedActivity?.component?.className)
+            .isEqualTo(ControlsFavoritingActivity::class.java.name)
+
+        assertThat(activityRule.activity.triedToFinish).isTrue()
+    }
+
+    @Test
+    fun testOnAppSelectedForPanelTriggersDialog() {
+        val label = "label"
+        val info =
+            ControlsServiceInfo(
+                ComponentName("test_pkg", "service"),
+                label,
+                ComponentName("test_pkg", "activity")
+            )
+
+        val dialog: Dialog = mock()
+        whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
+
+        activityRule.activity.onAppSelected(info)
+        verify(dialogFactory).createConfirmationDialog(any(), eq(label), any())
+        verify(dialog).show()
+
+        assertThat(activityRule.activity.triedToFinish).isFalse()
+    }
+
+    @Test
+    fun dialogAcceptAddsPackage() {
+        val label = "label"
+        val info =
+            ControlsServiceInfo(
+                ComponentName("test_pkg", "service"),
+                label,
+                ComponentName("test_pkg", "activity")
+            )
+
+        val dialog: Dialog = mock()
+        whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
+
+        activityRule.activity.onAppSelected(info)
+
+        val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor()
+        verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor))
+
+        captor.value.accept(true)
+
+        val setCaptor: ArgumentCaptor<Set<String>> = argumentCaptor()
+        verify(authorizedPanelsRepository).addAuthorizedPanels(capture(setCaptor))
+        assertThat(setCaptor.value).containsExactly(info.componentName.packageName)
+
+        assertThat(activityRule.activity.triedToFinish).isTrue()
+    }
+
+    @Test
+    fun dialogCancelDoesntAddPackage() {
+        val label = "label"
+        val info =
+            ControlsServiceInfo(
+                ComponentName("test_pkg", "service"),
+                label,
+                ComponentName("test_pkg", "activity")
+            )
+
+        val dialog: Dialog = mock()
+        whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
+
+        activityRule.activity.onAppSelected(info)
+
+        val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor()
+        verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor))
+
+        captor.value.accept(false)
+
+        verify(authorizedPanelsRepository, never()).addAuthorizedPanels(any())
+
+        assertThat(activityRule.activity.triedToFinish).isFalse()
+    }
+
+    class TestableControlsProviderSelectorActivity(
         executor: Executor,
         backExecutor: Executor,
         listingController: ControlsListingController,
         controlsController: ControlsController,
         userTracker: UserTracker,
-        uiController: ControlsUiController,
+        authorizedPanelsRepository: AuthorizedPanelsRepository,
+        dialogFactory: PanelConfirmationDialogFactory,
         private val mockDispatcher: OnBackInvokedDispatcher,
         private val latch: CountDownLatch
     ) :
@@ -129,16 +236,50 @@
             listingController,
             controlsController,
             userTracker,
-            uiController
+            authorizedPanelsRepository,
+            dialogFactory
         ) {
+
+        var lastStartedActivity: Intent? = null
+        var triedToFinish = false
+
         override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
             return mockDispatcher
         }
 
+        override fun startActivity(intent: Intent?, options: Bundle?) {
+            lastStartedActivity = intent
+        }
+
         override fun onStop() {
             super.onStop()
             // ensures that test runner thread does not proceed until ui thread is done
             latch.countDown()
         }
+
+        override fun animateExitAndFinish() {
+            // Activity should only be finished from the rule.
+            triedToFinish = true
+        }
+    }
+
+    companion object {
+        private fun ControlsServiceInfo(
+            componentName: ComponentName,
+            label: CharSequence,
+            panelComponentName: ComponentName? = null
+        ): ControlsServiceInfo {
+            val serviceInfo =
+                ServiceInfo().apply {
+                    applicationInfo = ApplicationInfo()
+                    packageName = componentName.packageName
+                    name = componentName.className
+                }
+            return Mockito.spy(ControlsServiceInfo(mock(), serviceInfo)).apply {
+                doReturn(label).`when`(this).loadLabel()
+                doReturn(mock<Drawable>()).`when`(this).loadIcon()
+                doReturn(panelComponentName).`when`(this).panelActivity
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
new file mode 100644
index 0000000..756f267
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.controls.management
+
+import android.content.DialogInterface
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PanelConfirmationDialogFactoryTest : SysuiTestCase() {
+
+    @Test
+    fun testDialogHasCorrectInfo() {
+        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+        val factory = PanelConfirmationDialogFactory { mockDialog }
+        val appName = "appName"
+
+        factory.createConfirmationDialog(context, appName) {}
+
+        verify(mockDialog).setCanceledOnTouchOutside(true)
+        verify(mockDialog)
+            .setTitle(context.getString(R.string.controls_panel_authorization_title, appName))
+        verify(mockDialog)
+            .setMessage(context.getString(R.string.controls_panel_authorization, appName))
+    }
+
+    @Test
+    fun testDialogPositiveButton() {
+        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+        val factory = PanelConfirmationDialogFactory { mockDialog }
+
+        var response: Boolean? = null
+
+        factory.createConfirmationDialog(context, "") { response = it }
+
+        val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
+        verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor))
+
+        captor.value.onClick(mockDialog, DialogInterface.BUTTON_POSITIVE)
+
+        assertThat(response).isTrue()
+    }
+
+    @Test
+    fun testDialogNeutralButton() {
+        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+        val factory = PanelConfirmationDialogFactory { mockDialog }
+
+        var response: Boolean? = null
+
+        factory.createConfirmationDialog(context, "") { response = it }
+
+        val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
+        verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor))
+
+        captor.value.onClick(mockDialog, DialogInterface.BUTTON_NEUTRAL)
+
+        assertThat(response).isFalse()
+    }
+
+    @Test
+    fun testDialogCancel() {
+        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+        val factory = PanelConfirmationDialogFactory { mockDialog }
+
+        var response: Boolean? = null
+
+        factory.createConfirmationDialog(context, "") { response = it }
+
+        val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor()
+        verify(mockDialog).setOnCancelListener(capture(captor))
+
+        captor.value.onCancel(mockDialog)
+
+        assertThat(response).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
new file mode 100644
index 0000000..7ac1953
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.controls.panels
+
+import android.content.SharedPreferences
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var userTracker: UserTracker
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mContext.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf<String>()
+        )
+        whenever(userTracker.userId).thenReturn(0)
+    }
+
+    @Test
+    fun testPreApprovedPackagesAreSeededIfNoSavedPreferences() {
+        mContext.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE)
+        )
+        val sharedPrefs = FakeSharedPreferences()
+        val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+        val repository = createRepository(fileManager)
+
+        assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
+        assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE)
+    }
+
+    @Test
+    fun testPreApprovedPackagesNotSeededIfEmptySavedPreferences() {
+        mContext.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE)
+        )
+        val sharedPrefs = FakeSharedPreferences()
+        sharedPrefs.edit().putStringSet(KEY, emptySet()).apply()
+        val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+        createRepository(fileManager)
+
+        assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty()
+    }
+
+    @Test
+    fun testPreApprovedPackagesOnlySetForUserThatDoesntHaveThem() {
+        mContext.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE)
+        )
+        val sharedPrefs_0 = FakeSharedPreferences()
+        val sharedPrefs_1 = FakeSharedPreferences()
+        sharedPrefs_1.edit().putStringSet(KEY, emptySet()).apply()
+        val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs_0, 1 to sharedPrefs_1))
+        val repository = createRepository(fileManager)
+
+        assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
+        whenever(userTracker.userId).thenReturn(1)
+        assertThat(repository.getAuthorizedPanels()).isEmpty()
+    }
+
+    @Test
+    fun testGetAuthorizedPackages() {
+        val sharedPrefs = FakeSharedPreferences()
+        sharedPrefs.edit().putStringSet(KEY, mutableSetOf(TEST_PACKAGE)).apply()
+        val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+
+        val repository = createRepository(fileManager)
+        assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
+    }
+
+    @Test
+    fun testSetAuthorizedPackage() {
+        val sharedPrefs = FakeSharedPreferences()
+        val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+
+        val repository = createRepository(fileManager)
+        repository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+        assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE)
+    }
+
+    @Test
+    fun testRemoveAuthorizedPackageRemovesIt() {
+        val sharedPrefs = FakeSharedPreferences()
+        val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+        val repository = createRepository(fileManager)
+        repository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+
+        repository.removeAuthorizedPanels(setOf(TEST_PACKAGE))
+
+        assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty()
+    }
+
+    private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl {
+        return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker)
+    }
+
+    private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) :
+        UserFileManager {
+        override fun getFile(fileName: String, userId: Int): File {
+            throw UnsupportedOperationException()
+        }
+
+        override fun getSharedPreferences(
+            fileName: String,
+            mode: Int,
+            userId: Int
+        ): SharedPreferences {
+            if (fileName != FILE_NAME) {
+                throw IllegalArgumentException("Preference files must be $FILE_NAME")
+            }
+            return sharedPrefs.getValue(userId)
+        }
+    }
+
+    companion object {
+        private const val FILE_NAME = "controls_prefs"
+        private const val KEY = "authorized_panels"
+        private const val TEST_PACKAGE = "package"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
new file mode 100644
index 0000000..7ecaca6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
@@ -0,0 +1,281 @@
+/*
+ * 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.controls.start
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.SelectedItem
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsStartableTest : SysuiTestCase() {
+
+    @Mock private lateinit var controlsController: ControlsController
+    @Mock private lateinit var controlsListingController: ControlsListingController
+    @Mock private lateinit var userTracker: UserTracker
+
+    private lateinit var fakeExecutor: FakeExecutor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf<String>()
+        )
+
+        fakeExecutor = FakeExecutor(FakeSystemClock())
+    }
+
+    @Test
+    fun testDisabledNothingIsCalled() {
+        createStartable(enabled = false).start()
+
+        verifyZeroInteractions(controlsController, controlsListingController, userTracker)
+    }
+
+    @Test
+    fun testNoPreferredPackagesNoDefaultSelected_noNewSelection() {
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController, never()).setPreferredSelection(any())
+    }
+
+    @Test
+    fun testPreferredPackagesNotInstalled_noNewSelection() {
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE_PANEL)
+        )
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+        `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList())
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController, never()).setPreferredSelection(any())
+    }
+
+    @Test
+    fun testPreferredPackageNotPanel_noNewSelection() {
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE_PANEL)
+        )
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController, never()).setPreferredSelection(any())
+    }
+
+    @Test
+    fun testExistingSelection_noNewSelection() {
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE_PANEL)
+        )
+        `when`(controlsController.getPreferredSelection())
+            .thenReturn(mock<SelectedItem.PanelItem>())
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController, never()).setPreferredSelection(any())
+    }
+
+    @Test
+    fun testPanelAdded() {
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE_PANEL)
+        )
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
+    }
+
+    @Test
+    fun testMultiplePreferredOnlyOnePanel_panelAdded() {
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf("other_package", TEST_PACKAGE_PANEL)
+        )
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+        val listings =
+            listOf(
+                ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true),
+                ControlsServiceInfo(ComponentName("other_package", "cls"), "non panel", false)
+            )
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
+    }
+
+    @Test
+    fun testMultiplePreferredMultiplePanels_firstPreferredAdded() {
+        context.orCreateTestableResources.addOverride(
+            R.array.config_controlsPreferredPackages,
+            arrayOf(TEST_PACKAGE_PANEL, "other_package")
+        )
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+        val listings =
+            listOf(
+                ControlsServiceInfo(ComponentName("other_package", "cls"), "panel", true),
+                ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)
+            )
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController).setPreferredSelection(listings[1].toPanelItem())
+    }
+
+    @Test
+    fun testPreferredSelectionIsPanel_bindOnStart() {
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+        `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem())
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL)
+    }
+
+    @Test
+    fun testPreferredSelectionPanel_listingNoPanel_notBind() {
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+        `when`(controlsController.getPreferredSelection())
+            .thenReturn(SelectedItem.PanelItem("panel", TEST_COMPONENT_PANEL))
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController, never()).bindComponentForPanel(any())
+    }
+
+    @Test
+    fun testNotPanelSelection_noBind() {
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
+        `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+        `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+
+        createStartable(enabled = true).start()
+
+        verify(controlsController, never()).bindComponentForPanel(any())
+    }
+
+    private fun createStartable(enabled: Boolean): ControlsStartable {
+        val component: ControlsComponent =
+            mock() {
+                `when`(isEnabled()).thenReturn(enabled)
+                if (enabled) {
+                    `when`(getControlsController()).thenReturn(Optional.of(controlsController))
+                    `when`(getControlsListingController())
+                        .thenReturn(Optional.of(controlsListingController))
+                } else {
+                    `when`(getControlsController()).thenReturn(Optional.empty())
+                    `when`(getControlsListingController()).thenReturn(Optional.empty())
+                }
+            }
+        return ControlsStartable(context.resources, fakeExecutor, component, userTracker)
+    }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private fun ControlsServiceInfo.toPanelItem(): SelectedItem.PanelItem {
+            if (panelActivity == null) {
+                throw IllegalArgumentException("$this is not a panel")
+            }
+            return SelectedItem.PanelItem(loadLabel(), componentName)
+        }
+
+        private const val TEST_PACKAGE = "pkg"
+        private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
new file mode 100644
index 0000000..1e8cd41
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.controls.ui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.FakeSystemUIDialogController
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsDialogsFactoryTest : SysuiTestCase() {
+
+    private companion object {
+        const val APP_NAME = "Test App"
+    }
+
+    private val fakeDialogController = FakeSystemUIDialogController()
+
+    private lateinit var underTest: ControlsDialogsFactory
+
+    @Before
+    fun setup() {
+        underTest = ControlsDialogsFactory { fakeDialogController.dialog }
+    }
+
+    @Test
+    fun testCreatesRemoveAppDialog() {
+        val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {}
+
+        verify(dialog)
+            .setTitle(
+                eq(context.getString(R.string.controls_panel_remove_app_authorization, APP_NAME))
+            )
+        verify(dialog).setCanceledOnTouchOutside(eq(true))
+    }
+
+    @Test
+    fun testPositiveClickRemoveAppDialogWorks() {
+        var dialogResult: Boolean? = null
+        underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+
+        fakeDialogController.clickPositive()
+
+        assertThat(dialogResult).isTrue()
+    }
+
+    @Test
+    fun testNeutralClickRemoveAppDialogWorks() {
+        var dialogResult: Boolean? = null
+        underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+
+        fakeDialogController.clickNeutral()
+
+        assertThat(dialogResult).isFalse()
+    }
+
+    @Test
+    fun testCancelRemoveAppDialogWorks() {
+        var dialogResult: Boolean? = null
+        underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+
+        fakeDialogController.cancel()
+
+        assertThat(dialogResult).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index d172c9a..23faa99 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
 import android.content.pm.ServiceInfo
 import android.os.UserHandle
 import android.service.controls.ControlsProviderService
@@ -28,6 +29,7 @@
 import android.util.AttributeSet
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
@@ -39,8 +41,10 @@
 import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
 import com.android.systemui.controls.settings.FakeControlsSettingsRepository
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
@@ -48,6 +52,7 @@
 import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.FakeSystemUIDialogController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
@@ -59,21 +64,20 @@
 import com.android.wm.shell.TaskView
 import com.android.wm.shell.TaskViewFactory
 import com.google.common.truth.Truth.assertThat
-import dagger.Lazy
-import java.util.Optional
-import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
+import java.util.Optional
+import java.util.function.Consumer
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -91,13 +95,18 @@
     @Mock lateinit var userTracker: UserTracker
     @Mock lateinit var taskViewFactory: TaskViewFactory
     @Mock lateinit var dumpManager: DumpManager
-    val sharedPreferences = FakeSharedPreferences()
-    lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+    @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    @Mock lateinit var featureFlags: FeatureFlags
+    @Mock lateinit var packageManager: PackageManager
 
-    var uiExecutor = FakeExecutor(FakeSystemClock())
-    var bgExecutor = FakeExecutor(FakeSystemClock())
-    lateinit var underTest: ControlsUiControllerImpl
-    lateinit var parent: FrameLayout
+    private val sharedPreferences = FakeSharedPreferences()
+    private val fakeDialogController = FakeSystemUIDialogController()
+    private val uiExecutor = FakeExecutor(FakeSystemClock())
+    private val bgExecutor = FakeExecutor(FakeSystemClock())
+
+    private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+    private lateinit var parent: FrameLayout
+    private lateinit var underTest: ControlsUiControllerImpl
 
     @Before
     fun setup() {
@@ -118,11 +127,12 @@
 
         underTest =
             ControlsUiControllerImpl(
-                Lazy { controlsController },
+                { controlsController },
                 context,
+                packageManager,
                 uiExecutor,
                 bgExecutor,
-                Lazy { controlsListingController },
+                { controlsListingController },
                 controlActionCoordinator,
                 activityStarter,
                 iconCache,
@@ -132,7 +142,10 @@
                 userTracker,
                 Optional.of(taskViewFactory),
                 controlsSettingsRepository,
-                dumpManager
+                authorizedPanelsRepository,
+                featureFlags,
+                ControlsDialogsFactory { fakeDialogController.dialog },
+                dumpManager,
             )
         `when`(
                 userFileManager.getSharedPreferences(
@@ -229,9 +242,20 @@
     }
 
     @Test
+    fun testPanelBindsForPanel() {
+        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        setUpPanel(panel)
+
+        underTest.show(parent, {}, context)
+        verify(controlsController).bindComponentForPanel(panel.componentName)
+    }
+
+    @Test
     fun testPanelCallsTaskViewFactoryCreate() {
         mockLayoutInflater()
-        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val packageName = "pkg"
+        `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
+        val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls"))
         val serviceInfo = setUpPanel(panel)
 
         underTest.show(parent, {}, context)
@@ -249,9 +273,11 @@
     @Test
     fun testPanelControllerStartActivityWithCorrectArguments() {
         mockLayoutInflater()
+        val packageName = "pkg"
+        `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
         controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
 
-        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls"))
         val serviceInfo = setUpPanel(panel)
 
         underTest.show(parent, {}, context)
@@ -281,9 +307,11 @@
     @Test
     fun testPendingIntentExtrasAreModified() {
         mockLayoutInflater()
+        val packageName = "pkg"
+        `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
         controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
 
-        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls"))
         val serviceInfo = setUpPanel(panel)
 
         underTest.show(parent, {}, context)
@@ -304,7 +332,7 @@
             )
             .isTrue()
 
-        underTest.hide()
+        underTest.hide(parent)
 
         clearInvocations(controlsListingController, taskViewFactory)
         controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(false)
@@ -363,8 +391,67 @@
         assertThat(underTest.resolveActivity()).isEqualTo(ControlsActivity::class.java)
     }
 
+    @Test
+    fun testRemoveViewsOnlyForParentPassedInHide() {
+        underTest.show(parent, {}, context)
+        parent.addView(View(context))
+
+        val mockParent: ViewGroup = mock()
+
+        underTest.hide(mockParent)
+
+        verify(mockParent).removeAllViews()
+        assertThat(parent.childCount).isGreaterThan(0)
+    }
+
+    @Test
+    fun testHideDifferentParentDoesntCancelListeners() {
+        underTest.show(parent, {}, context)
+        underTest.hide(mock())
+
+        verify(controlsController, never()).unsubscribe()
+        verify(controlsListingController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun testRemovingAppsRemovesFavorite() {
+        val componentName = ComponentName(context, "cls")
+        whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true)
+        val panel = SelectedItem.PanelItem("App name", componentName)
+        sharedPreferences
+            .edit()
+            .putString("controls_component", panel.componentName.flattenToString())
+            .putString("controls_structure", panel.appName.toString())
+            .putBoolean("controls_is_panel", true)
+            .commit()
+        underTest.show(parent, {}, context)
+        underTest.startRemovingApp(componentName, "Test App")
+
+        fakeDialogController.clickPositive()
+
+        verify(controlsController).removeFavorites(eq(componentName))
+        assertThat(underTest.getPreferredSelectedItem(emptyList()))
+            .isEqualTo(SelectedItem.EMPTY_SELECTION)
+        with(sharedPreferences) {
+            assertThat(contains("controls_component")).isFalse()
+            assertThat(contains("controls_structure")).isFalse()
+            assertThat(contains("controls_is_panel")).isFalse()
+        }
+    }
+
+    @Test
+    fun testHideCancelsTheRemoveAppDialog() {
+        val componentName = ComponentName(context, "cls")
+        underTest.show(parent, {}, context)
+        underTest.startRemovingApp(componentName, "Test App")
+
+        underTest.hide(parent)
+
+        verify(fakeDialogController.dialog).cancel()
+    }
+
     private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo {
-        val activity = ComponentName("pkg", "activity")
+        val activity = ComponentName(context, "activity")
         sharedPreferences
             .edit()
             .putString("controls_component", panel.componentName.flattenToString())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt
new file mode 100644
index 0000000..dbaf94f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.controls.ui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class OverflowMenuAdapterTest : SysuiTestCase() {
+
+    @Test
+    fun testGetItemId() {
+        val ids = listOf(27L, 73L)
+        val labels = listOf("first", "second")
+        val adapter =
+            OverflowMenuAdapter(
+                context,
+                layoutId = 0,
+                labels.zip(ids).map { OverflowMenuAdapter.MenuItem(it.first, it.second) }
+            ) { true }
+
+        ids.forEachIndexed { index, id -> assertThat(adapter.getItemId(index)).isEqualTo(id) }
+    }
+
+    @Test
+    fun testCheckEnabled() {
+        val ids = listOf(27L, 73L)
+        val labels = listOf("first", "second")
+        val adapter =
+            OverflowMenuAdapter(
+                context,
+                layoutId = 0,
+                labels.zip(ids).map { OverflowMenuAdapter.MenuItem(it.first, it.second) }
+            ) { position -> position == 0 }
+
+        assertThat(adapter.isEnabled(0)).isTrue()
+        assertThat(adapter.isEnabled(1)).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/coroutines/FlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/coroutines/FlowTest.kt
new file mode 100644
index 0000000..1e4753e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/coroutines/FlowTest.kt
@@ -0,0 +1,24 @@
+package com.android.systemui.coroutines
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class FlowTest : SysuiTestCase() {
+
+    @Test
+    fun collectLastValue() = runTest {
+        val flow = flowOf(0, 1, 2)
+        val lastValue by collectLastValue(flow)
+        assertThat(lastValue).isEqualTo(2)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
index 5c2b153..af027e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
@@ -36,6 +37,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerFake;
 import com.android.systemui.doze.DozeMachine.State;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -48,6 +50,7 @@
 @RunWithLooper
 public class DozeDockHandlerTest extends SysuiTestCase {
     @Mock private DozeMachine mMachine;
+    @Mock private UserTracker mUserTracker;
     private AmbientDisplayConfiguration mConfig;
     private DockManagerFake mDockManagerFake;
     private DozeDockHandler mDockHandler;
@@ -57,9 +60,10 @@
         MockitoAnnotations.initMocks(this);
         mConfig = DozeConfigurationUtil.createMockConfig();
         mDockManagerFake = spy(new DockManagerFake());
-        mDockHandler = new DozeDockHandler(mConfig, mDockManagerFake);
+        mDockHandler = new DozeDockHandler(mConfig, mDockManagerFake, mUserTracker);
         mDockHandler.setDozeMachine(mMachine);
 
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
         when(mMachine.getState()).thenReturn(State.DOZE_AOD);
         doReturn(true).when(mConfig).alwaysOnEnabled(anyInt());
         mDockHandler.transitionTo(DozeMachine.State.UNINITIALIZED, DozeMachine.State.INITIALIZED);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 5bbd810..a636b7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -45,6 +45,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
@@ -57,6 +58,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.util.wakelock.WakeLockFake;
 
@@ -85,6 +87,8 @@
     private DozeMachine.Part mPartMock;
     @Mock
     private DozeMachine.Part mAnotherPartMock;
+    @Mock
+    private UserTracker mUserTracker;
     private DozeServiceFake mServiceFake;
     private WakeLockFake mWakeLockFake;
     private AmbientDisplayConfiguration mAmbientDisplayConfigMock;
@@ -97,6 +101,7 @@
         mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class);
         when(mDockManager.isDocked()).thenReturn(false);
         when(mDockManager.isHidden()).thenReturn(false);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
         mMachine = new DozeMachine(mServiceFake,
                 mAmbientDisplayConfigMock,
@@ -105,7 +110,8 @@
                 mDozeLog,
                 mDockManager,
                 mHost,
-                new DozeMachine.Part[]{mPartMock, mAnotherPartMock});
+                new DozeMachine.Part[]{mPartMock, mAnotherPartMock},
+                mUserTracker);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
index 03827da..3af444a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
@@ -34,6 +34,7 @@
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.reset;
@@ -57,6 +58,7 @@
 import com.android.systemui.util.concurrency.FakeThreadFactory;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 import com.android.systemui.util.sensors.FakeSensorManager;
+import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
@@ -94,6 +96,8 @@
     DevicePostureController mDevicePostureController;
     @Mock
     DozeLog mDozeLog;
+    @Mock
+    SystemSettings mSystemSettings;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor);
 
@@ -102,9 +106,8 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        Settings.System.putIntForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS, DEFAULT_BRIGHTNESS,
-                UserHandle.USER_CURRENT);
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
+                eq(UserHandle.USER_CURRENT))).thenReturn(DEFAULT_BRIGHTNESS);
         doAnswer(invocation -> {
             ((Runnable) invocation.getArgument(0)).run();
             return null;
@@ -131,7 +134,8 @@
                 mWakefulnessLifecycle,
                 mDozeParameters,
                 mDevicePostureController,
-                mDozeLog);
+                mDozeLog,
+                mSystemSettings);
     }
 
     @Test
@@ -157,11 +161,10 @@
     }
 
     @Test
-    public void testAod_usesLightSensorRespectingUserSetting() throws Exception {
+    public void testAod_usesLightSensorRespectingUserSetting() {
         int maxBrightness = 3;
-        Settings.System.putIntForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS, maxBrightness,
-                UserHandle.USER_CURRENT);
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
+                eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         assertEquals(maxBrightness, mServiceFake.screenBrightness);
@@ -238,7 +241,8 @@
                 mWakefulnessLifecycle,
                 mDozeParameters,
                 mDevicePostureController,
-                mDozeLog);
+                mDozeLog,
+                mSystemSettings);
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
         reset(mDozeHost);
@@ -275,7 +279,8 @@
                 mWakefulnessLifecycle,
                 mDozeParameters,
                 mDevicePostureController,
-                mDozeLog);
+                mDozeLog,
+                mSystemSettings);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -306,7 +311,8 @@
                 mWakefulnessLifecycle,
                 mDozeParameters,
                 mDevicePostureController,
-                mDozeLog);
+                mDozeLog,
+                mSystemSettings);
 
         // GIVEN the device is in AOD
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
@@ -344,7 +350,8 @@
                 mWakefulnessLifecycle,
                 mDozeParameters,
                 mDevicePostureController,
-                mDozeLog);
+                mDozeLog,
+                mSystemSettings);
 
         // GIVEN device is in AOD
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
@@ -386,7 +393,8 @@
                 mWakefulnessLifecycle,
                 mDozeParameters,
                 mDevicePostureController,
-                mDozeLog);
+                mDozeLog,
+                mSystemSettings);
         verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture());
 
         // GIVEN device is in AOD
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index d910a27..986d6d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -35,11 +35,11 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
 import android.hardware.display.AmbientDisplayConfiguration;
-import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
@@ -120,11 +120,13 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mTestableLooper = TestableLooper.get(this);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
         when(mAmbientDisplayConfiguration.tapSensorTypeMapping())
                 .thenReturn(new String[]{"tapSensor"});
         when(mAmbientDisplayConfiguration.getWakeLockScreenDebounce()).thenReturn(5000L);
         when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true);
-        when(mAmbientDisplayConfiguration.enabled(UserHandle.USER_CURRENT)).thenReturn(true);
+        when(mAmbientDisplayConfiguration.enabled(ActivityManager.getCurrentUser())).thenReturn(
+                true);
         doAnswer(invocation -> {
             ((Runnable) invocation.getArgument(0)).run();
             return null;
@@ -144,7 +146,7 @@
 
     @Test
     public void testSensorDebounce() {
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
 
         mWakeLockScreenListener.onSensorChanged(mock(SensorManagerPlugin.SensorEvent.class));
         mTestableLooper.processAllMessages();
@@ -162,7 +164,7 @@
     @Test
     public void testSetListening_firstTrue_registerSettingsObserver() {
         verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt());
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
 
         verify(mTriggerSensor).registerSettingsObserver(any(ContentObserver.class));
     }
@@ -170,8 +172,8 @@
     @Test
     public void testSetListening_twiceTrue_onlyRegisterSettingsObserverOnce() {
         verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt());
-        mDozeSensors.setListening(true, true);
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
+        mDozeSensors.setListening(true, true, true);
 
         verify(mTriggerSensor, times(1)).registerSettingsObserver(any(ContentObserver.class));
     }
@@ -196,7 +198,7 @@
         assertFalse(mSensorTap.mRequested);
 
         // WHEN we're now in a low powered state
-        dozeSensors.setListening(true, true, true);
+        dozeSensors.setListeningWithPowerState(true, true, true, true);
 
         // THEN the tap sensor is registered
         assertTrue(mSensorTap.mRequested);
@@ -207,12 +209,12 @@
         // GIVEN doze sensors enabled
         when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
 
-        // GIVEN a trigger sensor
+        // GIVEN a trigger sensor that's enabled by settings
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled */ true,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled */ true
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -228,12 +230,12 @@
         // GIVEN doze sensors enabled
         when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
 
-        // GIVEN a trigger sensor
+        // GIVEN a trigger sensor that's not enabled by settings
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled*/ false,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled*/ false
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -249,12 +251,12 @@
         // GIVEN doze sensors enabled
         when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
 
-        // GIVEN a trigger sensor that's
+        // GIVEN a trigger sensor that's not enabled by settings
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled*/ false,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled*/ false
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -264,7 +266,7 @@
         // WHEN ignoreSetting is called
         triggerSensor.ignoreSetting(true);
 
-        // THEN the sensor is registered
+        // THEN the sensor is still registered since the setting is ignore
         assertTrue(triggerSensor.mRegistered);
     }
 
@@ -275,10 +277,10 @@
 
         // GIVEN a trigger sensor
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled*/ true,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled*/ true
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -295,7 +297,7 @@
         // GIVEN doze sensor that supports postures
         Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
         Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 new Sensor[] {
                         null /* unknown */,
                         closedSensor,
@@ -316,7 +318,7 @@
         // GIVEN doze sensor that supports postures
         Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
         Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 new Sensor[] {
                         null /* unknown */,
                         closedSensor,
@@ -345,7 +347,7 @@
         // GIVEN doze sensor that supports postures
         Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
         Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 new Sensor[] {
                         null /* unknown */,
                         closedSensor,
@@ -400,7 +402,7 @@
     public void testUdfpsEnrollmentChanged() throws Exception {
         // GIVEN a UDFPS_LONG_PRESS trigger sensor that's not configured
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 mockSensor,
                 REASON_SENSOR_UDFPS_LONG_PRESS,
                 /* configured */ false);
@@ -409,7 +411,7 @@
                 .thenReturn(true);
 
         // WHEN listening state is set to TRUE
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
 
         // THEN mRegistered is still false b/c !mConfigured
         assertFalse(triggerSensor.mConfigured);
@@ -439,6 +441,35 @@
     }
 
     @Test
+    public void aodOnlySensor_onlyRegisteredWhenAodSensorsIncluded() {
+        // GIVEN doze sensors enabled
+        when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
+
+        // GIVEN a trigger sensor that requires aod
+        Sensor mockSensor = mock(Sensor.class);
+        TriggerSensor aodOnlyTriggerSensor = mDozeSensors.createDozeSensorRequiringAod(mockSensor);
+        when(mSensorManager.requestTriggerSensor(eq(aodOnlyTriggerSensor), eq(mockSensor)))
+                .thenReturn(true);
+        mDozeSensors.addSensor(aodOnlyTriggerSensor);
+
+        // WHEN aod only sensors aren't included
+        mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true,
+                /* includeAodOnlySensors */false);
+
+        // THEN the sensor is not registered or requested
+        assertFalse(aodOnlyTriggerSensor.mRequested);
+        assertFalse(aodOnlyTriggerSensor.mRegistered);
+
+        // WHEN aod only sensors ARE included
+        mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true,
+                /* includeAodOnlySensors */true);
+
+        // THEN the sensor is registered and requested
+        assertTrue(aodOnlyTriggerSensor.mRequested);
+        assertTrue(aodOnlyTriggerSensor.mRegistered);
+    }
+
+    @Test
     public void liftToWake_defaultSetting_configDefaultFalse() {
         // WHEN the default lift to wake gesture setting is false
         when(mResources.getBoolean(
@@ -494,8 +525,8 @@
             mTriggerSensors = new TriggerSensor[] {mTriggerSensor, mSensorTap};
         }
 
-        public TriggerSensor createDozeSensor(Sensor sensor, boolean settingEnabled,
-                boolean requiresTouchScreen) {
+        public TriggerSensor createDozeSensorWithSettingEnabled(Sensor sensor,
+                boolean settingEnabled) {
             return new TriggerSensor(/* sensor */ sensor,
                     /* setting name */ "test_setting",
                     /* settingDefault */ settingEnabled,
@@ -504,11 +535,13 @@
                     /* reportsTouchCoordinate*/ false,
                     /* requiresTouchscreen */ false,
                     /* ignoresSetting */ false,
-                    requiresTouchScreen,
-                    /* immediatelyReRegister */ true);
+                    /* requiresProx */ false,
+                    /* immediatelyReRegister */ true,
+                    /* requiresAod */false
+            );
         }
 
-        public TriggerSensor createDozeSensor(
+        public TriggerSensor createDozeSensorForPosture(
                 Sensor sensor,
                 int pulseReason,
                 boolean configured
@@ -522,15 +555,35 @@
                     /* requiresTouchscreen */ false,
                     /* ignoresSetting */ false,
                     /* requiresTouchScreen */ false,
-                    /* immediatelyReRegister*/ true);
+                    /* immediatelyReRegister*/ true,
+                    false
+            );
         }
 
         /**
-         * create a doze sensor that supports postures and is enabled
+         * Create a doze sensor that requires Aod
          */
-        public TriggerSensor createDozeSensor(Sensor[] sensors, int posture) {
+        public TriggerSensor createDozeSensorRequiringAod(Sensor sensor) {
+            return new TriggerSensor(/* sensor */ sensor,
+                    /* setting name */ "aod_requiring_sensor",
+                    /* settingDefault */ true,
+                    /* configured */ true,
+                    /* pulseReason*/ 0,
+                    /* reportsTouchCoordinate*/ false,
+                    /* requiresTouchscreen */ false,
+                    /* ignoresSetting */ false,
+                    /* requiresProx */ false,
+                    /* immediatelyReRegister */ true,
+                    /* requiresAoD */ true
+            );
+        }
+
+        /**
+         * Create a doze sensor that supports postures and is enabled
+         */
+        public TriggerSensor createDozeSensorForPosture(Sensor[] sensors, int posture) {
             return new TriggerSensor(/* sensor */ sensors,
-                    /* setting name */ "test_setting",
+                    /* setting name */ "posture_test_setting",
                     /* settingDefault */ true,
                     /* configured */ true,
                     /* pulseReason*/ 0,
@@ -539,7 +592,9 @@
                     /* ignoresSetting */ true,
                     /* requiresProx */ false,
                     /* immediatelyReRegister */ true,
-                    posture);
+                    posture,
+                    /* requiresUi */ false
+            );
         }
 
         public void addSensor(TriggerSensor sensor) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index 32b9945..9064470 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
@@ -43,6 +44,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
 import org.junit.After;
@@ -73,6 +75,8 @@
     private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     @Mock
     private BiometricUnlockController mBiometricUnlockController;
+    @Mock
+    private UserTracker mUserTracker;
 
     @Mock
     private DozeMachine mDozeMachine;
@@ -89,12 +93,14 @@
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
         when(mBiometricUnlockController.hasPendingAuthentication()).thenReturn(false);
         when(mDozeHost.isProvisioned()).thenReturn(true);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
         mDozeSuppressor = new DozeSuppressor(
                 mDozeHost,
                 mConfig,
                 mDozeLog,
-                mBiometricUnlockControllerLazy);
+                mBiometricUnlockControllerLazy,
+                mUserTracker);
 
         mDozeSuppressor.setDozeMachine(mDozeMachine);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index b66a454..3552399 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -395,6 +395,14 @@
         verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
     }
 
+
+    @Test
+    public void udfpsLongPress_dozeState_notRegistered() {
+        // GIVEN device is DOZE_AOD_PAUSED
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+        // beverlyt
+    }
+
     private void waitForSensorManager() {
         mExecutor.runAllReady();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 2799a25..6b095ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -36,11 +36,10 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.complication.ComplicationHostViewController;
+import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.statusbar.BlurUtils;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -81,12 +80,6 @@
     BlurUtils mBlurUtils;
 
     @Mock
-    StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-
-    @Mock
-    KeyguardBouncer mBouncer;
-
-    @Mock
     ViewRootImpl mViewRoot;
 
     @Mock
@@ -96,6 +89,9 @@
     DreamOverlayAnimationsController mAnimationsController;
 
     @Mock
+    BouncerlessScrimController mBouncerlessScrimController;
+
+    @Mock
     DreamOverlayStateController mStateController;
 
     DreamOverlayContainerViewController mController;
@@ -106,7 +102,6 @@
 
         when(mDreamOverlayContainerView.getResources()).thenReturn(mResources);
         when(mDreamOverlayContainerView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
-        when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn(mBouncer);
         when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
 
         mController = new DreamOverlayContainerViewController(
@@ -114,7 +109,6 @@
                 mComplicationHostViewController,
                 mDreamOverlayContentView,
                 mDreamOverlayStatusBarViewController,
-                mStatusBarKeyguardViewManager,
                 mBlurUtils,
                 mHandler,
                 mResources,
@@ -123,7 +117,8 @@
                 MILLIS_UNTIL_FULL_JITTER,
                 mPrimaryBouncerCallbackInteractor,
                 mAnimationsController,
-                mStateController);
+                mStateController,
+                mBouncerlessScrimController);
     }
 
     @Test
@@ -170,7 +165,8 @@
         final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
                 ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
         mController.onViewAttached();
-        verify(mBouncer).addBouncerExpansionCallback(bouncerExpansionCaptor.capture());
+        verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
+                bouncerExpansionCaptor.capture());
 
         bouncerExpansionCaptor.getValue().onExpansionChanged(0.5f);
         verify(mBlurUtils, never()).applyBlur(eq(mViewRoot), anyInt(), eq(false));
@@ -181,7 +177,8 @@
         final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
                 ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
         mController.onViewAttached();
-        verify(mBouncer).addBouncerExpansionCallback(bouncerExpansionCaptor.capture());
+        verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
+                bouncerExpansionCaptor.capture());
 
         final float blurRadius = 1337f;
         when(mBlurUtils.blurRadiusOfRatio(anyFloat())).thenReturn(blurRadius);
@@ -217,6 +214,27 @@
     }
 
     @Test
+    public void testSkipEntryAnimationsWhenExitingLowLight() {
+        ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        when(mStateController.isLowLightActive()).thenReturn(false);
+
+        // Call onInit so that the callback is added.
+        mController.onInit();
+        verify(mStateController).addCallback(callbackCaptor.capture());
+
+        // Send the signal that low light is exiting
+        callbackCaptor.getValue().onExitLowLight();
+
+        // View is attached to trigger animations.
+        mController.onViewAttached();
+
+        // Entry animations should be started then immediately ended to skip to the end.
+        verify(mAnimationsController).startEntryAnimations();
+        verify(mAnimationsController).endAnimations();
+    }
+
+    @Test
     public void testCancelDreamEntryAnimationsOnDetached() {
         mController.onViewAttached();
         mController.onViewDetached();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 4568d1e..de70033 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -31,6 +32,8 @@
 import android.os.RemoteException;
 import android.service.dreams.IDreamOverlay;
 import android.service.dreams.IDreamOverlayCallback;
+import android.service.dreams.IDreamOverlayClient;
+import android.service.dreams.IDreamOverlayClientCallback;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.view.ViewGroup;
@@ -58,6 +61,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
@@ -66,6 +70,7 @@
     private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package",
             "lowlight");
     private static final String DREAM_COMPONENT = "package/dream";
+    private static final String WINDOW_NAME = "test";
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
 
@@ -145,16 +150,29 @@
                 mKeyguardUpdateMonitor,
                 mUiEventLogger,
                 LOW_LIGHT_COMPONENT,
-                mDreamOverlayCallbackController);
+                mDreamOverlayCallbackController,
+                WINDOW_NAME);
+    }
+
+    public IDreamOverlayClient getClient() throws RemoteException {
+        final IBinder proxy = mService.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClientCallback callback =
+                Mockito.mock(IDreamOverlayClientCallback.class);
+        overlay.getClient(callback);
+        final ArgumentCaptor<IDreamOverlayClient> clientCaptor =
+                ArgumentCaptor.forClass(IDreamOverlayClient.class);
+        verify(callback).onDreamOverlayClient(clientCaptor.capture());
+
+        return clientCaptor.getValue();
     }
 
     @Test
     public void testOnStartMetricsLogged() throws Exception {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -165,24 +183,47 @@
 
     @Test
     public void testOverlayContainerViewAddedToWindow() throws Exception {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
         verify(mWindowManager).addView(any(), any());
     }
 
+    // Validates that {@link DreamOverlayService} properly handles the case where the dream's
+    // window is no longer valid by the time start is called.
+    @Test
+    public void testInvalidWindowAddStart() throws Exception {
+        final IDreamOverlayClient client = getClient();
+
+        doThrow(new WindowManager.BadTokenException()).when(mWindowManager).addView(any(), any());
+        // Inform the overlay service of dream starting.
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        verify(mWindowManager).addView(any(), any());
+
+        verify(mStateController).setOverlayActive(false);
+        verify(mStateController).setLowLightActive(false);
+        verify(mStateController).setEntryAnimationsFinished(false);
+
+        verify(mStateController, never()).setOverlayActive(true);
+        verify(mUiEventLogger, never()).log(
+                DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+
+        verify(mDreamOverlayCallbackController, never()).onStartDream();
+    }
+
     @Test
     public void testDreamOverlayContainerViewControllerInitialized() throws Exception {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -196,11 +237,10 @@
                 .thenReturn(mDreamOverlayContainerViewParent)
                 .thenReturn(null);
 
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -209,11 +249,10 @@
 
     @Test
     public void testShouldShowComplicationsSetByStartDream() throws RemoteException {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 true /*shouldShowComplication*/);
 
         assertThat(mService.shouldShowComplications()).isTrue();
@@ -221,11 +260,10 @@
 
     @Test
     public void testLowLightSetByStartDream() throws RemoteException {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback,
+        client.startDream(mWindowParams, mDreamOverlayCallback,
                 LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -234,12 +272,36 @@
     }
 
     @Test
-    public void testDestroy() throws RemoteException {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+    public void testOnEndDream() throws RemoteException {
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback,
+        client.startDream(mWindowParams, mDreamOverlayCallback,
+                LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        // Verify view added.
+        verify(mWindowManager).addView(mViewCaptor.capture(), any());
+
+        // Service destroyed.
+        mService.onEndDream();
+        mMainExecutor.runAllReady();
+
+        // Verify view removed.
+        verify(mWindowManager).removeView(mViewCaptor.getValue());
+
+        // Verify state correctly set.
+        verify(mStateController).setOverlayActive(false);
+        verify(mStateController).setLowLightActive(false);
+        verify(mStateController).setEntryAnimationsFinished(false);
+    }
+
+    @Test
+    public void testDestroy() throws RemoteException {
+        final IDreamOverlayClient client = getClient();
+
+        // Inform the overlay service of dream starting.
+        client.startDream(mWindowParams, mDreamOverlayCallback,
                 LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -279,15 +341,14 @@
 
     @Test
     public void testDecorViewNotAddedToWindowAfterDestroy() throws Exception {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Destroy the service.
         mService.onDestroy();
         mMainExecutor.runAllReady();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -305,11 +366,10 @@
 
     @Test
     public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting. Do not show dream complications.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -326,7 +386,7 @@
         // New dream starting with dream complications showing. Note that when a new dream is
         // binding to the dream overlay service, it receives the same instance of IBinder as the
         // first one.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 true /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
@@ -345,11 +405,10 @@
 
     @Test
     public void testWakeUp() throws RemoteException {
-        final IBinder proxy = mService.onBind(new Intent());
-        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        final IDreamOverlayClient client = getClient();
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 true /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index ee989d1..b7d0f29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -251,6 +251,30 @@
     }
 
     @Test
+    public void testNotifyLowLightExit() {
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor, true);
+
+        stateController.addCallback(mCallback);
+        mExecutor.runAllReady();
+        assertThat(stateController.isLowLightActive()).isFalse();
+
+        // Turn low light on then off to trigger the exiting callback.
+        stateController.setLowLightActive(true);
+        stateController.setLowLightActive(false);
+
+        // Callback was only called once, when
+        mExecutor.runAllReady();
+        verify(mCallback, times(1)).onExitLowLight();
+        assertThat(stateController.isLowLightActive()).isFalse();
+
+        // Set with false again, which should not cause the callback to trigger again.
+        stateController.setLowLightActive(false);
+        mExecutor.runAllReady();
+        verify(mCallback, times(1)).onExitLowLight();
+    }
+
+    @Test
     public void testNotifyEntryAnimationsFinishedChanged() {
         final DreamOverlayStateController stateController =
                 new DreamOverlayStateController(mExecutor, true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 85c2819..596b903 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -31,6 +31,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.res.Resources;
@@ -47,6 +48,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -109,6 +111,8 @@
     View mStatusBarItemView;
     @Mock
     DreamOverlayStateController mDreamOverlayStateController;
+    @Mock
+    UserTracker mUserTracker;
 
     @Captor
     private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
@@ -125,6 +129,7 @@
                 .thenReturn(NOTIFICATION_INDICATOR_FORMATTER_STRING);
         doCallRealMethod().when(mView).setVisibility(anyInt());
         doCallRealMethod().when(mView).getVisibility();
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
         mController = new DreamOverlayStatusBarViewController(
                 mView,
@@ -140,7 +145,8 @@
                 mZenModeController,
                 mStatusBarWindowStateController,
                 mDreamOverlayStatusBarItemsProvider,
-                mDreamOverlayStateController);
+                mDreamOverlayStateController,
+                mUserTracker);
     }
 
     @Test
@@ -282,7 +288,8 @@
                 mZenModeController,
                 mStatusBarWindowStateController,
                 mDreamOverlayStatusBarItemsProvider,
-                mDreamOverlayStateController);
+                mDreamOverlayStateController,
+                mUserTracker);
         controller.onViewAttached();
         verify(mView, never()).showIcon(
                 eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
index 9f4a7c8..b3329eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
@@ -32,7 +32,9 @@
 
 import com.android.settingslib.dream.DreamBackend;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.condition.SelfExecutingMonitor;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Monitor;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -66,13 +68,16 @@
 
     private ComplicationTypesUpdater mController;
 
+    private Monitor mMonitor;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mDreamBackend.getEnabledComplications()).thenReturn(new HashSet<>());
 
+        mMonitor = SelfExecutingMonitor.createInstance();
         mController = new ComplicationTypesUpdater(mDreamBackend, mExecutor,
-                mSecureSettings, mDreamOverlayStateController);
+                mSecureSettings, mDreamOverlayStateController, mMonitor);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
index ec448f9..f6662d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
@@ -29,7 +29,9 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.condition.SelfExecutingMonitor;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Monitor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -69,10 +71,13 @@
     @Mock
     private ComplicationLayoutParams mLayoutParams;
 
+    private Monitor mMonitor;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mDreamClockTimeViewHolderProvider.get()).thenReturn(mDreamClockTimeViewHolder);
+        mMonitor = SelfExecutingMonitor.createInstance();
     }
 
     /**
@@ -83,7 +88,8 @@
         final DreamClockTimeComplication.Registrant registrant =
                 new DreamClockTimeComplication.Registrant(
                         mDreamOverlayStateController,
-                        mComplication);
+                        mComplication,
+                        mMonitor);
         registrant.start();
         verify(mDreamOverlayStateController).addComplication(eq(mComplication));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index 89c7280..3312c43 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -31,13 +31,14 @@
 import android.content.Context;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
-import android.widget.ImageView;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.view.LaunchableImageView;
+import com.android.systemui.condition.SelfExecutingMonitor;
 import com.android.systemui.controls.ControlsServiceInfo;
 import com.android.systemui.controls.controller.ControlsController;
 import com.android.systemui.controls.controller.StructureInfo;
@@ -46,6 +47,7 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.shared.condition.Monitor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -90,7 +92,7 @@
     private View mView;
 
     @Mock
-    private ImageView mHomeControlsView;
+    private LaunchableImageView mHomeControlsView;
 
     @Mock
     private ActivityStarter mActivityStarter;
@@ -101,6 +103,8 @@
     @Captor
     private ArgumentCaptor<DreamOverlayStateController.Callback> mStateCallbackCaptor;
 
+    private Monitor mMonitor;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -112,6 +116,8 @@
                 Optional.of(mControlsListingController));
         when(mControlsComponent.getVisibility()).thenReturn(AVAILABLE);
         when(mView.findViewById(R.id.home_controls_chip)).thenReturn(mHomeControlsView);
+
+        mMonitor = SelfExecutingMonitor.createInstance();
     }
 
     @Test
@@ -126,7 +132,7 @@
     public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(false);
@@ -139,7 +145,7 @@
     public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(false);
@@ -152,7 +158,7 @@
     public void complicationAvailability_serviceAvailable_noFavorites_panel_addComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(false);
@@ -165,7 +171,7 @@
     public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(true);
@@ -178,7 +184,7 @@
     public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(true);
@@ -191,7 +197,7 @@
     public void complicationAvailability_checkAvailabilityWhenDreamOverlayBecomesActive() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setServiceAvailable(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
index c8b2b25..175da0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
@@ -30,9 +30,14 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.condition.SelfExecutingMonitor;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.shared.condition.Monitor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,6 +48,8 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -60,9 +67,19 @@
     @Mock
     private View mBcSmartspaceView;
 
+    @Mock
+    private FeatureFlags mFeatureFlags;
+
+    private Monitor mMonitor;
+
+    private final Set<Condition> mPreconditions = new HashSet<>();
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mMonitor = SelfExecutingMonitor.createInstance();
+
+        when(mFeatureFlags.isEnabled(Flags.HIDE_SMARTSPACE_ON_DREAM_OVERLAY)).thenReturn(false);
     }
 
     /**
@@ -75,11 +92,22 @@
         verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication));
     }
 
-    private SmartSpaceComplication.Registrant getRegistrant() {
-        return new SmartSpaceComplication.Registrant(
-                mDreamOverlayStateController,
-                mComplication,
-                mSmartspaceController);
+    @Test
+    public void testRegistrantStart_featureEnabled_addOverlayStateCallback() {
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
+
+        verify(mDreamOverlayStateController).addCallback(any());
+    }
+
+    @Test
+    public void testRegistrantStart_featureDisabled_doesNotAddOverlayStateCallback() {
+        when(mFeatureFlags.isEnabled(Flags.HIDE_SMARTSPACE_ON_DREAM_OVERLAY)).thenReturn(true);
+
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
+
+        verify(mDreamOverlayStateController, never()).addCallback(any());
     }
 
     @Test
@@ -177,4 +205,13 @@
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mBcSmartspaceView);
         assertEquals(viewHolder.getView(), viewHolder.getView());
     }
+
+    private SmartSpaceComplication.Registrant getRegistrant() {
+        return new SmartSpaceComplication.Registrant(
+                mDreamOverlayStateController,
+                mComplication,
+                mSmartspaceController,
+                mMonitor,
+                mFeatureFlags);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
new file mode 100644
index 0000000..19347c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.dreams.conditions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.condition.Condition;
+
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DreamConditionTest extends SysuiTestCase {
+    @Mock
+    Context mContext;
+
+    @Mock
+    Condition.Callback mCallback;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    /**
+     * Ensure a dreaming state immediately triggers the condition.
+     */
+    @Test
+    public void testInitialState() {
+        final Intent intent = new Intent(Intent.ACTION_DREAMING_STARTED);
+        when(mContext.registerReceiver(any(), any())).thenReturn(intent);
+        final DreamCondition condition = new DreamCondition(mContext);
+        condition.addCallback(mCallback);
+        condition.start();
+
+        verify(mCallback).onConditionChanged(eq(condition));
+        assertThat(condition.isConditionMet()).isTrue();
+    }
+
+    /**
+     * Ensure that changing dream state triggers condition.
+     */
+    @Test
+    public void testChange() {
+        final Intent intent = new Intent(Intent.ACTION_DREAMING_STARTED);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        when(mContext.registerReceiver(receiverCaptor.capture(), any())).thenReturn(intent);
+        final DreamCondition condition = new DreamCondition(mContext);
+        condition.addCallback(mCallback);
+        condition.start();
+        clearInvocations(mCallback);
+        receiverCaptor.getValue().onReceive(mContext, new Intent(Intent.ACTION_DREAMING_STOPPED));
+        verify(mCallback).onConditionChanged(eq(condition));
+        assertThat(condition.isConditionMet()).isFalse();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
index 4bd53c0..3a168d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
@@ -41,12 +41,13 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.touch.scrim.ScrimController;
+import com.android.systemui.dreams.touch.scrim.ScrimManager;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import org.junit.Before;
@@ -63,10 +64,13 @@
 @RunWith(AndroidTestingRunner.class)
 public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
     @Mock
-    StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    CentralSurfaces mCentralSurfaces;
 
     @Mock
-    CentralSurfaces mCentralSurfaces;
+    ScrimManager mScrimManager;
+
+    @Mock
+    ScrimController mScrimController;
 
     @Mock
     NotificationShadeWindowController mNotificationShadeWindowController;
@@ -111,7 +115,7 @@
         MockitoAnnotations.initMocks(this);
         mTouchHandler = new BouncerSwipeTouchHandler(
                 mDisplayMetrics,
-                mStatusBarKeyguardViewManager,
+                mScrimManager,
                 Optional.of(mCentralSurfaces),
                 mNotificationShadeWindowController,
                 mValueAnimatorCreator,
@@ -121,6 +125,7 @@
                 TOUCH_REGION,
                 mUiEventLogger);
 
+        when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
         when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
         when(mCentralSurfaces.getDisplayHeight()).thenReturn((float) SCREEN_HEIGHT_PX);
         when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -193,7 +198,7 @@
         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
                 .isTrue();
 
-        verify(mStatusBarKeyguardViewManager, never()).onPanelExpansionChanged(any());
+        verify(mScrimController, never()).expand(any());
     }
 
     /**
@@ -220,7 +225,7 @@
         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
                 .isTrue();
 
-        verify(mStatusBarKeyguardViewManager, never()).onPanelExpansionChanged(any());
+        verify(mScrimController, never()).expand(any());
     }
 
     /**
@@ -274,12 +279,12 @@
         final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
                 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
 
-        reset(mStatusBarKeyguardViewManager);
+        reset(mScrimController);
         assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
                 .isTrue();
 
         // Ensure only called once
-        verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(any());
+        verify(mScrimController).expand(any());
 
         final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent;
         final float dragDownAmount = event2.getY() - event1.getY();
@@ -288,7 +293,7 @@
         ShadeExpansionChangeEvent event =
                 new ShadeExpansionChangeEvent(
                         expansion, /* expanded= */ false, /* tracking= */ true, dragDownAmount);
-        verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(event);
+        verify(mScrimController).expand(event);
     }
 
     /**
@@ -302,12 +307,13 @@
         final float velocityY = -1;
         swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
 
-        verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_HIDDEN));
+        verify(mValueAnimatorCreator).create(eq(expansion),
+                eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
         verify(mValueAnimator, never()).addListener(any());
 
         verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
                 eq(SCREEN_HEIGHT_PX * expansion),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_HIDDEN),
+                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
         verify(mValueAnimator).start();
         verify(mUiEventLogger, never()).log(any());
@@ -324,7 +330,8 @@
         final float velocityY = 1;
         swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
 
-        verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_VISIBLE));
+        verify(mValueAnimatorCreator).create(eq(expansion),
+                eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
 
         ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor =
                 ArgumentCaptor.forClass(AnimatorListenerAdapter.class);
@@ -332,7 +339,7 @@
         AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue();
 
         verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE),
+                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
         verify(mValueAnimator).start();
         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED);
@@ -355,12 +362,12 @@
         swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
 
         verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
-                eq(KeyguardBouncer.EXPANSION_VISIBLE));
+                eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
         verify(mValueAnimator, never()).addListener(any());
 
         verify(mFlingAnimationUtils).apply(eq(mValueAnimator),
                 eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE),
+                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
         verify(mValueAnimator).start();
         verify(mUiEventLogger, never()).log(any());
@@ -381,12 +388,12 @@
         swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
 
         verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
-                eq(KeyguardBouncer.EXPANSION_HIDDEN));
+                eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
         verify(mValueAnimator, never()).addListener(any());
 
         verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
                 eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_HIDDEN),
+                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
         verify(mValueAnimator).start();
         verify(mUiEventLogger, never()).log(any());
@@ -405,7 +412,8 @@
         final float velocityY = -1;
         swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
 
-        verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_VISIBLE));
+        verify(mValueAnimatorCreator).create(eq(expansion),
+                eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
 
         ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor =
                 ArgumentCaptor.forClass(AnimatorListenerAdapter.class);
@@ -413,7 +421,7 @@
         AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue();
 
         verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion),
-                eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE),
+                eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
                 eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
         verify(mValueAnimator).start();
         verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
index a807407..178b9cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
@@ -424,6 +424,32 @@
         verify(gestureListener2).onDown(eq(followupEvent));
     }
 
+    @Test
+    public void testOnRemovedCallbackOnStopMonitoring() {
+        final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
+        final DreamTouchHandler.TouchSession.Callback callback =
+                Mockito.mock(DreamTouchHandler.TouchSession.Callback.class);
+
+        final Environment environment = new Environment(Stream.of(touchHandler)
+                .collect(Collectors.toCollection(HashSet::new)));
+
+        final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(initialEvent);
+
+        final DreamTouchHandler.TouchSession session = captureSession(touchHandler);
+        session.registerCallback(callback);
+
+        environment.executeAll();
+
+        environment.updateLifecycle(observerOwnerPair -> {
+            observerOwnerPair.first.onPause(observerOwnerPair.second);
+        });
+
+        environment.executeAll();
+
+        verify(callback).onRemoved();
+    }
+
     public GestureDetector.OnGestureListener registerGestureListener(DreamTouchHandler handler) {
         final GestureDetector.OnGestureListener gestureListener = Mockito.mock(
                 GestureDetector.OnGestureListener.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
new file mode 100644
index 0000000..79c535a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.os.PowerManager;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class BouncerlessScrimControllerTest extends SysuiTestCase {
+    @Mock
+    BouncerlessScrimController.Callback mCallback;
+
+    @Mock
+    PowerManager mPowerManager;
+
+    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testWakeupOnSwipeOpen() {
+        final BouncerlessScrimController scrimController =
+                new BouncerlessScrimController(mExecutor, mPowerManager);
+        scrimController.addCallback(mCallback);
+        scrimController.expand(new ShadeExpansionChangeEvent(.5f, true, false, 0.0f));
+        mExecutor.runAllReady();
+        verify(mPowerManager).wakeUp(anyLong(), eq(PowerManager.WAKE_REASON_GESTURE), any());
+        verify(mCallback).onWakeup();
+    }
+
+    @Test
+    public void testExpansionPropagation() {
+        final BouncerlessScrimController scrimController =
+                new BouncerlessScrimController(mExecutor, mPowerManager);
+        scrimController.addCallback(mCallback);
+        final ShadeExpansionChangeEvent expansionEvent =
+                new ShadeExpansionChangeEvent(0.5f, false, false, 0.0f);
+        scrimController.expand(expansionEvent);
+        mExecutor.runAllReady();
+        verify(mCallback).onExpansion(eq(expansionEvent));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
new file mode 100644
index 0000000..ac9822d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+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.SysuiTestCase;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+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.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ScrimManagerTest extends SysuiTestCase {
+    @Mock
+    ScrimController mBouncerlessScrimController;
+
+    @Mock
+    ScrimController mBouncerScrimController;
+
+    @Mock
+    KeyguardStateController mKeyguardStateController;
+
+    @Mock
+    ScrimManager.Callback mCallback;
+
+    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testControllerSelection() {
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+        ArgumentCaptor<KeyguardStateController.Callback> callbackCaptor =
+                ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+        final ScrimManager manager = new ScrimManager(mExecutor, mBouncerScrimController,
+                mBouncerlessScrimController, mKeyguardStateController);
+        verify(mKeyguardStateController).addCallback(callbackCaptor.capture());
+
+        assertThat(manager.getCurrentController()).isEqualTo(mBouncerScrimController);
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+        callbackCaptor.getValue().onKeyguardShowingChanged();
+        mExecutor.runAllReady();
+        assertThat(manager.getCurrentController()).isEqualTo(mBouncerlessScrimController);
+    }
+
+    @Test
+    public void testCallback() {
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+        ArgumentCaptor<KeyguardStateController.Callback> callbackCaptor =
+                ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+        final ScrimManager manager = new ScrimManager(mExecutor, mBouncerScrimController,
+                mBouncerlessScrimController, mKeyguardStateController);
+        verify(mKeyguardStateController).addCallback(callbackCaptor.capture());
+
+        manager.addCallback(mCallback);
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+        callbackCaptor.getValue().onKeyguardShowingChanged();
+        mExecutor.runAllReady();
+        verify(mCallback).onScrimControllerChanged(eq(mBouncerlessScrimController));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
index 170a70f..35f0f6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
@@ -125,7 +125,7 @@
         flags.set(unreleasedFlag, false)
         flags.set(unreleasedFlag, false)
 
-        listener.verifyInOrder(unreleasedFlag.id, unreleasedFlag.id)
+        listener.verifyInOrder(unreleasedFlag.name, unreleasedFlag.name)
     }
 
     @Test
@@ -137,7 +137,7 @@
         flags.set(stringFlag, "Test")
         flags.set(stringFlag, "Test")
 
-        listener.verifyInOrder(stringFlag.id)
+        listener.verifyInOrder(stringFlag.name)
     }
 
     @Test
@@ -149,7 +149,7 @@
         flags.removeListener(listener)
         flags.set(unreleasedFlag, false)
 
-        listener.verifyInOrder(unreleasedFlag.id)
+        listener.verifyInOrder(unreleasedFlag.name)
     }
 
     @Test
@@ -162,7 +162,7 @@
         flags.removeListener(listener)
         flags.set(stringFlag, "Other")
 
-        listener.verifyInOrder(stringFlag.id)
+        listener.verifyInOrder(stringFlag.name)
     }
 
     @Test
@@ -175,7 +175,7 @@
         flags.set(releasedFlag, true)
         flags.set(unreleasedFlag, true)
 
-        listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
+        listener.verifyInOrder(releasedFlag.name, unreleasedFlag.name)
     }
 
     @Test
@@ -191,7 +191,7 @@
         flags.set(releasedFlag, false)
         flags.set(unreleasedFlag, false)
 
-        listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
+        listener.verifyInOrder(releasedFlag.name, unreleasedFlag.name)
     }
 
     @Test
@@ -204,8 +204,8 @@
 
         flags.set(releasedFlag, true)
 
-        listener1.verifyInOrder(releasedFlag.id)
-        listener2.verifyInOrder(releasedFlag.id)
+        listener1.verifyInOrder(releasedFlag.name)
+        listener2.verifyInOrder(releasedFlag.name)
     }
 
     @Test
@@ -220,18 +220,18 @@
         flags.removeListener(listener2)
         flags.set(releasedFlag, false)
 
-        listener1.verifyInOrder(releasedFlag.id, releasedFlag.id)
-        listener2.verifyInOrder(releasedFlag.id)
+        listener1.verifyInOrder(releasedFlag.name, releasedFlag.name)
+        listener2.verifyInOrder(releasedFlag.name)
     }
 
     class VerifyingListener : FlagListenable.Listener {
-        var flagEventIds = mutableListOf<Int>()
+        var flagEventNames = mutableListOf<String>()
         override fun onFlagChanged(event: FlagListenable.FlagEvent) {
-            flagEventIds.add(event.flagId)
+            flagEventNames.add(event.flagName)
         }
 
-        fun verifyInOrder(vararg eventIds: Int) {
-            assertThat(flagEventIds).containsExactlyElementsIn(eventIds.asList())
+        fun verifyInOrder(vararg eventNames: String) {
+            assertThat(flagEventNames).containsExactlyElementsIn(eventNames.asList())
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
index ed16721..686782f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP
 import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE
+import com.android.systemui.util.mockito.any
 import org.junit.Before
 import org.junit.Test
 import org.mockito.ArgumentCaptor
@@ -48,22 +49,22 @@
     @Test
     fun testRestart_ImmediateWhenAsleep() {
         whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
-        restarter.restartSystemUI()
-        verify(systemExitRestarter).restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
+        verify(systemExitRestarter).restartSystemUI(any())
     }
 
     @Test
     fun testRestart_WaitsForSceenOff() {
         whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
 
-        restarter.restartSystemUI()
-        verify(systemExitRestarter, never()).restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
+        verify(systemExitRestarter, never()).restartSystemUI(any())
 
         val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
         verify(wakefulnessLifecycle).addObserver(captor.capture())
 
         captor.value.onFinishedGoingToSleep()
 
-        verify(systemExitRestarter).restartSystemUI()
+        verify(systemExitRestarter).restartSystemUI(any())
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
index 7592cc5..2bcd75b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
@@ -23,13 +23,11 @@
 import android.content.res.Resources.NotFoundException
 import android.test.suitebuilder.annotation.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.commandline.CommandRegistry
-import com.android.systemui.util.DeviceConfigProxyFake
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.withArgCaptor
-import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.GlobalSettings
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert
 import org.junit.Before
@@ -62,21 +60,18 @@
     @Mock
     private lateinit var mockContext: Context
     @Mock
-    private lateinit var secureSettings: SecureSettings
+    private lateinit var globalSettings: GlobalSettings
     @Mock
     private lateinit var systemProperties: SystemPropertiesHelper
     @Mock
     private lateinit var resources: Resources
     @Mock
-    private lateinit var commandRegistry: CommandRegistry
-    @Mock
     private lateinit var restarter: Restarter
-    private val flagMap = mutableMapOf<Int, Flag<*>>()
+    private val flagMap = mutableMapOf<String, Flag<*>>()
     private lateinit var broadcastReceiver: BroadcastReceiver
-    private lateinit var clearCacheAction: Consumer<Int>
+    private lateinit var clearCacheAction: Consumer<String>
     private val serverFlagReader = ServerFlagReaderFake()
 
-    private val deviceConfig = DeviceConfigProxyFake()
     private val teamfoodableFlagA = UnreleasedFlag(
         500, name = "a", namespace = "test", teamfood = true
     )
@@ -87,12 +82,13 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        flagMap.put(teamfoodableFlagA.id, teamfoodableFlagA)
-        flagMap.put(teamfoodableFlagB.id, teamfoodableFlagB)
+        flagMap.put(Flags.TEAMFOOD.name, Flags.TEAMFOOD)
+        flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA)
+        flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB)
         mFeatureFlagsDebug = FeatureFlagsDebug(
             flagManager,
             mockContext,
-            secureSettings,
+            globalSettings,
             systemProperties,
             resources,
             serverFlagReader,
@@ -110,14 +106,14 @@
         clearCacheAction = withArgCaptor {
             verify(flagManager).clearCacheAction = capture()
         }
-        whenever(flagManager.idToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
+        whenever(flagManager.nameToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
     }
 
     @Test
     fun readBooleanFlag() {
         // Remember that the TEAMFOOD flag is id#1 and has special behavior.
-        whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
-        whenever(flagManager.readFlagValue<Boolean>(eq(4), any())).thenReturn(false)
+        whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true)
+        whenever(flagManager.readFlagValue<Boolean>(eq("4"), any())).thenReturn(false)
 
         assertThat(
             mFeatureFlagsDebug.isEnabled(
@@ -141,7 +137,7 @@
             mFeatureFlagsDebug.isEnabled(
                 ReleasedFlag(
                     4,
-                    name = "3",
+                    name = "4",
                     namespace = "test"
                 )
             )
@@ -150,7 +146,7 @@
             mFeatureFlagsDebug.isEnabled(
                 UnreleasedFlag(
                     5,
-                    name = "4",
+                    name = "5",
                     namespace = "test"
                 )
             )
@@ -159,7 +155,8 @@
 
     @Test
     fun teamFoodFlag_False() {
-        whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(false)
+        whenever(flagManager.readFlagValue<Boolean>(
+            eq(Flags.TEAMFOOD.name), any())).thenReturn(false)
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isFalse()
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
 
@@ -170,7 +167,8 @@
 
     @Test
     fun teamFoodFlag_True() {
-        whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
+        whenever(flagManager.readFlagValue<Boolean>(
+            eq(Flags.TEAMFOOD.name), any())).thenReturn(true)
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
 
@@ -181,11 +179,12 @@
 
     @Test
     fun teamFoodFlag_Overridden() {
-        whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.id), any()))
+        whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.name), any()))
             .thenReturn(true)
-        whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.id), any()))
+        whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any()))
             .thenReturn(false)
-        whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
+        whenever(flagManager.readFlagValue<Boolean>(
+            eq(Flags.TEAMFOOD.name), any())).thenReturn(true)
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse()
 
@@ -202,8 +201,8 @@
         whenever(resources.getBoolean(1004)).thenAnswer { throw NameNotFoundException() }
         whenever(resources.getBoolean(1005)).thenAnswer { throw NameNotFoundException() }
 
-        whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
-        whenever(flagManager.readFlagValue<Boolean>(eq(5), any())).thenReturn(false)
+        whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true)
+        whenever(flagManager.readFlagValue<Boolean>(eq("5"), any())).thenReturn(false)
 
         assertThat(
             mFeatureFlagsDebug.isEnabled(
@@ -255,8 +254,8 @@
 
     @Test
     fun readStringFlag() {
-        whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("foo")
-        whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("bar")
+        whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("foo")
+        whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("bar")
         assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "1", "test", "biz"))).isEqualTo("biz")
         assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "2", "test", "baz"))).isEqualTo("baz")
         assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "3", "test", "buz"))).isEqualTo("foo")
@@ -272,9 +271,9 @@
         whenever(resources.getString(1005)).thenAnswer { throw NameNotFoundException() }
         whenever(resources.getString(1006)).thenAnswer { throw NameNotFoundException() }
 
-        whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("override3")
-        whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("override4")
-        whenever(flagManager.readFlagValue<String>(eq(6), any())).thenReturn("override6")
+        whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("override3")
+        whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("override4")
+        whenever(flagManager.readFlagValue<String>(eq("6"), any())).thenReturn("override6")
 
         assertThat(
             mFeatureFlagsDebug.getString(
@@ -322,8 +321,8 @@
 
     @Test
     fun readIntFlag() {
-        whenever(flagManager.readFlagValue<Int>(eq(3), any())).thenReturn(22)
-        whenever(flagManager.readFlagValue<Int>(eq(4), any())).thenReturn(48)
+        whenever(flagManager.readFlagValue<Int>(eq("3"), any())).thenReturn(22)
+        whenever(flagManager.readFlagValue<Int>(eq("4"), any())).thenReturn(48)
         assertThat(mFeatureFlagsDebug.getInt(IntFlag(1, "1", "test", 12))).isEqualTo(12)
         assertThat(mFeatureFlagsDebug.getInt(IntFlag(2, "2", "test", 93))).isEqualTo(93)
         assertThat(mFeatureFlagsDebug.getInt(IntFlag(3, "3", "test", 8))).isEqualTo(22)
@@ -368,12 +367,12 @@
         broadcastReceiver.onReceive(mockContext, Intent())
         broadcastReceiver.onReceive(mockContext, Intent("invalid action"))
         broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG))
-        setByBroadcast(0, false) // unknown id does nothing
-        setByBroadcast(1, "string") // wrong type does nothing
-        setByBroadcast(2, 123) // wrong type does nothing
-        setByBroadcast(3, false) // wrong type does nothing
-        setByBroadcast(4, 123) // wrong type does nothing
-        verifyNoMoreInteractions(flagManager, secureSettings)
+        setByBroadcast("0", false) // unknown id does nothing
+        setByBroadcast("1", "string") // wrong type does nothing
+        setByBroadcast("2", 123) // wrong type does nothing
+        setByBroadcast("3", false) // wrong type does nothing
+        setByBroadcast("4", 123) // wrong type does nothing
+        verifyNoMoreInteractions(flagManager, globalSettings)
     }
 
     @Test
@@ -383,16 +382,16 @@
         // trying to erase an id not in the map does nothing
         broadcastReceiver.onReceive(
             mockContext,
-            Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 0)
+            Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "")
         )
-        verifyNoMoreInteractions(flagManager, secureSettings)
+        verifyNoMoreInteractions(flagManager, globalSettings)
 
         // valid id with no value puts empty string in the setting
         broadcastReceiver.onReceive(
             mockContext,
-            Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 1)
+            Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "1")
         )
-        verifyPutData(1, "", numReads = 0)
+        verifyPutData("1", "", numReads = 0)
     }
 
     @Test
@@ -402,51 +401,51 @@
         addFlag(ResourceBooleanFlag(3, "3", "test", 1003))
         addFlag(ResourceBooleanFlag(4, "4", "test", 1004))
 
-        setByBroadcast(1, false)
-        verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}")
+        setByBroadcast("1", false)
+        verifyPutData("1", "{\"type\":\"boolean\",\"value\":false}")
 
-        setByBroadcast(2, true)
-        verifyPutData(2, "{\"type\":\"boolean\",\"value\":true}")
+        setByBroadcast("2", true)
+        verifyPutData("2", "{\"type\":\"boolean\",\"value\":true}")
 
-        setByBroadcast(3, false)
-        verifyPutData(3, "{\"type\":\"boolean\",\"value\":false}")
+        setByBroadcast("3", false)
+        verifyPutData("3", "{\"type\":\"boolean\",\"value\":false}")
 
-        setByBroadcast(4, true)
-        verifyPutData(4, "{\"type\":\"boolean\",\"value\":true}")
+        setByBroadcast("4", true)
+        verifyPutData("4", "{\"type\":\"boolean\",\"value\":true}")
     }
 
     @Test
     fun setStringFlag() {
-        addFlag(StringFlag(1, "flag1", "1", "test"))
+        addFlag(StringFlag(1, "1", "1", "test"))
         addFlag(ResourceStringFlag(2, "2", "test", 1002))
 
-        setByBroadcast(1, "override1")
-        verifyPutData(1, "{\"type\":\"string\",\"value\":\"override1\"}")
+        setByBroadcast("1", "override1")
+        verifyPutData("1", "{\"type\":\"string\",\"value\":\"override1\"}")
 
-        setByBroadcast(2, "override2")
-        verifyPutData(2, "{\"type\":\"string\",\"value\":\"override2\"}")
+        setByBroadcast("2", "override2")
+        verifyPutData("2", "{\"type\":\"string\",\"value\":\"override2\"}")
     }
 
     @Test
     fun setFlag_ClearsCache() {
         val flag1 = addFlag(StringFlag(1, "1", "test", "flag1"))
-        whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("original")
+        whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("original")
 
         // gets the flag & cache it
         assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
-        verify(flagManager).readFlagValue(eq(1), eq(StringFlagSerializer))
+        verify(flagManager, times(1)).readFlagValue(eq("1"), eq(StringFlagSerializer))
 
         // hit the cache
         assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
         verifyNoMoreInteractions(flagManager)
 
         // set the flag
-        setByBroadcast(1, "new")
-        verifyPutData(1, "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
-        whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("new")
+        setByBroadcast("1", "new")
+        verifyPutData("1", "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
+        whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("new")
 
         assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("new")
-        verify(flagManager, times(3)).readFlagValue(eq(1), eq(StringFlagSerializer))
+        verify(flagManager, times(3)).readFlagValue(eq("1"), eq(StringFlagSerializer))
     }
 
     @Test
@@ -463,7 +462,6 @@
         val flag = UnreleasedFlag(100, name = "100", namespace = "test")
 
         serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
-
         assertThat(mFeatureFlagsDebug.isEnabled(flag)).isTrue()
     }
 
@@ -503,26 +501,26 @@
         assertThat(dump).contains(" sysui_flag_7: [length=9] \"override7\"\n")
     }
 
-    private fun verifyPutData(id: Int, data: String, numReads: Int = 1) {
-        inOrder(flagManager, secureSettings).apply {
-            verify(flagManager, times(numReads)).readFlagValue(eq(id), any<FlagSerializer<*>>())
-            verify(flagManager).idToSettingsKey(eq(id))
-            verify(secureSettings).putStringForUser(eq("key-$id"), eq(data), anyInt())
-            verify(flagManager).dispatchListenersAndMaybeRestart(eq(id), any())
+    private fun verifyPutData(name: String, data: String, numReads: Int = 1) {
+        inOrder(flagManager, globalSettings).apply {
+            verify(flagManager, times(numReads)).readFlagValue(eq(name), any<FlagSerializer<*>>())
+            verify(flagManager).nameToSettingsKey(eq(name))
+            verify(globalSettings).putStringForUser(eq("key-$name"), eq(data), anyInt())
+            verify(flagManager).dispatchListenersAndMaybeRestart(eq(name), any())
         }.verifyNoMoreInteractions()
-        verifyNoMoreInteractions(flagManager, secureSettings)
+        verifyNoMoreInteractions(flagManager, globalSettings)
     }
 
-    private fun setByBroadcast(id: Int, value: Serializable?) {
+    private fun setByBroadcast(name: String, value: Serializable?) {
         val intent = Intent(FlagManager.ACTION_SET_FLAG)
-        intent.putExtra(FlagManager.EXTRA_ID, id)
+        intent.putExtra(FlagManager.EXTRA_NAME, name)
         intent.putExtra(FlagManager.EXTRA_VALUE, value)
         broadcastReceiver.onReceive(mockContext, intent)
     }
 
     private fun <F : Flag<*>> addFlag(flag: F): F {
-        val old = flagMap.put(flag.id, flag)
-        check(old == null) { "Flag ${flag.id} already registered" }
+        val old = flagMap.put(flag.name, flag)
+        check(old == null) { "Flag ${flag.name} already registered" }
         return flag
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
index 7d807e2..6060afe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -63,7 +64,7 @@
         whenever(batteryController.isPluggedIn).thenReturn(true)
 
         assertThat(executor.numPending()).isEqualTo(0)
-        restarter.restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
         assertThat(executor.numPending()).isEqualTo(1)
     }
 
@@ -72,11 +73,11 @@
         whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
         whenever(batteryController.isPluggedIn).thenReturn(true)
 
-        restarter.restartSystemUI()
-        verify(systemExitRestarter, never()).restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
+        verify(systemExitRestarter, never()).restartSystemUI("Restart for test")
         executor.advanceClockToLast()
         executor.runAllReady()
-        verify(systemExitRestarter).restartSystemUI()
+        verify(systemExitRestarter).restartSystemUI(any())
     }
 
     @Test
@@ -85,7 +86,7 @@
         whenever(batteryController.isPluggedIn).thenReturn(true)
 
         assertThat(executor.numPending()).isEqualTo(0)
-        restarter.restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
         assertThat(executor.numPending()).isEqualTo(0)
     }
 
@@ -95,7 +96,7 @@
         whenever(batteryController.isPluggedIn).thenReturn(false)
 
         assertThat(executor.numPending()).isEqualTo(0)
-        restarter.restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
         assertThat(executor.numPending()).isEqualTo(0)
     }
 
@@ -105,8 +106,8 @@
         whenever(batteryController.isPluggedIn).thenReturn(true)
 
         assertThat(executor.numPending()).isEqualTo(0)
-        restarter.restartSystemUI()
-        restarter.restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
+        restarter.restartSystemUI("Restart for test")
         assertThat(executor.numPending()).isEqualTo(1)
     }
 
@@ -115,7 +116,7 @@
         whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
         whenever(batteryController.isPluggedIn).thenReturn(true)
         assertThat(executor.numPending()).isEqualTo(0)
-        restarter.restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
 
         val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
         verify(wakefulnessLifecycle).addObserver(captor.capture())
@@ -131,7 +132,7 @@
         whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
         whenever(batteryController.isPluggedIn).thenReturn(false)
         assertThat(executor.numPending()).isEqualTo(0)
-        restarter.restartSystemUI()
+        restarter.restartSystemUI("Restart for test")
 
         val captor =
             ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
index d5b5a4a..4c6028c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
@@ -19,7 +19,6 @@
 import android.content.res.Resources
 import android.test.suitebuilder.annotation.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.DeviceConfigProxyFake
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
 import org.junit.Before
@@ -39,9 +38,8 @@
     @Mock private lateinit var mResources: Resources
     @Mock private lateinit var mSystemProperties: SystemPropertiesHelper
     @Mock private lateinit var restarter: Restarter
-    private val flagMap = mutableMapOf<Int, Flag<*>>()
+    private val flagMap = mutableMapOf<String, Flag<*>>()
     private val serverFlagReader = ServerFlagReaderFake()
-    private val deviceConfig = DeviceConfigProxyFake()
 
     @Before
     fun setup() {
@@ -49,7 +47,6 @@
         mFeatureFlagsRelease = FeatureFlagsRelease(
             mResources,
             mSystemProperties,
-            deviceConfig,
             serverFlagReader,
             flagMap,
             restarter)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index fea91c5..28131b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
@@ -32,7 +32,7 @@
 
     @Mock private lateinit var featureFlags: FeatureFlagsDebug
     @Mock private lateinit var pw: PrintWriter
-    private val flagMap = mutableMapOf<Int, Flag<*>>()
+    private val flagMap = mutableMapOf<String, Flag<*>>()
     private val flagA = UnreleasedFlag(500, "500", "test")
     private val flagB = ReleasedFlag(501, "501", "test")
     private val stringFlag = StringFlag(502, "502", "test", "abracadabra")
@@ -53,59 +53,59 @@
             (invocation.getArgument(0) as IntFlag).default
         }
 
-        flagMap.put(flagA.id, flagA)
-        flagMap.put(flagB.id, flagB)
-        flagMap.put(stringFlag.id, stringFlag)
-        flagMap.put(intFlag.id, intFlag)
+        flagMap.put(flagA.name, flagA)
+        flagMap.put(flagB.name, flagB)
+        flagMap.put(stringFlag.name, stringFlag)
+        flagMap.put(intFlag.name, intFlag)
 
         cmd = FlagCommand(featureFlags, flagMap)
     }
 
     @Test
     fun readBooleanFlagCommand() {
-        cmd.execute(pw, listOf(flagA.id.toString()))
+        cmd.execute(pw, listOf(flagA.name))
         Mockito.verify(featureFlags).isEnabled(flagA)
     }
 
     @Test
     fun readStringFlagCommand() {
-        cmd.execute(pw, listOf(stringFlag.id.toString()))
+        cmd.execute(pw, listOf(stringFlag.name))
         Mockito.verify(featureFlags).getString(stringFlag)
     }
 
     @Test
     fun readIntFlag() {
-        cmd.execute(pw, listOf(intFlag.id.toString()))
+        cmd.execute(pw, listOf(intFlag.name))
         Mockito.verify(featureFlags).getInt(intFlag)
     }
 
     @Test
     fun setBooleanFlagCommand() {
-        cmd.execute(pw, listOf(flagB.id.toString(), "on"))
+        cmd.execute(pw, listOf(flagB.name, "on"))
         Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, true)
     }
 
     @Test
     fun setStringFlagCommand() {
-        cmd.execute(pw, listOf(stringFlag.id.toString(), "set", "foobar"))
+        cmd.execute(pw, listOf(stringFlag.name, "set", "foobar"))
         Mockito.verify(featureFlags).setStringFlagInternal(stringFlag, "foobar")
     }
 
     @Test
     fun setIntFlag() {
-        cmd.execute(pw, listOf(intFlag.id.toString(), "put", "123"))
+        cmd.execute(pw, listOf(intFlag.name, "put", "123"))
         Mockito.verify(featureFlags).setIntFlagInternal(intFlag, 123)
     }
 
     @Test
     fun toggleBooleanFlagCommand() {
-        cmd.execute(pw, listOf(flagB.id.toString(), "toggle"))
+        cmd.execute(pw, listOf(flagB.name, "toggle"))
         Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, false)
     }
 
     @Test
     fun eraseFlagCommand() {
-        cmd.execute(pw, listOf(flagA.id.toString(), "erase"))
+        cmd.execute(pw, listOf(flagA.name, "erase"))
         Mockito.verify(featureFlags).eraseFlag(flagA)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
index fca7e96..e679d47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
@@ -87,14 +87,14 @@
     @Test
     fun testObserverClearsCache() {
         val listener = mock<FlagListenable.Listener>()
-        val clearCacheAction = mock<Consumer<Int>>()
+        val clearCacheAction = mock<Consumer<String>>()
         mFlagManager.clearCacheAction = clearCacheAction
         mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
         observer.onChange(false, flagUri(1))
-        verify(clearCacheAction).accept(eq(1))
+        verify(clearCacheAction).accept(eq("1"))
     }
 
     @Test
@@ -110,14 +110,14 @@
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
             verify(listener1).onFlagChanged(capture())
         }
-        assertThat(flagEvent1.flagId).isEqualTo(1)
+        assertThat(flagEvent1.flagName).isEqualTo("1")
         verifyNoMoreInteractions(listener1, listener10)
 
         observer.onChange(false, flagUri(10))
         val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
             verify(listener10).onFlagChanged(capture())
         }
-        assertThat(flagEvent10.flagId).isEqualTo(10)
+        assertThat(flagEvent10.flagName).isEqualTo("10")
         verifyNoMoreInteractions(listener1, listener10)
     }
 
@@ -130,18 +130,18 @@
         mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
         mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener10)
 
-        mFlagManager.dispatchListenersAndMaybeRestart(1, null)
+        mFlagManager.dispatchListenersAndMaybeRestart("1", null)
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
             verify(listener1).onFlagChanged(capture())
         }
-        assertThat(flagEvent1.flagId).isEqualTo(1)
+        assertThat(flagEvent1.flagName).isEqualTo("1")
         verifyNoMoreInteractions(listener1, listener10)
 
-        mFlagManager.dispatchListenersAndMaybeRestart(10, null)
+        mFlagManager.dispatchListenersAndMaybeRestart("10", null)
         val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
             verify(listener10).onFlagChanged(capture())
         }
-        assertThat(flagEvent10.flagId).isEqualTo(10)
+        assertThat(flagEvent10.flagName).isEqualTo("10")
         verifyNoMoreInteractions(listener1, listener10)
     }
 
@@ -151,25 +151,25 @@
         mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
         mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener)
 
-        mFlagManager.dispatchListenersAndMaybeRestart(1, null)
+        mFlagManager.dispatchListenersAndMaybeRestart("1", null)
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
             verify(listener).onFlagChanged(capture())
         }
-        assertThat(flagEvent1.flagId).isEqualTo(1)
+        assertThat(flagEvent1.flagName).isEqualTo("1")
         verifyNoMoreInteractions(listener)
 
-        mFlagManager.dispatchListenersAndMaybeRestart(10, null)
+        mFlagManager.dispatchListenersAndMaybeRestart("10", null)
         val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
             verify(listener, times(2)).onFlagChanged(capture())
         }
-        assertThat(flagEvent10.flagId).isEqualTo(10)
+        assertThat(flagEvent10.flagName).isEqualTo("10")
         verifyNoMoreInteractions(listener)
     }
 
     @Test
     fun testRestartWithNoListeners() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+        mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
         verify(restartAction).accept(eq(false))
         verifyNoMoreInteractions(restartAction)
     }
@@ -180,7 +180,7 @@
         mFlagManager.addListener(ReleasedFlag(1, "1", "test")) { event ->
             event.requestNoRestart()
         }
-        mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+        mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
         verify(restartAction).accept(eq(true))
         verifyNoMoreInteractions(restartAction)
     }
@@ -191,7 +191,7 @@
         mFlagManager.addListener(ReleasedFlag(10, "10", "test")) { event ->
             event.requestNoRestart()
         }
-        mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+        mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
         verify(restartAction).accept(eq(false))
         verifyNoMoreInteractions(restartAction)
     }
@@ -205,7 +205,7 @@
         mFlagManager.addListener(ReleasedFlag(10, "10", "test")) {
             // do not request
         }
-        mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+        mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
         verify(restartAction).accept(eq(false))
         verifyNoMoreInteractions(restartAction)
     }
@@ -214,31 +214,31 @@
     fun testReadBooleanFlag() {
         // test that null string returns null
         whenever(mFlagSettingsHelper.getString(any())).thenReturn(null)
-        assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
+        assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull()
 
         // test that empty string returns null
         whenever(mFlagSettingsHelper.getString(any())).thenReturn("")
-        assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
+        assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull()
 
         // test false
         whenever(mFlagSettingsHelper.getString(any()))
             .thenReturn("{\"type\":\"boolean\",\"value\":false}")
-        assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isFalse()
+        assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isFalse()
 
         // test true
         whenever(mFlagSettingsHelper.getString(any()))
             .thenReturn("{\"type\":\"boolean\",\"value\":true}")
-        assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isTrue()
+        assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isTrue()
 
         // Reading a value of a different type should just return null
         whenever(mFlagSettingsHelper.getString(any()))
             .thenReturn("{\"type\":\"string\",\"value\":\"foo\"}")
-        assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
+        assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull()
 
         // Reading a value that isn't json should throw an exception
         assertThrows(InvalidFlagStorageException::class.java) {
             whenever(mFlagSettingsHelper.getString(any())).thenReturn("1")
-            mFlagManager.readFlagValue(1, BooleanFlagSerializer)
+            mFlagManager.readFlagValue("1", BooleanFlagSerializer)
         }
     }
 
@@ -257,31 +257,31 @@
     fun testReadStringFlag() {
         // test that null string returns null
         whenever(mFlagSettingsHelper.getString(any())).thenReturn(null)
-        assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
+        assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull()
 
         // test that empty string returns null
         whenever(mFlagSettingsHelper.getString(any())).thenReturn("")
-        assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
+        assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull()
 
         // test json with the empty string value returns empty string
         whenever(mFlagSettingsHelper.getString(any()))
             .thenReturn("{\"type\":\"string\",\"value\":\"\"}")
-        assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isEqualTo("")
+        assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isEqualTo("")
 
         // test string with value is returned
         whenever(mFlagSettingsHelper.getString(any()))
             .thenReturn("{\"type\":\"string\",\"value\":\"foo\"}")
-        assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isEqualTo("foo")
+        assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isEqualTo("foo")
 
         // Reading a value of a different type should just return null
         whenever(mFlagSettingsHelper.getString(any()))
             .thenReturn("{\"type\":\"boolean\",\"value\":false}")
-        assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
+        assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull()
 
         // Reading a value that isn't json should throw an exception
         assertThrows(InvalidFlagStorageException::class.java) {
             whenever(mFlagSettingsHelper.getString(any())).thenReturn("1")
-            mFlagManager.readFlagValue(1, StringFlagSerializer)
+            mFlagManager.readFlagValue("1", StringFlagSerializer)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/RestartDozeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/RestartDozeListenerTest.kt
new file mode 100644
index 0000000..de0e511
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/RestartDozeListenerTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.flags
+
+import android.os.PowerManager
+import android.test.suitebuilder.annotation.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class RestartDozeListenerTest : SysuiTestCase() {
+
+    lateinit var restartDozeListener: RestartDozeListener
+
+    val settings = FakeSettings()
+    @Mock lateinit var statusBarStateController: StatusBarStateController
+    @Mock lateinit var powerManager: PowerManager
+    val clock = FakeSystemClock()
+    lateinit var listener: StatusBarStateController.StateListener
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        restartDozeListener =
+            RestartDozeListener(settings, statusBarStateController, powerManager, clock)
+
+        val captor = ArgumentCaptor.forClass(StatusBarStateController.StateListener::class.java)
+        restartDozeListener.init()
+        verify(statusBarStateController).addCallback(captor.capture())
+        listener = captor.value
+    }
+
+    @Test
+    fun testStoreDreamState_onDreamingStarted() {
+        listener.onDreamingChanged(true)
+        assertThat(settings.getBool(RestartDozeListener.RESTART_NAP_KEY)).isTrue()
+    }
+
+    @Test
+    fun testStoreDreamState_onDreamingStopped() {
+        listener.onDreamingChanged(false)
+        assertThat(settings.getBool(RestartDozeListener.RESTART_NAP_KEY)).isFalse()
+    }
+
+    @Test
+    fun testRestoreDreamState_dreamingShouldStart() {
+        settings.putBool(RestartDozeListener.RESTART_NAP_KEY, true)
+        restartDozeListener.maybeRestartSleep()
+        verify(powerManager).wakeUp(clock.uptimeMillis())
+        verify(powerManager).goToSleep(clock.uptimeMillis())
+    }
+
+    @Test
+    fun testRestoreDreamState_dreamingShouldNot() {
+        settings.putBool(RestartDozeListener.RESTART_NAP_KEY, false)
+        restartDozeListener.maybeRestartSleep()
+        verify(powerManager, never()).wakeUp(anyLong())
+        verify(powerManager, never()).goToSleep(anyLong())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
index 1633912..2e98006 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
@@ -26,6 +26,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -45,17 +46,29 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
-        serverFlagReader = ServerFlagReaderImpl(NAMESPACE, deviceConfig, executor)
+        serverFlagReader = ServerFlagReaderImpl(NAMESPACE, deviceConfig, executor, false)
     }
 
     @Test
     fun testChange_alertsListener() {
+        val flag = ReleasedFlag(1, "flag_1", "test")
+        serverFlagReader.listenForChanges(listOf(flag), changeListener)
+
+        deviceConfig.setProperty(NAMESPACE, "flag_1", "1", false)
+        executor.runAllReady()
+
+        verify(changeListener).onChange(flag)
+    }
+
+    @Test
+    fun testChange_ignoresListenersDuringTest() {
+        val serverFlagReader = ServerFlagReaderImpl(NAMESPACE, deviceConfig, executor, true)
         val flag = ReleasedFlag(1, "1", "test")
         serverFlagReader.listenForChanges(listOf(flag), changeListener)
 
         deviceConfig.setProperty(NAMESPACE, "flag_override_1", "1", false)
         executor.runAllReady()
 
-        verify(changeListener).onChange()
+        verify(changeListener, never()).onChange(flag)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt
index 77c837b..a2dc1eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt
@@ -14,6 +14,7 @@
 @SmallTest
 class FragmentServiceTest : SysuiTestCase() {
     private val fragmentCreator = TestFragmentCreator()
+    private val fragmenetHostManagerFactory: FragmentHostManager.Factory = mock()
     private val fragmentCreatorFactory = FragmentService.FragmentCreator.Factory { fragmentCreator }
 
     private lateinit var fragmentService: FragmentService
@@ -24,7 +25,13 @@
             Looper.prepare()
         }
 
-        fragmentService = FragmentService(fragmentCreatorFactory, mock(), DumpManager())
+        fragmentService =
+            FragmentService(
+                fragmentCreatorFactory,
+                fragmenetHostManagerFactory,
+                mock(),
+                DumpManager()
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
new file mode 100644
index 0000000..f6ff4b2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.keyboard.data.repository
+
+import android.hardware.input.InputManager
+import android.view.InputDevice
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyboardRepositoryTest : SysuiTestCase() {
+
+    @Captor
+    private lateinit var deviceListenerCaptor: ArgumentCaptor<InputManager.InputDeviceListener>
+    @Mock private lateinit var inputManager: InputManager
+
+    private lateinit var underTest: KeyboardRepository
+    private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
+        whenever(inputManager.getInputDevice(any())).then { invocation ->
+            val id = invocation.arguments.first()
+            INPUT_DEVICES_MAP[id]
+        }
+        dispatcher = StandardTestDispatcher()
+        testScope = TestScope(dispatcher)
+        underTest = KeyboardRepositoryImpl(testScope.backgroundScope, dispatcher, inputManager)
+    }
+
+    @Test
+    fun emitsDisconnected_ifNothingIsConnected() =
+        testScope.runTest {
+            val initialState = underTest.keyboardConnected.first()
+            assertThat(initialState).isFalse()
+        }
+
+    @Test
+    fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() =
+        testScope.runTest {
+            whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
+            val initialValue = underTest.keyboardConnected.first()
+            assertThat(initialValue).isTrue()
+        }
+
+    @Test
+    fun emitsConnected_whenNewPhysicalKeyboardConnects() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+
+            assertThat(isKeyboardConnected).isTrue()
+        }
+
+    @Test
+    fun emitsDisconnected_whenKeyboardDisconnects() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            assertThat(isKeyboardConnected).isTrue()
+
+            deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
+            assertThat(isKeyboardConnected).isFalse()
+        }
+
+    private suspend fun captureDeviceListener(): InputManager.InputDeviceListener {
+        underTest.keyboardConnected.first()
+        verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
+        return deviceListenerCaptor.value
+    }
+
+    @Test
+    fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID)
+            assertThat(isKeyboardConnected).isFalse()
+
+            deviceListener.onInputDeviceAdded(VIRTUAL_FULL_KEYBOARD_ID)
+            assertThat(isKeyboardConnected).isFalse()
+        }
+
+    @Test
+    fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
+            assertThat(isKeyboardConnected).isFalse()
+        }
+
+    @Test
+    fun emitsConnected_whenAnotherDeviceDisconnects() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID)
+
+            assertThat(isKeyboardConnected).isTrue()
+        }
+
+    @Test
+    fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            deviceListener.onInputDeviceRemoved(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+
+            assertThat(isKeyboardConnected).isTrue()
+        }
+
+    @Test
+    fun passesKeyboardBacklightValues_fromBacklightListener() {
+        // TODO(b/268645734): implement when implementing backlight listener
+    }
+
+    private companion object {
+        private const val PHYSICAL_FULL_KEYBOARD_ID = 1
+        private const val VIRTUAL_FULL_KEYBOARD_ID = 2
+        private const val PHYSICAL_NOT_FULL_KEYBOARD_ID = 3
+        private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4
+
+        private val INPUT_DEVICES_MAP: Map<Int, InputDevice> =
+            mapOf(
+                PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true),
+                VIRTUAL_FULL_KEYBOARD_ID to inputDevice(virtual = true, fullKeyboard = true),
+                PHYSICAL_NOT_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = false),
+                ANOTHER_PHYSICAL_FULL_KEYBOARD_ID to
+                    inputDevice(virtual = false, fullKeyboard = true)
+            )
+
+        private fun inputDevice(virtual: Boolean, fullKeyboard: Boolean): InputDevice =
+            mock<InputDevice>().also {
+                whenever(it.isVirtual).thenReturn(virtual)
+                whenever(it.isFullKeyboard).thenReturn(fullKeyboard)
+            }
+    }
+}
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 4659766..a4e5bca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard
 
+import android.app.admin.DevicePolicyManager
 import android.content.ContentValues
 import android.content.pm.PackageManager
 import android.content.pm.ProviderInfo
@@ -39,6 +40,7 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -51,6 +53,7 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
@@ -59,8 +62,8 @@
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -86,9 +89,10 @@
     @Mock private lateinit var backgroundHandler: Handler
     @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage
     @Mock private lateinit var launchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     private lateinit var underTest: CustomizationProvider
-
     private lateinit var testScope: TestScope
 
     @Before
@@ -99,7 +103,7 @@
         whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper)
 
         underTest = CustomizationProvider()
-        val testDispatcher = StandardTestDispatcher()
+        val testDispatcher = UnconfinedTestDispatcher()
         testScope = TestScope(testDispatcher)
         val localUserSelectionManager =
             KeyguardQuickAffordanceLocalUserSelectionManager(
@@ -155,25 +159,33 @@
                 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,
+                        bouncerRepository = FakeKeyguardBouncerRepository(),
                     ),
                 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)
-                    },
+                featureFlags = featureFlags,
                 repository = { quickAffordanceRepository },
                 launchAnimator = launchAnimator,
+                devicePolicyManager = devicePolicyManager,
+                backgroundDispatcher = testDispatcher,
             )
         underTest.previewManager =
             KeyguardRemotePreviewManager(
@@ -181,6 +193,7 @@
                 mainDispatcher = testDispatcher,
                 backgroundHandler = backgroundHandler,
             )
+        underTest.mainDispatcher = UnconfinedTestDispatcher()
 
         underTest.attachInfoForTesting(
             context,
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/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 9b0d8db..c93e677 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -483,6 +483,38 @@
         assertTrue(mViewMediator.isShowingAndNotOccluded());
     }
 
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testDoKeyguardWhileInteractive_resets() {
+        mViewMediator.setShowingLocked(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        TestableLooper.get(this).processAllMessages();
+
+        when(mPowerManager.isInteractive()).thenReturn(true);
+
+        mViewMediator.onSystemReady();
+        TestableLooper.get(this).processAllMessages();
+
+        assertTrue(mViewMediator.isShowingAndNotOccluded());
+        verify(mStatusBarKeyguardViewManager).reset(anyBoolean());
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() {
+        mViewMediator.setShowingLocked(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        TestableLooper.get(this).processAllMessages();
+
+        when(mPowerManager.isInteractive()).thenReturn(false);
+
+        mViewMediator.onSystemReady();
+        TestableLooper.get(this).processAllMessages();
+
+        assertTrue(mViewMediator.isShowingAndNotOccluded());
+        verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean());
+    }
+
     private void createAndStartViewMediator() {
         mViewMediator = new KeyguardViewMediator(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
index f32d76b..39a453d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
@@ -30,6 +30,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,7 +52,12 @@
     public void setUp() throws Exception {
         mWallpaperManager = mock(IWallpaperManager.class);
         mWakefulness =
-                new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class));
+                new WakefulnessLifecycle(
+                        mContext,
+                        mWallpaperManager,
+                        new FakeSystemClock(),
+                        mock(DumpManager.class)
+                );
         mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class);
         mWakefulness.addObserver(mWakefulnessObserver);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
index e9db8cc..b9cfc65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -35,12 +36,12 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.os.UserHandle;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
@@ -71,6 +72,7 @@
     private @Mock Context mContext;
     private @Mock TaskStackChangeListeners mTaskStackChangeListeners;
     private @Mock IActivityTaskManager mIActivityTaskManager;
+    private @Mock UserTracker mUserTracker;
 
     private WorkLockActivityController mController;
     private TaskStackChangeListener mTaskStackListener;
@@ -81,12 +83,13 @@
 
         // Set a package name to use for checking ComponentName well-formedness in tests.
         doReturn("com.example.test").when(mContext).getPackageName();
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
         // Construct controller. Save the TaskStackListener for injecting events.
         final ArgumentCaptor<TaskStackChangeListener> listenerCaptor =
                 ArgumentCaptor.forClass(TaskStackChangeListener.class);
-        mController = new WorkLockActivityController(mContext, mTaskStackChangeListeners,
-                mIActivityTaskManager);
+        mController = new WorkLockActivityController(mContext, mUserTracker,
+                mTaskStackChangeListeners, mIActivityTaskManager);
 
         verify(mTaskStackChangeListeners).registerTaskStackListener(listenerCaptor.capture());
         mTaskStackListener = listenerCaptor.getValue();
@@ -135,7 +138,7 @@
                 anyInt(),
                 eq((ProfilerInfo) null),
                 argThat(hasOptions(taskId, taskOverlay)),
-                eq(UserHandle.USER_CURRENT));
+                eq(ActivityManager.getCurrentUser()));
     }
 
     private void verifyStartActivity(int taskId, boolean taskOverlay) throws Exception {
@@ -151,7 +154,7 @@
                 anyInt(),
                 eq((ProfilerInfo) null),
                 argThat(hasOptions(taskId, taskOverlay)),
-                eq(UserHandle.USER_CURRENT));
+                eq(ActivityManager.getCurrentUser()));
     }
 
     private static ArgumentMatcher<Intent> hasComponent(final Context context,
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 7205f30..5bb8367 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
@@ -18,38 +18,58 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.app.StatusBarManager
+import android.app.admin.DevicePolicyManager
 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
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
 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
     @Mock private lateinit var context: Context
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     private lateinit var underTest: CameraQuickAffordanceConfig
+    private lateinit var testScope: TestScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        setLaunchable()
 
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         underTest =
             CameraQuickAffordanceConfig(
                 context,
-            ) {
-                cameraGestureHelper
-            }
+                packageManager,
+                { cameraGestureHelper },
+                userTracker,
+                devicePolicyManager,
+                testDispatcher,
+            )
     }
 
     @Test
@@ -62,4 +82,59 @@
             .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
         assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
     }
+
+    @Test
+    fun `getPickerScreenState - default when launchable`() =
+        testScope.runTest {
+            setLaunchable(true)
+
+            Truth.assertThat(underTest.getPickerScreenState())
+                .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+        }
+
+    @Test
+    fun `getPickerScreenState - unavailable when camera app not installed`() =
+        testScope.runTest {
+            setLaunchable(isCameraAppInstalled = false)
+
+            Truth.assertThat(underTest.getPickerScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+        }
+
+    @Test
+    fun `getPickerScreenState - unavailable when camera disabled by admin`() =
+        testScope.runTest {
+            setLaunchable(isCameraDisabledByDeviceAdmin = true)
+
+            Truth.assertThat(underTest.getPickerScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+        }
+
+    @Test
+    fun `getPickerScreenState - unavailable when secure camera disabled by admin`() =
+        testScope.runTest {
+            setLaunchable(isSecureCameraDisabledByDeviceAdmin = true)
+
+            Truth.assertThat(underTest.getPickerScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+        }
+
+    private fun setLaunchable(
+        isCameraAppInstalled: Boolean = true,
+        isCameraDisabledByDeviceAdmin: Boolean = false,
+        isSecureCameraDisabledByDeviceAdmin: Boolean = false,
+    ) {
+        whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
+            .thenReturn(isCameraAppInstalled)
+        whenever(devicePolicyManager.getCameraDisabled(null, userTracker.userId))
+            .thenReturn(isCameraDisabledByDeviceAdmin)
+        whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
+            .thenReturn(
+                if (isSecureCameraDisabledByDeviceAdmin) {
+                    DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA
+                } else {
+                    0
+                }
+            )
+    }
 }
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 7c10108..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
@@ -38,6 +39,7 @@
 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.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestDispatcher
@@ -50,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
@@ -59,7 +60,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var zenModeController: ZenModeController
@@ -83,169 +84,205 @@
 
         settings = FakeSettings()
 
-        underTest = DoNotDisturbQuickAffordanceConfig(
-            context,
-            zenModeController,
-            settings,
-            userTracker,
-            testDispatcher,
-            conditionUri,
-            enableZenModeDialog,
-        )
+        underTest =
+            DoNotDisturbQuickAffordanceConfig(
+                context,
+                zenModeController,
+                settings,
+                userTracker,
+                testDispatcher,
+                conditionUri,
+                enableZenModeDialog,
+            )
     }
 
     @Test
-    fun `dnd not available - picker state hidden`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(false)
+    fun `dnd not available - picker state hidden`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(false)
 
-        //when
-        val result = underTest.getPickerScreenState()
+            // when
+            val result = underTest.getPickerScreenState()
 
-        //then
-        assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result)
-    }
+            // then
+            assertEquals(
+                KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice,
+                result
+            )
+        }
 
     @Test
-    fun `dnd available - picker state visible`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
+    fun `dnd available - picker state visible`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
 
-        //when
-        val result = underTest.getPickerScreenState()
+            // when
+            val result = underTest.getPickerScreenState()
 
-        //then
-        assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default, result)
-    }
+            // then
+            assertThat(result)
+                .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+            val defaultPickerState =
+                result as KeyguardQuickAffordanceConfig.PickerScreenState.Default
+            assertThat(defaultPickerState.configureIntent).isNotNull()
+            assertThat(defaultPickerState.configureIntent?.action)
+                .isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+        }
 
     @Test
-    fun `onTriggered - dnd mode is not ZEN_MODE_OFF - set to ZEN_MODE_OFF`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
-        whenever(zenModeController.zen).thenReturn(-1)
-        settings.putInt(Settings.Secure.ZEN_DURATION, -2)
-        collectLastValue(underTest.lockScreenState)
-        runCurrent()
+    fun `onTriggered - dnd mode is not ZEN_MODE_OFF - set to ZEN_MODE_OFF`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
+            whenever(zenModeController.zen).thenReturn(-1)
+            settings.putInt(Settings.Secure.ZEN_DURATION, -2)
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
 
-        //when
-        val result = underTest.onTriggered(null)
-        verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG))
+            // when
+            val result = underTest.onTriggered(null)
+            verify(zenModeController)
+                .setZen(
+                    spyZenMode.capture(),
+                    spyConditionId.capture(),
+                    eq(DoNotDisturbQuickAffordanceConfig.TAG)
+                )
 
-        //then
-        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
-        assertEquals(ZEN_MODE_OFF, spyZenMode.value)
-        assertNull(spyConditionId.value)
-    }
+            // then
+            assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+            assertEquals(ZEN_MODE_OFF, spyZenMode.value)
+            assertNull(spyConditionId.value)
+        }
 
     @Test
-    fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is FOREVER - set zen with no condition`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
-        whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
-        settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
-        collectLastValue(underTest.lockScreenState)
-        runCurrent()
+    fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting FOREVER - set zen without condition`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
+            whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+            settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
 
-        //when
-        val result = underTest.onTriggered(null)
-        verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG))
+            // when
+            val result = underTest.onTriggered(null)
+            verify(zenModeController)
+                .setZen(
+                    spyZenMode.capture(),
+                    spyConditionId.capture(),
+                    eq(DoNotDisturbQuickAffordanceConfig.TAG)
+                )
 
-        //then
-        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
-        assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
-        assertNull(spyConditionId.value)
-    }
+            // then
+            assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+            assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
+            assertNull(spyConditionId.value)
+        }
 
     @Test
-    fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is not FOREVER or PROMPT - set zen with condition`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
-        whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
-        settings.putInt(Settings.Secure.ZEN_DURATION, -900)
-        collectLastValue(underTest.lockScreenState)
-        runCurrent()
+    fun `onTriggered - dnd ZEN_MODE_OFF - setting not FOREVER or PROMPT - zen with condition`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
+            whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+            settings.putInt(Settings.Secure.ZEN_DURATION, -900)
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
 
-        //when
-        val result = underTest.onTriggered(null)
-        verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG))
+            // when
+            val result = underTest.onTriggered(null)
+            verify(zenModeController)
+                .setZen(
+                    spyZenMode.capture(),
+                    spyConditionId.capture(),
+                    eq(DoNotDisturbQuickAffordanceConfig.TAG)
+                )
 
-        //then
-        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
-        assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
-        assertEquals(conditionUri, spyConditionId.value)
-    }
+            // then
+            assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+            assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
+            assertEquals(conditionUri, spyConditionId.value)
+        }
 
     @Test
-    fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is PROMPT - show dialog`() = testScope.runTest {
-        //given
-        val expandable: Expandable = mock()
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
-        whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
-        settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
-        whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
-        collectLastValue(underTest.lockScreenState)
-        runCurrent()
+    fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is PROMPT - show dialog`() =
+        testScope.runTest {
+            // given
+            val expandable: Expandable = mock()
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
+            whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+            settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
+            whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
 
-        //when
-        val result = underTest.onTriggered(expandable)
+            // when
+            val result = underTest.onTriggered(expandable)
 
-        //then
-        assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
-        assertEquals(expandable, (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable)
-    }
+            // then
+            assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
+            assertEquals(
+                expandable,
+                (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable
+            )
+        }
 
     @Test
-    fun `lockScreenState - dndAvailable starts as true - changes to false - State moves to Hidden`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
-        val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
-        val valueSnapshot = collectLastValue(underTest.lockScreenState)
-        val secondLastValue = valueSnapshot()
-        verify(zenModeController).addCallback(callbackCaptor.capture())
+    fun `lockScreenState - dndAvailable starts as true - change to false - State is Hidden`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
+            val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
+            val valueSnapshot = collectLastValue(underTest.lockScreenState)
+            val secondLastValue = valueSnapshot()
+            verify(zenModeController).addCallback(callbackCaptor.capture())
 
-        //when
-        callbackCaptor.value.onZenAvailableChanged(false)
-        val lastValue = valueSnapshot()
+            // when
+            callbackCaptor.value.onZenAvailableChanged(false)
+            val lastValue = valueSnapshot()
 
-        //then
-        assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
-        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
-    }
+            // then
+            assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+            assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        }
 
     @Test
-    fun `lockScreenState - dndMode starts as ZEN_MODE_OFF - changes to not OFF - State moves to Visible`() = testScope.runTest {
-        //given
-        whenever(zenModeController.isZenAvailable).thenReturn(true)
-        whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
-        val valueSnapshot = collectLastValue(underTest.lockScreenState)
-        val secondLastValue = valueSnapshot()
-        val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
-        verify(zenModeController).addCallback(callbackCaptor.capture())
+    fun `lockScreenState - dndMode starts as ZEN_MODE_OFF - change to not OFF - State Visible`() =
+        testScope.runTest {
+            // given
+            whenever(zenModeController.isZenAvailable).thenReturn(true)
+            whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+            val valueSnapshot = collectLastValue(underTest.lockScreenState)
+            val secondLastValue = valueSnapshot()
+            val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
+            verify(zenModeController).addCallback(callbackCaptor.capture())
 
-        //when
-        callbackCaptor.value.onZenChanged(ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-        val lastValue = valueSnapshot()
+            // when
+            callbackCaptor.value.onZenChanged(ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            val lastValue = valueSnapshot()
 
-        //then
-        assertEquals(
-            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                Icon.Resource(
-                    R.drawable.qs_dnd_icon_off,
-                    ContentDescription.Resource(R.string.dnd_is_off)
+            // then
+            assertEquals(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    Icon.Resource(
+                        R.drawable.qs_dnd_icon_off,
+                        ContentDescription.Resource(R.string.dnd_is_off)
+                    ),
+                    ActivationState.Inactive
                 ),
-                ActivationState.Inactive
-            ),
-            secondLastValue,
-        )
-        assertEquals(
-            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                Icon.Resource(
-                    R.drawable.qs_dnd_icon_on,
-                    ContentDescription.Resource(R.string.dnd_is_on)
+                secondLastValue,
+            )
+            assertEquals(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    Icon.Resource(
+                        R.drawable.qs_dnd_icon_on,
+                        ContentDescription.Resource(R.string.dnd_is_on)
+                    ),
+                    ActivationState.Active
                 ),
-                ActivationState.Active
-            ),
-            lastValue,
-        )
-    }
-}
\ No newline at end of file
+                lastValue,
+            )
+        }
+}
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/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..925c06f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+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.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MuteQuickAffordanceConfigTest : SysuiTestCase() {
+
+    private lateinit var underTest: MuteQuickAffordanceConfig
+    @Mock
+    private lateinit var ringerModeTracker: RingerModeTracker
+    @Mock
+    private lateinit var audioManager: AudioManager
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var userFileManager: UserFileManager
+
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+
+        whenever(userTracker.userContext).thenReturn(context)
+        whenever(userFileManager.getSharedPreferences(any(), any(), any()))
+                .thenReturn(context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE))
+
+        underTest = MuteQuickAffordanceConfig(
+                context,
+                userTracker,
+                userFileManager,
+                ringerModeTracker,
+                audioManager,
+                testScope.backgroundScope,
+                testDispatcher,
+                testDispatcher,
+        )
+    }
+
+    @Test
+    fun `picker state - volume fixed - not available`() = testScope.runTest {
+        //given
+        whenever(audioManager.isVolumeFixed).thenReturn(true)
+
+        //when
+        val result = underTest.getPickerScreenState()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result)
+    }
+
+    @Test
+    fun `picker state - volume not fixed - available`() = testScope.runTest {
+        //given
+        whenever(audioManager.isVolumeFixed).thenReturn(false)
+
+        //when
+        val result = underTest.getPickerScreenState()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result)
+    }
+
+    @Test
+    fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() = testScope.runTest {
+        //given
+        val ringerModeCapture = argumentCaptor<Int>()
+        whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+        underTest.onTriggered(null)
+        whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
+
+        //when
+        val result = underTest.onTriggered(null)
+        runCurrent()
+        verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+        assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value)
+    }
+
+    @Test
+    fun `triggered - state is not SILENT - move to SILENT ringer`() = testScope.runTest {
+        //given
+        val ringerModeCapture = argumentCaptor<Int>()
+        whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+
+        //when
+        val result = underTest.onTriggered(null)
+        runCurrent()
+        verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+        assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value)
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..facc747
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.content.Context
+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
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var featureFlags: FeatureFlags
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var ringerModeTracker: RingerModeTracker
+    @Mock
+    private lateinit var userFileManager: UserFileManager
+    @Mock
+    private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
+
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var testScope: TestScope
+
+    private lateinit var underTest: MuteQuickAffordanceCoreStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(true)
+
+        val config: KeyguardQuickAffordanceConfig = mock()
+        whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE)
+
+        val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
+        whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+
+        testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+
+        underTest = MuteQuickAffordanceCoreStartable(
+            featureFlags,
+            userTracker,
+            ringerModeTracker,
+            userFileManager,
+            keyguardQuickAffordanceRepository,
+            testScope.backgroundScope,
+            testDispatcher,
+        )
+    }
+
+    @Test
+    fun `feature flag is OFF - do nothing with keyguardQuickAffordanceRepository`() = testScope.runTest {
+        //given
+        whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(false)
+
+        //when
+        underTest.start()
+
+        //then
+        verifyZeroInteractions(keyguardQuickAffordanceRepository)
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `feature flag is ON - call to keyguardQuickAffordanceRepository`() = testScope.runTest {
+        //given
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(keyguardQuickAffordanceRepository).selections
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `ringer mode is changed to SILENT - do not save to shared preferences`() = testScope.runTest {
+        //given
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        val observerCaptor = argumentCaptor<Observer<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+        verify(ringerModeInternal).observeForever(observerCaptor.capture())
+        observerCaptor.value.onChanged(AudioManager.RINGER_MODE_SILENT)
+
+        //then
+        verifyZeroInteractions(userFileManager)
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `ringerModeInternal changes to something not SILENT - is set in sharedpreferences`() = testScope.runTest {
+        //given
+        val newRingerMode = 99
+        val observerCaptor = argumentCaptor<Observer<Int>>()
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        val sharedPrefs = context.getSharedPreferences("quick_affordance_mute_ringer_mode_cache_test", Context.MODE_PRIVATE)
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+        whenever(
+            userFileManager.getSharedPreferences(eq("quick_affordance_mute_ringer_mode_cache"), any(), any())
+        ).thenReturn(sharedPrefs)
+
+        //when
+        underTest.start()
+        runCurrent()
+        verify(ringerModeInternal).observeForever(observerCaptor.capture())
+        observerCaptor.value.onChanged(newRingerMode)
+        runCurrent()
+        val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
+
+        //then
+        assertEquals(newRingerMode, result)
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `MUTE is in selections - observe ringerModeInternal`() = testScope.runTest {
+        //given
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(ringerModeInternal).observeForever(any())
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `MUTE is in selections 2x - observe ringerModeInternal`() = testScope.runTest {
+        //given
+        val config: KeyguardQuickAffordanceConfig = mock()
+        whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE)
+        val emission = MutableStateFlow(mapOf("testKey" to listOf(config), "testkey2" to listOf(config)))
+        whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(ringerModeInternal).observeForever(any())
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `MUTE is not in selections - stop observing ringerModeInternal`() = testScope.runTest {
+        //given
+        val config: KeyguardQuickAffordanceConfig = mock()
+        whenever(config.key).thenReturn("notmutequickaffordance")
+        val emission = MutableStateFlow(mapOf("testKey" to listOf(config)))
+        whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(ringerModeInternal).removeObserver(any())
+        coroutineContext.cancelChildren()
+    }
+}
\ No newline at end of file
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 6255980..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
@@ -141,7 +141,7 @@
         whenever(controller.isAbleToOpenCameraApp).thenReturn(true)
 
         assertThat(underTest.getPickerScreenState())
-            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index d875dd9..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
@@ -114,20 +114,8 @@
     }
 
     @Test
-    fun `affordance - missing icon - model is none`() = runBlockingTest {
-        setUpState(hasWalletIcon = false)
-        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
-
-        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
-
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
-
-        job.cancel()
-    }
-
-    @Test
     fun `affordance - no selected card - model is none`() = runBlockingTest {
-        setUpState(hasWalletIcon = false)
+        setUpState(hasSelectedCard = false)
         var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
         val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
@@ -159,13 +147,13 @@
         setUpState()
 
         assertThat(underTest.getPickerScreenState())
-            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
     }
 
     @Test
     fun `getPickerScreenState - unavailable`() = runTest {
         setUpState(
-            isWalletEnabled = false,
+            isWalletServiceAvailable = false,
         )
 
         assertThat(underTest.getPickerScreenState())
@@ -173,9 +161,9 @@
     }
 
     @Test
-    fun `getPickerScreenState - disabled when there is no icon`() = runTest {
+    fun `getPickerScreenState - disabled when the feature is not enabled`() = runTest {
         setUpState(
-            hasWalletIcon = false,
+            isWalletEnabled = false,
         )
 
         assertThat(underTest.getPickerScreenState())
@@ -194,20 +182,16 @@
 
     private fun setUpState(
         isWalletEnabled: Boolean = true,
+        isWalletServiceAvailable: Boolean = true,
         isWalletQuerySuccessful: Boolean = true,
-        hasWalletIcon: Boolean = true,
         hasSelectedCard: Boolean = true,
     ) {
         whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled)
 
         val walletClient: QuickAccessWalletClient = mock()
-        val icon: Drawable? =
-            if (hasWalletIcon) {
-                ICON
-            } else {
-                null
-            }
-        whenever(walletClient.tileIcon).thenReturn(icon)
+        whenever(walletClient.tileIcon).thenReturn(ICON)
+        whenever(walletClient.isWalletServiceAvailable).thenReturn(isWalletServiceAvailable)
+
         whenever(walletController.walletClient).thenReturn(walletClient)
 
         whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
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
new file mode 100644
index 0000000..f1b9c5f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.app.admin.DevicePolicyManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.camera.CameraIntentsWrapper
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VideoCameraQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+
+    private lateinit var underTest: VideoCameraQuickAffordanceConfig
+    private lateinit var userTracker: UserTracker
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        userTracker = FakeUserTracker()
+        underTest =
+            VideoCameraQuickAffordanceConfig(
+                context = context,
+                cameraIntents = CameraIntentsWrapper(context),
+                activityIntentHelper = activityIntentHelper,
+                userTracker = userTracker,
+                devicePolicyManager = devicePolicyManager,
+                backgroundDispatcher = testDispatcher,
+            )
+    }
+
+    @Test
+    fun `lockScreenState - visible when launchable`() =
+        testScope.runTest {
+            setLaunchable()
+
+            val lockScreenState = collectLastValue(underTest.lockScreenState)
+
+            assertThat(lockScreenState())
+                .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java)
+        }
+
+    @Test
+    fun `lockScreenState - hidden when app not installed on device`() =
+        testScope.runTest {
+            setLaunchable(isVideoCameraAppInstalled = false)
+
+            val lockScreenState = collectLastValue(underTest.lockScreenState)
+
+            assertThat(lockScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        }
+
+    @Test
+    fun `lockScreenState - hidden when camera disabled by admin`() =
+        testScope.runTest {
+            setLaunchable(isCameraDisabledByAdmin = true)
+
+            val lockScreenState = collectLastValue(underTest.lockScreenState)
+
+            assertThat(lockScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        }
+
+    @Test
+    fun `getPickerScreenState - default when launchable`() =
+        testScope.runTest {
+            setLaunchable()
+
+            assertThat(underTest.getPickerScreenState())
+                .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+        }
+
+    @Test
+    fun `getPickerScreenState - unavailable when app not installed on device`() =
+        testScope.runTest {
+            setLaunchable(isVideoCameraAppInstalled = false)
+
+            assertThat(underTest.getPickerScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+        }
+
+    @Test
+    fun `getPickerScreenState - unavailable when camera disabled by admin`() =
+        testScope.runTest {
+            setLaunchable(isCameraDisabledByAdmin = true)
+
+            assertThat(underTest.getPickerScreenState())
+                .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+        }
+
+    private fun setLaunchable(
+        isVideoCameraAppInstalled: Boolean = true,
+        isCameraDisabledByAdmin: Boolean = false,
+    ) {
+        whenever(
+                activityIntentHelper.getTargetActivityInfo(
+                    any(),
+                    anyInt(),
+                    anyBoolean(),
+                )
+            )
+            .thenReturn(
+                if (isVideoCameraAppInstalled) {
+                    mock()
+                } else {
+                    null
+                }
+            )
+        whenever(devicePolicyManager.getCameraDisabled(null, userTracker.userId))
+            .thenReturn(isCameraDisabledByAdmin)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
new file mode 100644
index 0000000..5dc04f7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FACE
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.BiometricType.FACE
+import com.android.systemui.keyguard.data.repository.BiometricType.REAR_FINGERPRINT
+import com.android.systemui.keyguard.data.repository.BiometricType.SIDE_FINGERPRINT
+import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY_FINGERPRINT
+import com.android.systemui.keyguard.shared.model.DevicePosture
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+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.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class BiometricSettingsRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: BiometricSettingsRepository
+
+    @Mock private lateinit var authController: AuthController
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var biometricManager: BiometricManager
+    @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback>
+    @Captor
+    private lateinit var biometricManagerCallback:
+        ArgumentCaptor<IBiometricEnabledOnKeyguardCallback.Stub>
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var devicePostureRepository: FakeDevicePostureRepository
+
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var testScope: TestScope
+    private var testableLooper: TestableLooper? = null
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+        testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        userRepository = FakeUserRepository()
+        devicePostureRepository = FakeDevicePostureRepository()
+    }
+
+    private suspend fun createBiometricSettingsRepository() {
+        userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+        userRepository.setSelectedUserInfo(PRIMARY_USER)
+        underTest =
+            BiometricSettingsRepositoryImpl(
+                context = context,
+                lockPatternUtils = lockPatternUtils,
+                broadcastDispatcher = fakeBroadcastDispatcher,
+                authController = authController,
+                userRepository = userRepository,
+                devicePolicyManager = devicePolicyManager,
+                scope = testScope.backgroundScope,
+                backgroundDispatcher = testDispatcher,
+                looper = testableLooper!!.looper,
+                dumpManager = dumpManager,
+                biometricManager = biometricManager,
+                devicePostureRepository = devicePostureRepository,
+            )
+        testScope.runCurrent()
+    }
+
+    @Test
+    fun fingerprintEnrollmentChange() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            val fingerprintEnrolled = collectLastValue(underTest.isFingerprintEnrolled)
+            runCurrent()
+
+            verify(authController).addCallback(authControllerCallback.capture())
+            whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(true)
+            enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true)
+            assertThat(fingerprintEnrolled()).isTrue()
+
+            whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(false)
+            enrollmentChange(UNDER_DISPLAY_FINGERPRINT, ANOTHER_USER_ID, false)
+            assertThat(fingerprintEnrolled()).isTrue()
+
+            enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, false)
+            assertThat(fingerprintEnrolled()).isFalse()
+        }
+
+    @Test
+    fun strongBiometricAllowedChange() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed)
+            runCurrent()
+
+            val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>()
+            verify(lockPatternUtils).registerStrongAuthTracker(captor.capture())
+
+            captor.value.stub.onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
+            testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper
+            assertThat(strongBiometricAllowed()).isTrue()
+
+            captor.value.stub.onStrongAuthRequiredChanged(
+                STRONG_AUTH_REQUIRED_AFTER_BOOT,
+                PRIMARY_USER_ID
+            )
+            testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper
+            assertThat(strongBiometricAllowed()).isFalse()
+        }
+
+    @Test
+    fun fingerprintDisabledByDpmChange() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            val fingerprintEnabledByDevicePolicy =
+                collectLastValue(underTest.isFingerprintEnabledByDevicePolicy)
+            runCurrent()
+
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt()))
+                .thenReturn(KEYGUARD_DISABLE_FINGERPRINT)
+            broadcastDPMStateChange()
+            assertThat(fingerprintEnabledByDevicePolicy()).isFalse()
+
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())).thenReturn(0)
+            broadcastDPMStateChange()
+            assertThat(fingerprintEnabledByDevicePolicy()).isTrue()
+        }
+
+    @Test
+    fun faceEnrollmentChangeIsPropagatedForTheCurrentUser() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            runCurrent()
+            clearInvocations(authController)
+
+            whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(false)
+            val faceEnrolled = collectLastValue(underTest.isFaceEnrolled)
+
+            assertThat(faceEnrolled()).isFalse()
+            verify(authController).addCallback(authControllerCallback.capture())
+            enrollmentChange(REAR_FINGERPRINT, PRIMARY_USER_ID, true)
+
+            assertThat(faceEnrolled()).isFalse()
+
+            enrollmentChange(SIDE_FINGERPRINT, PRIMARY_USER_ID, true)
+
+            assertThat(faceEnrolled()).isFalse()
+
+            enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true)
+
+            assertThat(faceEnrolled()).isFalse()
+
+            enrollmentChange(FACE, ANOTHER_USER_ID, true)
+
+            assertThat(faceEnrolled()).isFalse()
+
+            enrollmentChange(FACE, PRIMARY_USER_ID, true)
+
+            assertThat(faceEnrolled()).isTrue()
+        }
+
+    @Test
+    fun faceEnrollmentStatusOfNewUserUponUserSwitch() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            runCurrent()
+            clearInvocations(authController)
+
+            whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(false)
+            whenever(authController.isFaceAuthEnrolled(ANOTHER_USER_ID)).thenReturn(true)
+            val faceEnrolled = collectLastValue(underTest.isFaceEnrolled)
+
+            assertThat(faceEnrolled()).isFalse()
+        }
+
+    @Test
+    fun faceEnrollmentChangesArePropagatedAfterUserSwitch() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+
+            userRepository.setSelectedUserInfo(ANOTHER_USER)
+            runCurrent()
+            clearInvocations(authController)
+
+            val faceEnrolled = collectLastValue(underTest.isFaceEnrolled)
+            runCurrent()
+
+            verify(authController).addCallback(authControllerCallback.capture())
+
+            enrollmentChange(FACE, ANOTHER_USER_ID, true)
+
+            assertThat(faceEnrolled()).isTrue()
+        }
+
+    @Test
+    fun devicePolicyControlsFaceAuthenticationEnabledState() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            verify(biometricManager)
+                .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
+
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
+                .thenReturn(KEYGUARD_DISABLE_FINGERPRINT or KEYGUARD_DISABLE_FACE)
+
+            val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled)
+            runCurrent()
+
+            broadcastDPMStateChange()
+
+            assertThat(isFaceAuthEnabled()).isFalse()
+
+            biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
+            runCurrent()
+            assertThat(isFaceAuthEnabled()).isFalse()
+
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
+                .thenReturn(KEYGUARD_DISABLE_FINGERPRINT)
+            broadcastDPMStateChange()
+
+            assertThat(isFaceAuthEnabled()).isTrue()
+        }
+
+    @Test
+    fun biometricManagerControlsFaceAuthenticationEnabledStatus() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            verify(biometricManager)
+                .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
+
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
+                .thenReturn(0)
+            broadcastDPMStateChange()
+
+            biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
+            val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled)
+
+            assertThat(isFaceAuthEnabled()).isTrue()
+
+            biometricManagerCallback.value.onChanged(false, PRIMARY_USER_ID)
+
+            assertThat(isFaceAuthEnabled()).isFalse()
+        }
+
+    @Test
+    fun biometricManagerCallbackIsRegisteredOnlyOnce() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+
+            collectLastValue(underTest.isFaceAuthenticationEnabled)()
+            collectLastValue(underTest.isFaceAuthenticationEnabled)()
+            collectLastValue(underTest.isFaceAuthenticationEnabled)()
+
+            verify(biometricManager, times(1)).registerEnabledOnKeyguardCallback(any())
+        }
+
+    @Test
+    fun faceAuthIsAlwaysSupportedIfSpecificPostureIsNotConfigured() =
+        testScope.runTest {
+            overrideResource(
+                R.integer.config_face_auth_supported_posture,
+                DevicePostureController.DEVICE_POSTURE_UNKNOWN
+            )
+
+            createBiometricSettingsRepository()
+
+            assertThat(collectLastValue(underTest.isFaceAuthSupportedInCurrentPosture)()).isTrue()
+        }
+
+    @Test
+    fun faceAuthIsSupportedOnlyWhenDevicePostureMatchesConfigValue() =
+        testScope.runTest {
+            overrideResource(
+                R.integer.config_face_auth_supported_posture,
+                DevicePostureController.DEVICE_POSTURE_FLIPPED
+            )
+
+            createBiometricSettingsRepository()
+
+            val isFaceAuthSupported =
+                collectLastValue(underTest.isFaceAuthSupportedInCurrentPosture)
+
+            assertThat(isFaceAuthSupported()).isFalse()
+
+            devicePostureRepository.setCurrentPosture(DevicePosture.CLOSED)
+            assertThat(isFaceAuthSupported()).isFalse()
+
+            devicePostureRepository.setCurrentPosture(DevicePosture.HALF_OPENED)
+            assertThat(isFaceAuthSupported()).isFalse()
+
+            devicePostureRepository.setCurrentPosture(DevicePosture.OPENED)
+            assertThat(isFaceAuthSupported()).isFalse()
+
+            devicePostureRepository.setCurrentPosture(DevicePosture.UNKNOWN)
+            assertThat(isFaceAuthSupported()).isFalse()
+
+            devicePostureRepository.setCurrentPosture(DevicePosture.FLIPPED)
+            assertThat(isFaceAuthSupported()).isTrue()
+        }
+
+    private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) {
+        authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled)
+    }
+
+    private fun broadcastDPMStateChange() {
+        fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+            receiver.onReceive(
+                context,
+                Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)
+            )
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+
+        private const val ANOTHER_USER_ID = 1
+        private val ANOTHER_USER =
+            UserInfo(
+                /* id= */ ANOTHER_USER_ID,
+                /* name= */ "another user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
new file mode 100644
index 0000000..0519a44
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.hardware.biometrics.BiometricSourceType
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+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
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var dumpManager: DumpManager
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback>
+
+    private lateinit var testScope: TestScope
+
+    private lateinit var underTest: DeviceEntryFingerprintAuthRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testScope = TestScope()
+
+        underTest =
+            DeviceEntryFingerprintAuthRepositoryImpl(
+                keyguardUpdateMonitor,
+                testScope.backgroundScope,
+                dumpManager,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        verify(keyguardUpdateMonitor).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun isLockedOut_whenFingerprintLockoutStateChanges_emitsNewValue() =
+        testScope.runTest {
+            val isLockedOutValue = collectLastValue(underTest.isLockedOut)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(callbackCaptor.capture())
+            val callback = callbackCaptor.value
+            whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
+
+            callback.onLockedOutStateChanged(BiometricSourceType.FACE)
+            assertThat(isLockedOutValue()).isFalse()
+
+            callback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT)
+            assertThat(isLockedOutValue()).isTrue()
+
+            whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
+            callback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT)
+            assertThat(isLockedOutValue()).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
new file mode 100644
index 0000000..bd6b7a8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.keyguard.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.shared.model.DevicePosture
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class DevicePostureRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: DevicePostureRepository
+    private lateinit var testScope: TestScope
+    @Mock private lateinit var devicePostureController: DevicePostureController
+    @Captor private lateinit var callback: ArgumentCaptor<DevicePostureController.Callback>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        testScope = TestScope()
+        underTest = DevicePostureRepositoryImpl(postureController = devicePostureController)
+    }
+
+    @Test
+    fun postureChangesArePropagated() =
+        testScope.runTest {
+            whenever(devicePostureController.devicePosture)
+                .thenReturn(DevicePostureController.DEVICE_POSTURE_FLIPPED)
+            val currentPosture = collectLastValue(underTest.currentDevicePosture)
+            assertThat(currentPosture()).isEqualTo(DevicePosture.FLIPPED)
+
+            verify(devicePostureController).addCallback(callback.capture())
+
+            callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_UNKNOWN)
+            assertThat(currentPosture()).isEqualTo(DevicePosture.UNKNOWN)
+
+            callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_CLOSED)
+            assertThat(currentPosture()).isEqualTo(DevicePosture.CLOSED)
+
+            callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED)
+            assertThat(currentPosture()).isEqualTo(DevicePosture.HALF_OPENED)
+
+            callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED)
+            assertThat(currentPosture()).isEqualTo(DevicePosture.OPENED)
+
+            callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_FLIPPED)
+            assertThat(currentPosture()).isEqualTo(DevicePosture.FLIPPED)
+        }
+}
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 9970a67..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,24 +16,26 @@
 
 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
 import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.util.time.SystemClock
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.TestCoroutineScope
 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
     @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback
     @Mock private lateinit var bouncerLogger: TableLogBuffer
     lateinit var underTest: KeyguardBouncerRepository
@@ -43,7 +45,12 @@
         MockitoAnnotations.initMocks(this)
         val testCoroutineScope = TestCoroutineScope()
         underTest =
-            KeyguardBouncerRepository(viewMediatorCallback, testCoroutineScope, bouncerLogger)
+            KeyguardBouncerRepositoryImpl(
+                viewMediatorCallback,
+                systemClock,
+                testCoroutineScope,
+                bouncerLogger,
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt
new file mode 100644
index 0000000..7c604f7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthManagerTest.kt
@@ -0,0 +1,428 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.app.StatusBarManager.SESSION_KEYGUARD
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_CANCELED
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT
+import android.hardware.biometrics.ComponentInfoInternal
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorProperties
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.os.CancellationSignal
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId.fakeInstanceId
+import com.android.internal.logging.UiEventLogger
+import com.android.keyguard.FaceAuthUiEvent
+import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN
+import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.FlowValue
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.dump.logcatLogBuffer
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus
+import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
+import com.android.systemui.log.FaceAuthenticationLogger
+import com.android.systemui.log.SessionTracker
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+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.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardFaceAuthManagerTest : SysuiTestCase() {
+    private lateinit var underTest: KeyguardFaceAuthManagerImpl
+
+    @Mock private lateinit var faceManager: FaceManager
+    @Mock private lateinit var bypassController: KeyguardBypassController
+    @Mock private lateinit var sessionTracker: SessionTracker
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var dumpManager: DumpManager
+
+    @Captor
+    private lateinit var authenticationCallback: ArgumentCaptor<FaceManager.AuthenticationCallback>
+    @Captor
+    private lateinit var detectionCallback: ArgumentCaptor<FaceManager.FaceDetectionCallback>
+    @Captor private lateinit var cancellationSignal: ArgumentCaptor<CancellationSignal>
+    @Captor
+    private lateinit var faceLockoutResetCallback: ArgumentCaptor<FaceManager.LockoutResetCallback>
+    private lateinit var testDispatcher: TestDispatcher
+
+    private lateinit var testScope: TestScope
+    private lateinit var fakeUserRepository: FakeUserRepository
+    private lateinit var authStatus: FlowValue<AuthenticationStatus?>
+    private lateinit var detectStatus: FlowValue<DetectionStatus?>
+    private lateinit var authRunning: FlowValue<Boolean?>
+    private lateinit var lockedOut: FlowValue<Boolean?>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        fakeUserRepository = FakeUserRepository()
+        fakeUserRepository.setUserInfos(listOf(currentUser))
+        testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        whenever(sessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(keyguardSessionId)
+        whenever(bypassController.bypassEnabled).thenReturn(true)
+        underTest = createFaceAuthManagerImpl(faceManager)
+    }
+
+    private fun createFaceAuthManagerImpl(
+        fmOverride: FaceManager? = faceManager,
+        bypassControllerOverride: KeyguardBypassController? = bypassController
+    ) =
+        KeyguardFaceAuthManagerImpl(
+            mContext,
+            fmOverride,
+            fakeUserRepository,
+            bypassControllerOverride,
+            testScope.backgroundScope,
+            testDispatcher,
+            sessionTracker,
+            uiEventLogger,
+            FaceAuthenticationLogger(logcatLogBuffer("KeyguardFaceAuthManagerLog")),
+            dumpManager,
+        )
+
+    @Test
+    fun faceAuthRunsAndProvidesAuthStatusUpdates() =
+        testScope.runTest {
+            testSetup(this)
+
+            FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER.extraInfo = 10
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+            uiEventIsLogged(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+
+            assertThat(authRunning()).isTrue()
+
+            val successResult = successResult()
+            authenticationCallback.value.onAuthenticationSucceeded(successResult)
+
+            assertThat(authStatus()).isEqualTo(SuccessAuthenticationStatus(successResult))
+
+            assertThat(authRunning()).isFalse()
+        }
+
+    private fun uiEventIsLogged(faceAuthUiEvent: FaceAuthUiEvent) {
+        verify(uiEventLogger)
+            .logWithInstanceIdAndPosition(
+                faceAuthUiEvent,
+                0,
+                null,
+                keyguardSessionId,
+                faceAuthUiEvent.extraInfo
+            )
+    }
+
+    @Test
+    fun faceAuthDoesNotRunWhileItIsAlreadyRunning() =
+        testScope.runTest {
+            testSetup(this)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+            clearInvocations(faceManager)
+            clearInvocations(uiEventLogger)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            verifyNoMoreInteractions(faceManager)
+            verifyNoMoreInteractions(uiEventLogger)
+        }
+
+    @Test
+    fun faceLockoutStatusIsPropagated() =
+        testScope.runTest {
+            testSetup(this)
+            verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture())
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+
+            authenticationCallback.value.onAuthenticationError(
+                FACE_ERROR_LOCKOUT_PERMANENT,
+                "face locked out"
+            )
+
+            assertThat(lockedOut()).isTrue()
+
+            faceLockoutResetCallback.value.onLockoutReset(0)
+            assertThat(lockedOut()).isFalse()
+        }
+
+    @Test
+    fun faceDetectionSupportIsTheCorrectValue() =
+        testScope.runTest {
+            assertThat(createFaceAuthManagerImpl(fmOverride = null).isDetectionSupported).isFalse()
+
+            whenever(faceManager.sensorPropertiesInternal).thenReturn(null)
+            assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
+
+            whenever(faceManager.sensorPropertiesInternal).thenReturn(listOf())
+            assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
+
+            whenever(faceManager.sensorPropertiesInternal)
+                .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = false)))
+            assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
+
+            whenever(faceManager.sensorPropertiesInternal)
+                .thenReturn(
+                    listOf(
+                        createFaceSensorProperties(supportsFaceDetection = false),
+                        createFaceSensorProperties(supportsFaceDetection = true)
+                    )
+                )
+            assertThat(createFaceAuthManagerImpl().isDetectionSupported).isFalse()
+
+            whenever(faceManager.sensorPropertiesInternal)
+                .thenReturn(
+                    listOf(
+                        createFaceSensorProperties(supportsFaceDetection = true),
+                        createFaceSensorProperties(supportsFaceDetection = false)
+                    )
+                )
+            assertThat(createFaceAuthManagerImpl().isDetectionSupported).isTrue()
+        }
+
+    @Test
+    fun cancelStopsFaceAuthentication() =
+        testScope.runTest {
+            testSetup(this)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+
+            var wasAuthCancelled = false
+            cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true }
+
+            underTest.cancel()
+            assertThat(wasAuthCancelled).isTrue()
+            assertThat(authRunning()).isFalse()
+        }
+
+    @Test
+    fun cancelInvokedWithoutFaceAuthRunningIsANoop() = testScope.runTest { underTest.cancel() }
+
+    @Test
+    fun faceDetectionRunsAndPropagatesDetectionStatus() =
+        testScope.runTest {
+            whenever(faceManager.sensorPropertiesInternal)
+                .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true)))
+            underTest = createFaceAuthManagerImpl()
+            testSetup(this)
+
+            underTest.detect()
+            faceDetectIsCalled()
+
+            detectionCallback.value.onFaceDetected(1, 1, true)
+
+            assertThat(detectStatus()).isEqualTo(DetectionStatus(1, 1, true))
+        }
+
+    @Test
+    fun faceDetectDoesNotRunIfDetectionIsNotSupported() =
+        testScope.runTest {
+            whenever(faceManager.sensorPropertiesInternal)
+                .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = false)))
+            underTest = createFaceAuthManagerImpl()
+            testSetup(this)
+            clearInvocations(faceManager)
+
+            underTest.detect()
+
+            verify(faceManager, never()).detectFace(any(), any(), anyInt())
+        }
+
+    @Test
+    fun faceAuthShouldWaitAndRunIfTriggeredWhileCancelling() =
+        testScope.runTest {
+            testSetup(this)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+
+            // Enter cancelling state
+            underTest.cancel()
+            clearInvocations(faceManager)
+
+            // Auth is while cancelling.
+            underTest.authenticate(FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
+            // Auth is not started
+            verifyNoMoreInteractions(faceManager)
+
+            // Auth is done cancelling.
+            authenticationCallback.value.onAuthenticationError(
+                FACE_ERROR_CANCELED,
+                "First auth attempt cancellation completed"
+            )
+            assertThat(authStatus())
+                .isEqualTo(
+                    ErrorAuthenticationStatus(
+                        FACE_ERROR_CANCELED,
+                        "First auth attempt cancellation completed"
+                    )
+                )
+
+            faceAuthenticateIsCalled()
+            uiEventIsLogged(FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
+        }
+
+    @Test
+    fun faceAuthAutoCancelsAfterDefaultCancellationTimeout() =
+        testScope.runTest {
+            testSetup(this)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+
+            clearInvocations(faceManager)
+            underTest.cancel()
+            advanceTimeBy(KeyguardFaceAuthManagerImpl.DEFAULT_CANCEL_SIGNAL_TIMEOUT + 1)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+        }
+
+    @Test
+    fun faceHelpMessagesAreIgnoredBasedOnConfig() =
+        testScope.runTest {
+            overrideResource(
+                R.array.config_face_acquire_device_entry_ignorelist,
+                intArrayOf(10, 11)
+            )
+            underTest = createFaceAuthManagerImpl()
+            testSetup(this)
+
+            underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+            faceAuthenticateIsCalled()
+
+            authenticationCallback.value.onAuthenticationHelp(9, "help msg")
+            authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
+            authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
+
+            assertThat(authStatus()).isEqualTo(HelpAuthenticationStatus(9, "help msg"))
+        }
+
+    @Test
+    fun dumpDoesNotErrorOutWhenFaceManagerOrBypassControllerIsNull() =
+        testScope.runTest {
+            fakeUserRepository.setSelectedUserInfo(currentUser)
+            underTest.dump(PrintWriter(StringWriter()), emptyArray())
+
+            underTest =
+                createFaceAuthManagerImpl(fmOverride = null, bypassControllerOverride = null)
+            fakeUserRepository.setSelectedUserInfo(currentUser)
+
+            underTest.dump(PrintWriter(StringWriter()), emptyArray())
+        }
+
+    private suspend fun testSetup(testScope: TestScope) {
+        with(testScope) {
+            authStatus = collectLastValue(underTest.authenticationStatus)
+            detectStatus = collectLastValue(underTest.detectionStatus)
+            authRunning = collectLastValue(underTest.isAuthRunning)
+            lockedOut = collectLastValue(underTest.isLockedOut)
+            fakeUserRepository.setSelectedUserInfo(currentUser)
+        }
+    }
+
+    private fun successResult() = FaceManager.AuthenticationResult(null, null, currentUserId, false)
+
+    private fun faceDetectIsCalled() {
+        verify(faceManager)
+            .detectFace(
+                cancellationSignal.capture(),
+                detectionCallback.capture(),
+                eq(currentUserId)
+            )
+    }
+
+    private fun faceAuthenticateIsCalled() {
+        verify(faceManager)
+            .authenticate(
+                isNull(),
+                cancellationSignal.capture(),
+                authenticationCallback.capture(),
+                isNull(),
+                eq(currentUserId),
+                eq(true)
+            )
+    }
+
+    private fun createFaceSensorProperties(
+        supportsFaceDetection: Boolean
+    ): FaceSensorPropertiesInternal {
+        val componentInfo =
+            listOf(
+                ComponentInfoInternal(
+                    "faceSensor" /* componentId */,
+                    "vendor/model/revision" /* hardwareVersion */,
+                    "1.01" /* firmwareVersion */,
+                    "00000001" /* serialNumber */,
+                    "" /* softwareVersion */
+                )
+            )
+        return FaceSensorPropertiesInternal(
+            0 /* id */,
+            FaceSensorProperties.STRENGTH_STRONG,
+            1 /* maxTemplatesAllowed */,
+            componentInfo,
+            FaceSensorProperties.TYPE_UNKNOWN,
+            supportsFaceDetection /* supportsFaceDetection */,
+            true /* supportsSelfIllumination */,
+            false /* resetLockoutRequiresChallenge */
+        )
+    }
+
+    companion object {
+        const val currentUserId = 1
+        val keyguardSessionId = fakeInstanceId(10)!!
+        val currentUser = UserInfo(currentUserId, "test user", 0)
+    }
+}
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 b071a02..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(
@@ -275,15 +268,14 @@
         expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
     ) {
         assertThat(observed).isEqualTo(expected)
-        assertThat(underTest.getSelections())
+        assertThat(underTest.getCurrentSelections())
             .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
         expected.forEach { (slotId, configs) ->
-            assertThat(underTest.getSelections(slotId)).isEqualTo(configs)
+            assertThat(underTest.getCurrentSelections(slotId)).isEqualTo(configs)
         }
     }
 
     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 be712f6..0e6f8d4 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,12 +18,14 @@
 
 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
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.common.shared.model.Position
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.doze.DozeHost
 import com.android.systemui.doze.DozeMachine
 import com.android.systemui.doze.DozeTransitionCallback
@@ -38,25 +40,27 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
+import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
+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.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardRepositoryImplTest : SysuiTestCase() {
 
     @Mock private lateinit var statusBarStateController: StatusBarStateController
@@ -68,6 +72,7 @@
     @Mock private lateinit var authController: AuthController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
+    @Mock private lateinit var dozeParameters: DozeParameters
 
     private lateinit var underTest: KeyguardRepositoryImpl
 
@@ -84,6 +89,7 @@
                 keyguardStateController,
                 keyguardUpdateMonitor,
                 dozeTransitionListener,
+                dozeParameters,
                 authController,
                 dreamOverlayCallbackController,
             )
@@ -170,6 +176,26 @@
         }
 
     @Test
+    fun isAodAvailable() = runTest {
+        val flow = underTest.isAodAvailable
+        var isAodAvailable = collectLastValue(flow)
+        runCurrent()
+
+        val callback =
+            withArgCaptor<DozeParameters.Callback> { verify(dozeParameters).addCallback(capture()) }
+
+        whenever(dozeParameters.getAlwaysOn()).thenReturn(false)
+        callback.onAlwaysOnChange()
+        assertThat(isAodAvailable()).isEqualTo(false)
+
+        whenever(dozeParameters.getAlwaysOn()).thenReturn(true)
+        callback.onAlwaysOnChange()
+        assertThat(isAodAvailable()).isEqualTo(true)
+
+        flow.onCompletion { verify(dozeParameters).removeCallback(callback) }
+    }
+
+    @Test
     fun isKeyguardOccluded() =
         runTest(UnconfinedTestDispatcher()) {
             whenever(keyguardStateController.isOccluded).thenReturn(false)
@@ -193,6 +219,29 @@
         }
 
     @Test
+    fun isKeyguardUnlocked() =
+        runTest(UnconfinedTestDispatcher()) {
+            whenever(keyguardStateController.isUnlocked).thenReturn(false)
+            var latest: Boolean? = null
+            val job = underTest.isKeyguardUnlocked.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            val captor = argumentCaptor<KeyguardStateController.Callback>()
+            verify(keyguardStateController).addCallback(captor.capture())
+
+            whenever(keyguardStateController.isUnlocked).thenReturn(true)
+            captor.value.onUnlockedChanged()
+            assertThat(latest).isTrue()
+
+            whenever(keyguardStateController.isUnlocked).thenReturn(false)
+            captor.value.onUnlockedChanged()
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun isDozing() =
         runTest(UnconfinedTestDispatcher()) {
             var latest: Boolean? = null
@@ -298,29 +347,6 @@
         }
 
     @Test
-    fun isBouncerShowing() =
-        runTest(UnconfinedTestDispatcher()) {
-            whenever(keyguardStateController.isBouncerShowing).thenReturn(false)
-            var latest: Boolean? = null
-            val job = underTest.isBouncerShowing.onEach { latest = it }.launchIn(this)
-
-            assertThat(latest).isFalse()
-
-            val captor = argumentCaptor<KeyguardStateController.Callback>()
-            verify(keyguardStateController).addCallback(captor.capture())
-
-            whenever(keyguardStateController.isBouncerShowing).thenReturn(true)
-            captor.value.onBouncerShowingChanged()
-            assertThat(latest).isTrue()
-
-            whenever(keyguardStateController.isBouncerShowing).thenReturn(false)
-            captor.value.onBouncerShowingChanged()
-            assertThat(latest).isFalse()
-
-            job.cancel()
-        }
-
-    @Test
     fun isKeyguardGoingAway() =
         runTest(UnconfinedTestDispatcher()) {
             whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false)
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 5d2f0eb..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
@@ -104,7 +104,7 @@
             val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
             assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
 
-            val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.9))
+            val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.1))
             assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
 
             job.cancel()
@@ -168,6 +168,25 @@
         assertThat(wtfHandler.failed).isTrue()
     }
 
+    @Test
+    fun `Attempt to manually update transition after CANCELED state throws exception`() {
+        val uuid =
+            underTest.startTransition(
+                TransitionInfo(
+                    ownerName = OWNER_NAME,
+                    from = AOD,
+                    to = LOCKSCREEN,
+                    animator = null,
+                )
+            )
+
+        checkNotNull(uuid).let {
+            underTest.updateTransition(it, 0.2f, TransitionState.CANCELED)
+            underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+        }
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
     private fun listWithStep(
         step: BigDecimal,
         start: BigDecimal = BigDecimal.ZERO,
@@ -201,7 +220,10 @@
                 )
             )
         fractions.forEachIndexed { index, fraction ->
-            assertThat(steps[index + 1])
+            val step = steps[index + 1]
+            val truncatedValue =
+                BigDecimal(step.value.toDouble()).setScale(2, RoundingMode.HALF_UP).toFloat()
+            assertThat(step.copy(value = truncatedValue))
                 .isEqualTo(
                     TransitionStep(
                         from,
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
new file mode 100644
index 0000000..a181137
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.keyguard.data.repository
+
+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
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TrustRepositoryTest : SysuiTestCase() {
+    @Mock private lateinit var trustManager: TrustManager
+    @Captor private lateinit var listenerCaptor: ArgumentCaptor<TrustManager.TrustListener>
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var testScope: TestScope
+    private val users = listOf(UserInfo(1, "user 1", 0), UserInfo(2, "user 1", 0))
+
+    private lateinit var underTest: TrustRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testScope = TestScope()
+        userRepository = FakeUserRepository()
+        userRepository.setUserInfos(users)
+
+        val logger =
+            TrustRepositoryLogger(
+                LogBuffer("TestBuffer", 1, mock(LogcatEchoTracker::class.java), false)
+            )
+        underTest =
+            TrustRepositoryImpl(testScope.backgroundScope, userRepository, trustManager, logger)
+    }
+
+    @Test
+    fun isCurrentUserTrusted_whenTrustChanges_emitsLatestValue() =
+        testScope.runTest {
+            runCurrent()
+            verify(trustManager).registerTrustListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+
+            val currentUserId = users[0].id
+            userRepository.setSelectedUserInfo(users[0])
+            val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+
+            listener.onTrustChanged(true, false, currentUserId, 0, emptyList())
+            assertThat(isCurrentUserTrusted()).isTrue()
+
+            listener.onTrustChanged(false, false, currentUserId, 0, emptyList())
+
+            assertThat(isCurrentUserTrusted()).isFalse()
+        }
+
+    @Test
+    fun isCurrentUserTrusted_isFalse_byDefault() =
+        testScope.runTest {
+            runCurrent()
+
+            val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+
+            assertThat(isCurrentUserTrusted()).isFalse()
+        }
+
+    @Test
+    fun isCurrentUserTrusted_whenTrustChangesForDifferentUser_noop() =
+        testScope.runTest {
+            runCurrent()
+            verify(trustManager).registerTrustListener(listenerCaptor.capture())
+            userRepository.setSelectedUserInfo(users[0])
+            val listener = listenerCaptor.value
+
+            val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+            // current user is trusted.
+            listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+            // some other user is not trusted.
+            listener.onTrustChanged(false, false, users[1].id, 0, emptyList())
+
+            assertThat(isCurrentUserTrusted()).isTrue()
+        }
+
+    @Test
+    fun isCurrentUserTrusted_whenTrustChangesForCurrentUser_emitsNewValue() =
+        testScope.runTest {
+            runCurrent()
+            verify(trustManager).registerTrustListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+            userRepository.setSelectedUserInfo(users[0])
+
+            val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+            listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+            assertThat(isCurrentUserTrusted()).isTrue()
+
+            listener.onTrustChanged(false, true, users[0].id, 0, emptyList())
+            assertThat(isCurrentUserTrusted()).isFalse()
+        }
+
+    @Test
+    fun isCurrentUserTrusted_whenUserChangesWithoutRecentTrustChange_defaultsToFalse() =
+        testScope.runTest {
+            runCurrent()
+            verify(trustManager).registerTrustListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+            userRepository.setSelectedUserInfo(users[0])
+            listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+
+            val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+            userRepository.setSelectedUserInfo(users[1])
+
+            assertThat(isCurrentUserTrusted()).isFalse()
+        }
+
+    @Test
+    fun isCurrentUserTrusted_trustChangesFirstBeforeUserInfoChanges_emitsCorrectValue() =
+        testScope.runTest {
+            runCurrent()
+            verify(trustManager).registerTrustListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+            val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+
+            listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+            assertThat(isCurrentUserTrusted()).isFalse()
+
+            userRepository.setSelectedUserInfo(users[0])
+
+            assertThat(isCurrentUserTrusted()).isTrue()
+        }
+}
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
new file mode 100644
index 0000000..1365132
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.ViewMediatorCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AlternateBouncerInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: AlternateBouncerInteractor
+    private lateinit var bouncerRepository: KeyguardBouncerRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+    private lateinit var deviceEntryFingerprintAuthRepository:
+        FakeDeviceEntryFingerprintAuthRepository
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var systemClock: SystemClock
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var bouncerLogger: TableLogBuffer
+    private lateinit var featureFlags: FakeFeatureFlags
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        bouncerRepository =
+            KeyguardBouncerRepositoryImpl(
+                mock(ViewMediatorCallback::class.java),
+                FakeSystemClock(),
+                TestCoroutineScope(),
+                bouncerLogger,
+            )
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+        deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+        featureFlags = FakeFeatureFlags().apply { this.set(Flags.MODERN_ALTERNATE_BOUNCER, true) }
+        underTest =
+            AlternateBouncerInteractor(
+                keyguardStateController,
+                bouncerRepository,
+                biometricSettingsRepository,
+                deviceEntryFingerprintAuthRepository,
+                systemClock,
+                keyguardUpdateMonitor,
+                featureFlags,
+            )
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_givenCanShow() {
+        givenCanShowAlternateBouncer()
+        assertTrue(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_alternateBouncerUIUnavailable() {
+        givenCanShowAlternateBouncer()
+        bouncerRepository.setAlternateBouncerUIAvailable(false)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() {
+        givenCanShowAlternateBouncer()
+        biometricSettingsRepository.setFingerprintEnrolled(false)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() {
+        givenCanShowAlternateBouncer()
+        biometricSettingsRepository.setStrongBiometricAllowed(false)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() {
+        givenCanShowAlternateBouncer()
+        biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(false)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_fingerprintLockedOut() {
+        givenCanShowAlternateBouncer()
+        deviceEntryFingerprintAuthRepository.setLockedOut(true)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun show_whenCanShow() {
+        givenCanShowAlternateBouncer()
+
+        assertTrue(underTest.show())
+        assertTrue(bouncerRepository.alternateBouncerVisible.value)
+    }
+
+    @Test
+    fun canShowAlternateBouncerForFingerprint_butCanDismissLockScreen() {
+        givenCanShowAlternateBouncer()
+        whenever(keyguardStateController.isUnlocked).thenReturn(true)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
+    fun show_whenCannotShow() {
+        givenCannotShowAlternateBouncer()
+
+        assertFalse(underTest.show())
+        assertFalse(bouncerRepository.alternateBouncerVisible.value)
+    }
+
+    @Test
+    fun hide_wasPreviouslyShowing() {
+        bouncerRepository.setAlternateVisible(true)
+
+        assertTrue(underTest.hide())
+        assertFalse(bouncerRepository.alternateBouncerVisible.value)
+    }
+
+    @Test
+    fun hide_wasNotPreviouslyShowing() {
+        bouncerRepository.setAlternateVisible(false)
+
+        assertFalse(underTest.hide())
+        assertFalse(bouncerRepository.alternateBouncerVisible.value)
+    }
+
+    private fun givenCanShowAlternateBouncer() {
+        bouncerRepository.setAlternateBouncerUIAvailable(true)
+        biometricSettingsRepository.setFingerprintEnrolled(true)
+        biometricSettingsRepository.setStrongBiometricAllowed(true)
+        biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
+        deviceEntryFingerprintAuthRepository.setLockedOut(false)
+        whenever(keyguardStateController.isUnlocked).thenReturn(false)
+    }
+
+    private fun givenCannotShowAlternateBouncer() {
+        biometricSettingsRepository.setFingerprintEnrolled(false)
+    }
+}
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
new file mode 100644
index 0000000..153439e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.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.FakeKeyguardBouncerRepository
+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.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.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardInteractorTest : SysuiTestCase() {
+    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
+    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+
+    @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()
+        bouncerRepository = FakeKeyguardBouncerRepository()
+        underTest =
+            KeyguardInteractor(
+                repository,
+                commandQueue,
+                featureFlags,
+                bouncerRepository,
+            )
+    }
+
+    @Test
+    fun onCameraLaunchDetected() =
+        testScope.runTest {
+            val flow = underTest.onCameraLaunchDetected
+            var cameraLaunchSource = collectLastValue(flow)
+            runCurrent()
+
+            commandQueue.doForEachCallback {
+                it.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)
+
+            commandQueue.doForEachCallback {
+                it.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)
+
+            flow.onCompletion { assertThat(commandQueue.callbackCount()).isEqualTo(0) }
+        }
+
+    @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()
+
+            bouncerRepository.setPrimaryVisible(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
new file mode 100644
index 0000000..51988ef
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.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
+import com.android.systemui.coroutines.collectLastValue
+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.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+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.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardLongPressInteractorTest : SysuiTestCase() {
+
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var logger: UiEventLogger
+
+    private lateinit var underTest: KeyguardLongPressInteractor
+
+    private lateinit var testScope: TestScope
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        runBlocking { createUnderTest() }
+    }
+
+    @Test
+    fun isEnabled() =
+        testScope.runTest {
+            val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
+            KeyguardState.values().forEach { keyguardState ->
+                setUpState(
+                    keyguardState = keyguardState,
+                )
+
+                if (keyguardState == KeyguardState.LOCKSCREEN) {
+                    assertThat(isEnabled()).isTrue()
+                } else {
+                    assertThat(isEnabled()).isFalse()
+                }
+            }
+        }
+
+    @Test
+    fun `isEnabled - always false when quick settings are visible`() =
+        testScope.runTest {
+            val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
+            KeyguardState.values().forEach { keyguardState ->
+                setUpState(
+                    keyguardState = keyguardState,
+                    isQuickSettingsVisible = true,
+                )
+
+                assertThat(isEnabled()).isFalse()
+            }
+        }
+
+    @Test
+    fun `long-pressed - pop-up clicked - starts activity`() =
+        testScope.runTest {
+            val menu = collectLastValue(underTest.menu)
+            runCurrent()
+
+            val x = 100
+            val y = 123
+            underTest.onLongPress(x, y)
+            assertThat(menu()).isNotNull()
+            assertThat(menu()?.position?.x).isEqualTo(x)
+            assertThat(menu()?.position?.y).isEqualTo(y)
+
+            menu()?.onClicked?.invoke()
+
+            assertThat(menu()).isNull()
+            verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean())
+        }
+
+    @Test
+    fun `long-pressed - pop-up dismissed - never starts activity`() =
+        testScope.runTest {
+            val menu = collectLastValue(underTest.menu)
+            runCurrent()
+
+            menu()?.onDismissed?.invoke()
+
+            assertThat(menu()).isNull()
+            verify(activityStarter, never()).dismissKeyguardThenExecute(any(), any(), anyBoolean())
+        }
+
+    @Suppress("DEPRECATION") // We're okay using ACTION_CLOSE_SYSTEM_DIALOGS on system UI.
+    @Test
+    fun `long pressed - close dialogs broadcast received - popup dismissed`() =
+        testScope.runTest {
+            val menu = collectLastValue(underTest.menu)
+            runCurrent()
+
+            underTest.onLongPress(123, 456)
+            assertThat(menu()).isNotNull()
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver ->
+                broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+            }
+
+            assertThat(menu()).isNull()
+        }
+
+    @Test
+    fun `logs when menu is shown`() =
+        testScope.runTest {
+            collectLastValue(underTest.menu)
+            runCurrent()
+
+            underTest.onLongPress(100, 123)
+
+            verify(logger)
+                .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
+        }
+
+    @Test
+    fun `logs when menu is clicked`() =
+        testScope.runTest {
+            val menu = collectLastValue(underTest.menu)
+            runCurrent()
+
+            underTest.onLongPress(100, 123)
+            menu()?.onClicked?.invoke()
+
+            verify(logger)
+                .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
+        }
+
+    private suspend fun createUnderTest(
+        isLongPressFeatureEnabled: Boolean = true,
+        isRevampedWppFeatureEnabled: Boolean = true,
+    ) {
+        testScope = TestScope()
+        keyguardRepository = FakeKeyguardRepository()
+        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+
+        underTest =
+            KeyguardLongPressInteractor(
+                unsafeContext = context,
+                scope = testScope.backgroundScope,
+                transitionInteractor =
+                    KeyguardTransitionInteractor(
+                        repository = keyguardTransitionRepository,
+                    ),
+                repository = keyguardRepository,
+                activityStarter = activityStarter,
+                logger = logger,
+                featureFlags =
+                    FakeFeatureFlags().apply {
+                        set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled)
+                        set(Flags.REVAMPED_WALLPAPER_UI, isRevampedWppFeatureEnabled)
+                    },
+                broadcastDispatcher = fakeBroadcastDispatcher,
+            )
+        setUpState()
+    }
+
+    private suspend fun setUpState(
+        keyguardState: KeyguardState = KeyguardState.LOCKSCREEN,
+        isQuickSettingsVisible: Boolean = false,
+    ) {
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                to = keyguardState,
+            ),
+        )
+        keyguardRepository.setQuickSettingsVisible(isVisible = isQuickSettingsVisible)
+    }
+}
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 14b7c31..84ec125 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
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.app.admin.DevicePolicyManager
 import android.content.Intent
 import android.os.UserHandle
 import androidx.test.filters.SmallTest
@@ -36,6 +37,7 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
@@ -44,6 +46,7 @@
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
@@ -52,7 +55,10 @@
 import com.android.systemui.util.settings.FakeSettings
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
@@ -68,6 +74,7 @@
 import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(Parameterized::class)
 class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
@@ -216,8 +223,11 @@
     @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
     @Mock private lateinit var expandable: Expandable
     @Mock private lateinit var launchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     private lateinit var underTest: KeyguardQuickAffordanceInteractor
+    private lateinit var testScope: TestScope
 
     @JvmField @Parameter(0) var needStrongAuthAfterBoot: Boolean = false
     @JvmField @Parameter(1) var canShowWhileLocked: Boolean = false
@@ -284,9 +294,22 @@
                 dumpManager = mock(),
                 userHandle = UserHandle.SYSTEM,
             )
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         underTest =
             KeyguardQuickAffordanceInteractor(
-                keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
+                keyguardInteractor =
+                    KeyguardInteractor(
+                        repository = FakeKeyguardRepository(),
+                        commandQueue = commandQueue,
+                        featureFlags = featureFlags,
+                        bouncerRepository = FakeKeyguardBouncerRepository(),
+                    ),
                 registry =
                     FakeKeyguardQuickAffordanceRegistry(
                         mapOf(
@@ -305,64 +328,64 @@
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
-                featureFlags =
-                    FakeFeatureFlags().apply {
-                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
-                    },
+                featureFlags = featureFlags,
                 repository = { quickAffordanceRepository },
                 launchAnimator = launchAnimator,
+                devicePolicyManager = devicePolicyManager,
+                backgroundDispatcher = testDispatcher,
             )
     }
 
     @Test
-    fun onQuickAffordanceTriggered() = runBlockingTest {
-        setUpMocks(
-            needStrongAuthAfterBoot = needStrongAuthAfterBoot,
-            keyguardIsUnlocked = keyguardIsUnlocked,
-        )
+    fun onQuickAffordanceTriggered() =
+        testScope.runTest {
+            setUpMocks(
+                needStrongAuthAfterBoot = needStrongAuthAfterBoot,
+                keyguardIsUnlocked = keyguardIsUnlocked,
+            )
 
-        homeControls.setState(
-            lockScreenState =
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = DRAWABLE,
-                )
-        )
-        homeControls.onTriggeredResult =
+            homeControls.setState(
+                lockScreenState =
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        icon = DRAWABLE,
+                    )
+            )
+            homeControls.onTriggeredResult =
+                if (startActivity) {
+                    KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
+                        intent = INTENT,
+                        canShowWhileLocked = canShowWhileLocked,
+                    )
+                } else {
+                    KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                }
+
+            underTest.onQuickAffordanceTriggered(
+                configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
+                expandable = expandable,
+            )
+
             if (startActivity) {
-                KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
-                    intent = INTENT,
-                    canShowWhileLocked = canShowWhileLocked,
-                )
+                if (needsToUnlockFirst) {
+                    verify(activityStarter)
+                        .postStartActivityDismissingKeyguard(
+                            any(),
+                            /* delay= */ eq(0),
+                            same(animationController),
+                        )
+                } else {
+                    verify(activityStarter)
+                        .startActivity(
+                            any(),
+                            /* dismissShade= */ eq(true),
+                            same(animationController),
+                            /* showOverLockscreenWhenLocked= */ eq(true),
+                        )
+                }
             } else {
-                KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                verifyZeroInteractions(activityStarter)
             }
-
-        underTest.onQuickAffordanceTriggered(
-            configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
-            expandable = expandable,
-        )
-
-        if (startActivity) {
-            if (needsToUnlockFirst) {
-                verify(activityStarter)
-                    .postStartActivityDismissingKeyguard(
-                        any(),
-                        /* delay= */ eq(0),
-                        same(animationController),
-                    )
-            } else {
-                verify(activityStarter)
-                    .startActivity(
-                        any(),
-                        /* dismissShade= */ eq(true),
-                        same(animationController),
-                        /* showOverLockscreenWhenLocked= */ eq(true),
-                    )
-            }
-        } else {
-            verifyZeroInteractions(activityStarter)
         }
-    }
 
     private fun setUpMocks(
         needStrongAuthAfterBoot: Boolean = true,
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 972919a..62c9e5f 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
@@ -17,7 +17,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.app.admin.DevicePolicyManager
 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
@@ -35,6 +37,7 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
@@ -46,6 +49,7 @@
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.mock
@@ -59,7 +63,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
@@ -67,7 +70,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -75,6 +78,8 @@
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var launchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     private lateinit var underTest: KeyguardQuickAffordanceInteractor
 
@@ -148,11 +153,18 @@
         featureFlags =
             FakeFeatureFlags().apply {
                 set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
             }
 
         underTest =
             KeyguardQuickAffordanceInteractor(
-                keyguardInteractor = KeyguardInteractor(repository = repository),
+                keyguardInteractor =
+                    KeyguardInteractor(
+                        repository = repository,
+                        commandQueue = commandQueue,
+                        featureFlags = featureFlags,
+                        bouncerRepository = FakeKeyguardBouncerRepository(),
+                    ),
                 registry =
                     FakeKeyguardQuickAffordanceRegistry(
                         mapOf(
@@ -174,6 +186,8 @@
                 featureFlags = featureFlags,
                 repository = { quickAffordanceRepository },
                 launchAnimator = launchAnimator,
+                devicePolicyManager = devicePolicyManager,
+                backgroundDispatcher = testDispatcher,
             )
     }
 
@@ -229,6 +243,44 @@
         }
 
     @Test
+    fun `quickAffordance - hidden when all features are disabled by device policy`() =
+        testScope.runTest {
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
+                .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL)
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = ICON,
+                )
+            )
+
+            val collectedValue by
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                )
+
+            assertThat(collectedValue).isInstanceOf(KeyguardQuickAffordanceModel.Hidden::class.java)
+        }
+
+    @Test
+    fun `quickAffordance - hidden when shortcuts feature is disabled by device policy`() =
+        testScope.runTest {
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
+                .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL)
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = ICON,
+                )
+            )
+
+            val collectedValue by
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                )
+
+            assertThat(collectedValue).isInstanceOf(KeyguardQuickAffordanceModel.Hidden::class.java)
+        }
+
+    @Test
     fun `quickAffordance - bottom start affordance hidden while dozing`() =
         testScope.runTest {
             repository.setDozing(true)
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 d2b7838..fe9098f 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
@@ -17,12 +17,20 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.animation.ValueAnimator
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -33,16 +41,21 @@
 import com.android.systemui.keyguard.util.KeyguardTransitionRunner
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
 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.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -54,10 +67,12 @@
  */
 @SmallTest
 @RunWith(JUnit4::class)
+@FlakyTest(bugId = 265303901)
 class KeyguardTransitionScenariosTest : SysuiTestCase() {
     private lateinit var testScope: TestScope
 
     private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
     private lateinit var shadeRepository: ShadeRepository
 
     // Used to issue real transition steps for test input
@@ -66,9 +81,19 @@
 
     // Used to verify transition requests for test output
     @Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
 
     private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
     private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
+    private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor
+    private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor
+    private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor
+    private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor
+    private lateinit var fromAlternateBouncerTransitionInteractor:
+        FromAlternateBouncerTransitionInteractor
+    private lateinit var fromPrimaryBouncerTransitionInteractor:
+        FromPrimaryBouncerTransitionInteractor
 
     @Before
     fun setUp() {
@@ -76,16 +101,20 @@
         testScope = TestScope()
 
         keyguardRepository = FakeKeyguardRepository()
+        bouncerRepository = FakeKeyguardBouncerRepository()
         shadeRepository = FakeShadeRepository()
 
         /* Used to issue full transition steps, to better simulate a real device */
         transitionRepository = KeyguardTransitionRepositoryImpl()
         runner = KeyguardTransitionRunner(transitionRepository)
 
+        whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
+
+        val featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository),
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
                 shadeRepository = shadeRepository,
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
@@ -95,19 +124,74 @@
         fromDreamingTransitionInteractor =
             FromDreamingTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository),
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
             )
         fromDreamingTransitionInteractor.start()
+
+        fromAodTransitionInteractor =
+            FromAodTransitionInteractor(
+                scope = testScope,
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
+                keyguardTransitionRepository = mockTransitionRepository,
+                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+            )
+        fromAodTransitionInteractor.start()
+
+        fromGoneTransitionInteractor =
+            FromGoneTransitionInteractor(
+                scope = testScope,
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
+                keyguardTransitionRepository = mockTransitionRepository,
+                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+            )
+        fromGoneTransitionInteractor.start()
+
+        fromDozingTransitionInteractor =
+            FromDozingTransitionInteractor(
+                scope = testScope,
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
+                keyguardTransitionRepository = mockTransitionRepository,
+                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+            )
+        fromDozingTransitionInteractor.start()
+
+        fromOccludedTransitionInteractor =
+            FromOccludedTransitionInteractor(
+                scope = testScope,
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
+                keyguardTransitionRepository = mockTransitionRepository,
+                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+            )
+        fromOccludedTransitionInteractor.start()
+
+        fromAlternateBouncerTransitionInteractor =
+            FromAlternateBouncerTransitionInteractor(
+                scope = testScope,
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
+                keyguardTransitionRepository = mockTransitionRepository,
+                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+            )
+        fromAlternateBouncerTransitionInteractor.start()
+
+        fromPrimaryBouncerTransitionInteractor =
+            FromPrimaryBouncerTransitionInteractor(
+                scope = testScope,
+                keyguardInteractor = createKeyguardInteractor(featureFlags),
+                keyguardTransitionRepository = mockTransitionRepository,
+                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+                keyguardSecurityModel = keyguardSecurityModel,
+            )
+        fromPrimaryBouncerTransitionInteractor.start()
     }
 
     @Test
     fun `DREAMING to LOCKSCREEN`() =
         testScope.runTest {
-            // GIVEN a device is dreaming and occluded
+            // GIVEN a device is dreaming
             keyguardRepository.setDreamingWithOverlay(true)
-            keyguardRepository.setKeyguardOccluded(true)
+            keyguardRepository.setWakefulnessModel(startingToWake())
             runCurrent()
 
             // GIVEN a prior transition has run to DREAMING
@@ -133,13 +217,14 @@
             )
             // AND dreaming has stopped
             keyguardRepository.setDreamingWithOverlay(false)
-            // AND occluded has stopped
+            advanceUntilIdle()
+            // AND then occluded has stopped
             keyguardRepository.setKeyguardOccluded(false)
-            runCurrent()
+            advanceUntilIdle()
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture())
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to BOUNCER should occur
             assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
@@ -151,7 +236,7 @@
         }
 
     @Test
-    fun `LOCKSCREEN to BOUNCER via bouncer showing call`() =
+    fun `LOCKSCREEN to PRIMARY_BOUNCER via bouncer showing call`() =
         testScope.runTest {
             // GIVEN a device that has at least woken up
             keyguardRepository.setWakefulnessModel(startingToWake())
@@ -173,18 +258,710 @@
             )
             runCurrent()
 
-            // WHEN the bouncer is set to show
-            keyguardRepository.setBouncerShowing(true)
+            // WHEN the primary bouncer is set to show
+            bouncerRepository.setPrimaryVisible(true)
             runCurrent()
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture())
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
                 }
-            // THEN a transition to BOUNCER should occur
+            // THEN a transition to PRIMARY_BOUNCER should occur
             assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
             assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
-            assertThat(info.to).isEqualTo(KeyguardState.BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `OCCLUDED to DOZING`() =
+        testScope.runTest {
+            // GIVEN a device with AOD not available
+            keyguardRepository.setAodAvailable(false)
+            runCurrent()
+
+            // GIVEN a prior transition has run to OCCLUDED
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to sleep
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
+            assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `OCCLUDED to AOD`() =
+        testScope.runTest {
+            // GIVEN a device with AOD available
+            keyguardRepository.setAodAvailable(true)
+            runCurrent()
+
+            // GIVEN a prior transition has run to OCCLUDED
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to sleep
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
+            assertThat(info.to).isEqualTo(KeyguardState.AOD)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `LOCKSCREEN to DOZING`() =
+        testScope.runTest {
+            // GIVEN a device with AOD not available
+            keyguardRepository.setAodAvailable(false)
+            runCurrent()
+
+            // GIVEN a prior transition has run to LOCKSCREEN
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to sleep
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `LOCKSCREEN to AOD`() =
+        testScope.runTest {
+            // GIVEN a device with AOD available
+            keyguardRepository.setAodAvailable(true)
+            runCurrent()
+
+            // GIVEN a prior transition has run to LOCKSCREEN
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to sleep
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.to).isEqualTo(KeyguardState.AOD)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `DOZING to LOCKSCREEN`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to DOZING
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DOZING,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to wake
+            keyguardRepository.setWakefulnessModel(startingToWake())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `DOZING to GONE`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to DOZING
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DOZING,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN biometrics succeeds with wake and unlock mode
+            keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.to).isEqualTo(KeyguardState.GONE)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `GONE to DOZING`() =
+        testScope.runTest {
+            // GIVEN a device with AOD not available
+            keyguardRepository.setAodAvailable(false)
+            runCurrent()
+
+            // GIVEN a prior transition has run to GONE
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to sleep
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.GONE)
+            assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `GONE to AOD`() =
+        testScope.runTest {
+            // GIVEN a device with AOD available
+            keyguardRepository.setAodAvailable(true)
+            runCurrent()
+
+            // GIVEN a prior transition has run to GONE
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to sleep
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to AOD should occur
+            assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.GONE)
+            assertThat(info.to).isEqualTo(KeyguardState.AOD)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `GONE to LOCKSREEN`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to GONE
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the keyguard starts to show
+            keyguardRepository.setKeyguardShowing(true)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to AOD should occur
+            assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.GONE)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `GONE to DREAMING`() =
+        testScope.runTest {
+            // GIVEN a device that is not dreaming or dozing
+            keyguardRepository.setDreamingWithOverlay(false)
+            keyguardRepository.setWakefulnessModel(startingToWake())
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            runCurrent()
+
+            // GIVEN a prior transition has run to GONE
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to dream
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DREAMING should occur
+            assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.GONE)
+            assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `ALTERNATE_BOUNCER to PRIMARY_BOUNCER`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN the alternateBouncer stops showing and then the primary bouncer shows
+            bouncerRepository.setPrimaryVisible(true)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to PRIMARY_BOUNCER should occur
+            assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `ALTERNATE_BOUNCER to AOD`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+            bouncerRepository.setAlternateVisible(true)
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // GIVEN the primary bouncer isn't showing, aod available and starting to sleep
+            bouncerRepository.setPrimaryVisible(false)
+            keyguardRepository.setAodAvailable(true)
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+
+            // WHEN the alternateBouncer stops showing
+            bouncerRepository.setAlternateVisible(false)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to AOD should occur
+            assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.AOD)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `ALTERNATE_BOUNCER to DOZING`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+            bouncerRepository.setAlternateVisible(true)
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // GIVEN the primary bouncer isn't showing, aod not available and starting to sleep
+            // to sleep
+            bouncerRepository.setPrimaryVisible(false)
+            keyguardRepository.setAodAvailable(false)
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+
+            // WHEN the alternateBouncer stops showing
+            bouncerRepository.setAlternateVisible(false)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `ALTERNATE_BOUNCER to LOCKSCREEN`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+            bouncerRepository.setAlternateVisible(true)
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // GIVEN the primary bouncer isn't showing and device not sleeping
+            bouncerRepository.setPrimaryVisible(false)
+            keyguardRepository.setWakefulnessModel(startingToWake())
+
+            // WHEN the alternateBouncer stops showing
+            bouncerRepository.setAlternateVisible(false)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to LOCKSCREEN should occur
+            assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `PRIMARY_BOUNCER to AOD`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to PRIMARY_BOUNCER
+            bouncerRepository.setPrimaryVisible(true)
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // GIVEN aod available and starting to sleep
+            keyguardRepository.setAodAvailable(true)
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+
+            // WHEN the primaryBouncer stops showing
+            bouncerRepository.setPrimaryVisible(false)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to AOD should occur
+            assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.AOD)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `PRIMARY_BOUNCER to DOZING`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to PRIMARY_BOUNCER
+            bouncerRepository.setPrimaryVisible(true)
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // GIVEN aod not available and starting to sleep to sleep
+            keyguardRepository.setAodAvailable(false)
+            keyguardRepository.setWakefulnessModel(startingToSleep())
+
+            // WHEN the primaryBouncer stops showing
+            bouncerRepository.setPrimaryVisible(false)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `PRIMARY_BOUNCER to LOCKSCREEN`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to PRIMARY_BOUNCER
+            bouncerRepository.setPrimaryVisible(true)
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // GIVEN device not sleeping
+            keyguardRepository.setWakefulnessModel(startingToWake())
+
+            // WHEN the alternateBouncer stops showing
+            bouncerRepository.setPrimaryVisible(false)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                }
+            // THEN a transition to LOCKSCREEN should occur
+            assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
             assertThat(info.animator).isNotNull()
 
             coroutineContext.cancelChildren()
@@ -197,4 +974,21 @@
             WakeSleepReason.OTHER,
             WakeSleepReason.OTHER
         )
+
+    private fun startingToSleep() =
+        WakefulnessModel(
+            WakefulnessState.STARTING_TO_SLEEP,
+            true,
+            WakeSleepReason.OTHER,
+            WakeSleepReason.OTHER
+        )
+
+    private fun createKeyguardInteractor(featureFlags: FeatureFlags): KeyguardInteractor {
+        return KeyguardInteractor(
+            keyguardRepository,
+            commandQueue,
+            featureFlags,
+            bouncerRepository,
+        )
+    }
 }
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 db9c4e7..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,25 +17,26 @@
 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 com.android.systemui.statusbar.phone.KeyguardBouncer
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
     private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
     @Mock
     private lateinit var mPrimaryBouncerExpansionCallback:
-        KeyguardBouncer.PrimaryBouncerExpansionCallback
-    @Mock private lateinit var keyguardResetCallback: KeyguardBouncer.KeyguardResetCallback
+        PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
+    @Mock
+    private lateinit var keyguardResetCallback:
+        PrimaryBouncerCallbackInteractor.KeyguardResetCallback
 
     @Before
     fun setup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
index a6fc13b..6b7fd61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -19,22 +19,24 @@
 import android.os.Looper
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.testing.TestableResources
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.DejankUtils
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.BouncerView
 import com.android.systemui.keyguard.data.BouncerViewDelegate
 import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
 import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.any
@@ -68,13 +70,14 @@
     @Mock private lateinit var keyguardBypassController: KeyguardBypassController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     private val mainHandler = FakeHandler(Looper.getMainLooper())
-    private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
+    private lateinit var underTest: PrimaryBouncerInteractor
+    private lateinit var resources: TestableResources
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         DejankUtils.setImmediate(true)
-        mPrimaryBouncerInteractor =
+        underTest =
             PrimaryBouncerInteractor(
                 repository,
                 bouncerView,
@@ -84,25 +87,26 @@
                 mPrimaryBouncerCallbackInteractor,
                 falsingCollector,
                 dismissCallbackRegistry,
-                keyguardBypassController,
+                context,
                 keyguardUpdateMonitor,
+                keyguardBypassController,
             )
         `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
         `when`(repository.primaryBouncerShow.value).thenReturn(null)
         `when`(bouncerView.delegate).thenReturn(bouncerViewDelegate)
+        resources = context.orCreateTestableResources
     }
 
     @Test
     fun testShow_isScrimmed() {
-        mPrimaryBouncerInteractor.show(true)
-        verify(repository).setOnScreenTurnedOff(false)
+        underTest.show(true)
         verify(repository).setKeyguardAuthenticated(null)
         verify(repository).setPrimaryHide(false)
         verify(repository).setPrimaryStartingToHide(false)
         verify(repository).setPrimaryScrimmed(true)
         verify(repository).setPanelExpansion(EXPANSION_VISIBLE)
         verify(repository).setPrimaryShowingSoon(true)
-        verify(keyguardStateController).notifyBouncerShowing(true)
+        verify(keyguardStateController).notifyPrimaryBouncerShowing(true)
         verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToShow()
         verify(repository).setPrimaryVisible(true)
         verify(repository).setPrimaryShow(any(KeyguardBouncerModel::class.java))
@@ -118,15 +122,15 @@
     @Test
     fun testShow_keyguardIsDone() {
         `when`(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
-        verify(keyguardStateController, never()).notifyBouncerShowing(true)
+        verify(keyguardStateController, never()).notifyPrimaryBouncerShowing(true)
         verify(mPrimaryBouncerCallbackInteractor, never()).dispatchStartingToShow()
     }
 
     @Test
     fun testHide() {
-        mPrimaryBouncerInteractor.hide()
+        underTest.hide()
         verify(falsingCollector).onBouncerHidden()
-        verify(keyguardStateController).notifyBouncerShowing(false)
+        verify(keyguardStateController).notifyPrimaryBouncerShowing(false)
         verify(repository).setPrimaryShowingSoon(false)
         verify(repository).setPrimaryVisible(false)
         verify(repository).setPrimaryHide(true)
@@ -137,7 +141,7 @@
     @Test
     fun testExpansion() {
         `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
-        mPrimaryBouncerInteractor.setPanelExpansion(0.6f)
+        underTest.setPanelExpansion(0.6f)
         verify(repository).setPanelExpansion(0.6f)
         verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
     }
@@ -146,7 +150,7 @@
     fun testExpansion_fullyShown() {
         `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
         `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_VISIBLE)
+        underTest.setPanelExpansion(EXPANSION_VISIBLE)
         verify(falsingCollector).onBouncerShown()
         verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
     }
@@ -155,7 +159,7 @@
     fun testExpansion_fullyHidden() {
         `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
         `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN)
+        underTest.setPanelExpansion(EXPANSION_HIDDEN)
         verify(repository).setPrimaryVisible(false)
         verify(repository).setPrimaryShow(null)
         verify(repository).setPrimaryHide(true)
@@ -167,7 +171,7 @@
     @Test
     fun testExpansion_startingToHide() {
         `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
-        mPrimaryBouncerInteractor.setPanelExpansion(0.1f)
+        underTest.setPanelExpansion(0.1f)
         verify(repository).setPrimaryStartingToHide(true)
         verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
     }
@@ -175,7 +179,7 @@
     @Test
     fun testShowMessage() {
         val argCaptor = ArgumentCaptor.forClass(BouncerShowMessageModel::class.java)
-        mPrimaryBouncerInteractor.showMessage("abc", null)
+        underTest.showMessage("abc", null)
         verify(repository).setShowMessage(argCaptor.capture())
         assertThat(argCaptor.value.message).isEqualTo("abc")
     }
@@ -184,62 +188,56 @@
     fun testDismissAction() {
         val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
         val cancelAction = mock(Runnable::class.java)
-        mPrimaryBouncerInteractor.setDismissAction(onDismissAction, cancelAction)
+        underTest.setDismissAction(onDismissAction, cancelAction)
         verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
     }
 
     @Test
     fun testUpdateResources() {
-        mPrimaryBouncerInteractor.updateResources()
+        underTest.updateResources()
         verify(repository).setResourceUpdateRequests(true)
     }
 
     @Test
     fun testNotifyKeyguardAuthenticated() {
-        mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(true)
+        underTest.notifyKeyguardAuthenticated(true)
         verify(repository).setKeyguardAuthenticated(true)
     }
 
     @Test
     fun testNotifyShowedMessage() {
-        mPrimaryBouncerInteractor.onMessageShown()
+        underTest.onMessageShown()
         verify(repository).setShowMessage(null)
     }
 
     @Test
-    fun testOnScreenTurnedOff() {
-        mPrimaryBouncerInteractor.onScreenTurnedOff()
-        verify(repository).setOnScreenTurnedOff(true)
-    }
-
-    @Test
     fun testSetKeyguardPosition() {
-        mPrimaryBouncerInteractor.setKeyguardPosition(0f)
+        underTest.setKeyguardPosition(0f)
         verify(repository).setKeyguardPosition(0f)
     }
 
     @Test
     fun testNotifyKeyguardAuthenticatedHandled() {
-        mPrimaryBouncerInteractor.notifyKeyguardAuthenticatedHandled()
+        underTest.notifyKeyguardAuthenticatedHandled()
         verify(repository).setKeyguardAuthenticated(null)
     }
 
     @Test
     fun testNotifyUpdatedResources() {
-        mPrimaryBouncerInteractor.notifyUpdatedResources()
+        underTest.notifyUpdatedResources()
         verify(repository).setResourceUpdateRequests(false)
     }
 
     @Test
     fun testSetBackButtonEnabled() {
-        mPrimaryBouncerInteractor.setBackButtonEnabled(true)
+        underTest.setBackButtonEnabled(true)
         verify(repository).setIsBackButtonEnabled(true)
     }
 
     @Test
     fun testStartDisappearAnimation() {
         val runnable = mock(Runnable::class.java)
-        mPrimaryBouncerInteractor.startDisappearAnimation(runnable)
+        underTest.startDisappearAnimation(runnable)
         verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
     }
 
@@ -248,42 +246,136 @@
         `when`(repository.primaryBouncerVisible.value).thenReturn(true)
         `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
         `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isTrue()
+        assertThat(underTest.isFullyShowing()).isTrue()
         `when`(repository.primaryBouncerVisible.value).thenReturn(false)
-        assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isFalse()
+        assertThat(underTest.isFullyShowing()).isFalse()
     }
 
     @Test
     fun testIsScrimmed() {
         `when`(repository.primaryBouncerScrimmed.value).thenReturn(true)
-        assertThat(mPrimaryBouncerInteractor.isScrimmed()).isTrue()
+        assertThat(underTest.isScrimmed()).isTrue()
         `when`(repository.primaryBouncerScrimmed.value).thenReturn(false)
-        assertThat(mPrimaryBouncerInteractor.isScrimmed()).isFalse()
+        assertThat(underTest.isScrimmed()).isFalse()
     }
 
     @Test
     fun testIsInTransit() {
         `when`(repository.primaryBouncerShowingSoon.value).thenReturn(true)
-        assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+        assertThat(underTest.isInTransit()).isTrue()
         `when`(repository.primaryBouncerShowingSoon.value).thenReturn(false)
-        assertThat(mPrimaryBouncerInteractor.isInTransit()).isFalse()
+        assertThat(underTest.isInTransit()).isFalse()
         `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
-        assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+        assertThat(underTest.isInTransit()).isTrue()
     }
 
     @Test
     fun testIsAnimatingAway() {
         `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
-        assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isTrue()
+        assertThat(underTest.isAnimatingAway()).isTrue()
         `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isFalse()
+        assertThat(underTest.isAnimatingAway()).isFalse()
     }
 
     @Test
     fun testWillDismissWithAction() {
         `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
-        assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isTrue()
+        assertThat(underTest.willDismissWithAction()).isTrue()
         `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
-        assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isFalse()
+        assertThat(underTest.willDismissWithAction()).isFalse()
+    }
+
+    @Test
+    fun testSideFpsVisibility() {
+        updateSideFpsVisibilityParameters(
+            isVisible = true,
+            sfpsEnabled = true,
+            fpsDetectionRunning = true,
+            isUnlockingWithFpAllowed = true,
+            isAnimatingAway = false
+        )
+        underTest.updateSideFpsVisibility()
+        verify(repository).setSideFpsShowing(true)
+    }
+
+    @Test
+    fun testSideFpsVisibility_notVisible() {
+        updateSideFpsVisibilityParameters(
+            isVisible = false,
+            sfpsEnabled = true,
+            fpsDetectionRunning = true,
+            isUnlockingWithFpAllowed = true,
+            isAnimatingAway = false
+        )
+        underTest.updateSideFpsVisibility()
+        verify(repository).setSideFpsShowing(false)
+    }
+
+    @Test
+    fun testSideFpsVisibility_sfpsNotEnabled() {
+        updateSideFpsVisibilityParameters(
+            isVisible = true,
+            sfpsEnabled = false,
+            fpsDetectionRunning = true,
+            isUnlockingWithFpAllowed = true,
+            isAnimatingAway = false
+        )
+        underTest.updateSideFpsVisibility()
+        verify(repository).setSideFpsShowing(false)
+    }
+
+    @Test
+    fun testSideFpsVisibility_fpsDetectionNotRunning() {
+        updateSideFpsVisibilityParameters(
+            isVisible = true,
+            sfpsEnabled = true,
+            fpsDetectionRunning = false,
+            isUnlockingWithFpAllowed = true,
+            isAnimatingAway = false
+        )
+        underTest.updateSideFpsVisibility()
+        verify(repository).setSideFpsShowing(false)
+    }
+
+    @Test
+    fun testSideFpsVisibility_UnlockingWithFpNotAllowed() {
+        updateSideFpsVisibilityParameters(
+            isVisible = true,
+            sfpsEnabled = true,
+            fpsDetectionRunning = true,
+            isUnlockingWithFpAllowed = false,
+            isAnimatingAway = false
+        )
+        underTest.updateSideFpsVisibility()
+        verify(repository).setSideFpsShowing(false)
+    }
+
+    @Test
+    fun testSideFpsVisibility_AnimatingAway() {
+        updateSideFpsVisibilityParameters(
+            isVisible = true,
+            sfpsEnabled = true,
+            fpsDetectionRunning = true,
+            isUnlockingWithFpAllowed = true,
+            isAnimatingAway = true
+        )
+        underTest.updateSideFpsVisibility()
+        verify(repository).setSideFpsShowing(false)
+    }
+
+    private fun updateSideFpsVisibilityParameters(
+        isVisible: Boolean,
+        sfpsEnabled: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+        isAnimatingAway: Boolean
+    ) {
+        `when`(repository.primaryBouncerVisible.value).thenReturn(isVisible)
+        resources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, sfpsEnabled)
+        `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(fpsDetectionRunning)
+        `when`(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value)
+            .thenReturn(if (isAnimatingAway) Runnable {} else null)
     }
 }
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
new file mode 100644
index 0000000..f675e79
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() {
+    private lateinit var repository: FakeKeyguardBouncerRepository
+    @Mock private lateinit var bouncerView: BouncerView
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+    @Mock private lateinit var primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private val mainHandler = FakeHandler(Looper.getMainLooper())
+    private lateinit var underTest: PrimaryBouncerInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        repository = FakeKeyguardBouncerRepository()
+        underTest =
+            PrimaryBouncerInteractor(
+                repository,
+                bouncerView,
+                mainHandler,
+                keyguardStateController,
+                keyguardSecurityModel,
+                primaryBouncerCallbackInteractor,
+                falsingCollector,
+                dismissCallbackRegistry,
+                context,
+                keyguardUpdateMonitor,
+                keyguardBypassController,
+            )
+    }
+
+    @Test
+    fun notInteractableWhenExpansionIsBelow90Percent() = runTest {
+        val isInteractable = collectLastValue(underTest.isInteractable)
+
+        repository.setPrimaryVisible(true)
+        repository.setPanelExpansion(0.15f)
+
+        assertThat(isInteractable()).isFalse()
+    }
+
+    @Test
+    fun notInteractableWhenExpansionAbove90PercentButNotVisible() = runTest {
+        val isInteractable = collectLastValue(underTest.isInteractable)
+
+        repository.setPrimaryVisible(false)
+        repository.setPanelExpansion(0.05f)
+
+        assertThat(isInteractable()).isFalse()
+    }
+
+    @Test
+    fun isInteractableWhenExpansionAbove90PercentAndVisible() = runTest {
+        var isInteractable = collectLastValue(underTest.isInteractable)
+
+        repository.setPrimaryVisible(true)
+        repository.setPanelExpansion(0.09f)
+
+        assertThat(isInteractable()).isTrue()
+
+        repository.setPanelExpansion(0.12f)
+        assertThat(isInteractable()).isFalse()
+
+        repository.setPanelExpansion(0f)
+        assertThat(isInteractable()).isTrue()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
new file mode 100644
index 0000000..a5b78b74
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.keyguard.ui
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+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.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.milliseconds
+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)
+class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
+    private lateinit var underTest: KeyguardTransitionAnimationFlow
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        underTest =
+            KeyguardTransitionAnimationFlow(
+                1000.milliseconds,
+                repository.transitions,
+            )
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun zeroDurationThrowsException() = runTest {
+        val flow = underTest.createFlow(duration = 0.milliseconds, onStep = { it })
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun startTimePlusDurationGreaterThanTransitionDurationThrowsException() = runTest {
+        val flow =
+            underTest.createFlow(
+                startTime = 300.milliseconds,
+                duration = 800.milliseconds,
+                onStep = { it }
+            )
+    }
+
+    @Test
+    fun onFinishRunsWhenSpecified() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 100.milliseconds,
+                onStep = { it },
+                onFinish = { 10f },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(animationValues()).isEqualTo(10f)
+    }
+
+    @Test
+    fun onCancelRunsWhenSpecified() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 100.milliseconds,
+                onStep = { it },
+                onCancel = { 100f },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
+        assertThat(animationValues()).isEqualTo(100f)
+    }
+
+    @Test
+    fun usesStartTime() = runTest {
+        val flow =
+            underTest.createFlow(
+                startTime = 500.milliseconds,
+                duration = 500.milliseconds,
+                onStep = { it },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(animationValues()).isEqualTo(0f)
+
+        // Should not emit a value
+        repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+
+        repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0f)
+        repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0.2f)
+        repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0.6f)
+        repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 1f)
+    }
+
+    @Test
+    fun usesInterpolator() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 1000.milliseconds,
+                interpolator = EMPHASIZED_ACCELERATE,
+                onStep = { it },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
+        repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.5f))
+        repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.6f))
+        repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.8f))
+        repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(1f))
+    }
+
+    @Test
+    fun usesOnStepToDoubleValue() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 1000.milliseconds,
+                onStep = { it * 2 },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertFloat(animationValues(), 0f)
+        repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0.6f)
+        repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 1.2f)
+        repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 1.6f)
+        repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 2f)
+    }
+
+    private fun assertFloat(actual: Float?, expected: Float) {
+        assertThat(actual!!).isWithin(0.01f).of(expected)
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING,
+            value = value,
+            transitionState = state,
+            ownerName = "GoneToDreamingTransitionViewModelTest"
+        )
+    }
+}
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 5571663..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,21 +16,16 @@
 
 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.animation.Interpolators.EMPHASIZED_ACCELERATE
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -39,13 +34,13 @@
 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
+    private lateinit var transitionAnimation: KeyguardTransitionAnimationFlow
 
     @Before
     fun setUp() {
@@ -63,32 +58,18 @@
             val job =
                 underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.6f))
+            // ...up to here
+            repository.sendTransitionStep(step(0.8f))
             repository.sendTransitionStep(step(1f))
 
-            // Only 3 values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
-                    ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
 
             job.cancel()
         }
@@ -100,16 +81,18 @@
 
             val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
 
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
             repository.sendTransitionStep(step(0.5f))
+            // ...up to here
             repository.sendTransitionStep(step(1f))
 
             // Only two values should be present, since the dream overlay runs for a small fraction
             // of the overall animation time
-            assertThat(values.size).isEqualTo(2)
-            assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
-            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -121,19 +104,15 @@
 
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
-            // Should start running here...
             repository.sendTransitionStep(step(0.2f))
             repository.sendTransitionStep(step(0.3f))
-            // ...up to here
             repository.sendTransitionStep(step(1f))
 
-            // Only two values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(2)
-            assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -147,58 +126,27 @@
             val job =
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
             repository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[3])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(1f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
 
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_LOCKSCREEN_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
-
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
-
-    private fun step(value: Float): TransitionStep {
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.DREAMING,
             to = KeyguardState.LOCKSCREEN,
             value = value,
-            transitionState = TransitionState.RUNNING,
+            transitionState = state,
             ownerName = "DreamingToLockscreenTransitionViewModelTest"
         )
     }
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
new file mode 100644
index 0000000..b15ce10
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.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
+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.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GoneToDreamingTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: GoneToDreamingTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor = KeyguardTransitionInteractor(repository)
+        underTest = GoneToDreamingTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun lockscreenFadeOut() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            // ...up to here
+            repository.sendTransitionStep(step(1f))
+
+            // Only three values should be present, since the dream overlay runs for a small
+            // fraction of the overall animation time
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.5f))
+            // And a final reset event on CANCEL
+            repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
+
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+            job.cancel()
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING,
+            value = value,
+            transitionState = state,
+            ownerName = "GoneToDreamingTransitionViewModelTest"
+        )
+    }
+}
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 a2c2f71..c727b3a 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.app.admin.DevicePolicyManager
 import android.content.Intent
 import android.os.UserHandle
 import androidx.test.filters.SmallTest
@@ -35,6 +36,7 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
@@ -47,6 +49,7 @@
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
@@ -84,6 +87,8 @@
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var launchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     private lateinit var underTest: KeyguardBottomAreaViewModel
 
@@ -123,9 +128,21 @@
                 ),
             )
         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)
+        val keyguardInteractor =
+            KeyguardInteractor(
+                repository = repository,
+                commandQueue = commandQueue,
+                featureFlags = featureFlags,
+                bouncerRepository = FakeKeyguardBouncerRepository(),
+            )
         whenever(userTracker.userHandle).thenReturn(mock())
+        whenever(userTracker.userId).thenReturn(10)
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
             .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
         val testDispatcher = StandardTestDispatcher()
@@ -188,12 +205,11 @@
                         keyguardStateController = keyguardStateController,
                         userTracker = userTracker,
                         activityStarter = activityStarter,
-                        featureFlags =
-                            FakeFeatureFlags().apply {
-                                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
-                            },
+                        featureFlags = featureFlags,
                         repository = { quickAffordanceRepository },
                         launchAnimator = launchAnimator,
+                        devicePolicyManager = devicePolicyManager,
+                        backgroundDispatcher = testDispatcher,
                     ),
                 bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
                 burnInHelperWrapper = burnInHelperWrapper,
@@ -229,9 +245,45 @@
         }
 
     @Test
+    fun `startButton - hidden when device policy disables all keyguard features`() =
+        testScope.runTest {
+            whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
+                .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL)
+            repository.setKeyguardShowing(true)
+            val latest by collectLastValue(underTest.startButton)
+
+            val testConfig =
+                TestConfig(
+                    isVisible = true,
+                    isClickable = true,
+                    isActivated = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest,
+                testConfig =
+                    TestConfig(
+                        isVisible = false,
+                    ),
+                configKey = configKey,
+            )
+        }
+
+    @Test
     fun `startButton - in preview mode - visible even when keyguard not showing`() =
         testScope.runTest {
-            underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
+            underTest.enablePreviewMode(
+                initiallySelectedSlotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                shouldHighlightSelectedAffordance = true,
+            )
             repository.setKeyguardShowing(false)
             val latest = collectLastValue(underTest.startButton)
 
@@ -256,14 +308,69 @@
                     TestConfig(
                         isVisible = true,
                         isClickable = false,
+                        isActivated = false,
+                        icon = icon,
+                        canShowWhileLocked = false,
+                        intent = Intent("action"),
+                        isSelected = true,
+                    ),
+                configKey = configKey,
+            )
+            assertThat(latest()?.isSelected).isTrue()
+        }
+
+    @Test
+    fun `endButton - in higlighted preview mode - dimmed when other is selected`() =
+        testScope.runTest {
+            underTest.enablePreviewMode(
+                initiallySelectedSlotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                shouldHighlightSelectedAffordance = true,
+            )
+            repository.setKeyguardShowing(false)
+            val startButton = collectLastValue(underTest.startButton)
+            val endButton = collectLastValue(underTest.endButton)
+
+            val icon: Icon = mock()
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig =
+                    TestConfig(
+                        isVisible = true,
+                        isClickable = true,
                         isActivated = true,
                         icon = icon,
                         canShowWhileLocked = false,
                         intent = Intent("action"),
                     ),
+            )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_END,
+                    testConfig =
+                        TestConfig(
+                            isVisible = true,
+                            isClickable = true,
+                            isActivated = true,
+                            icon = icon,
+                            canShowWhileLocked = false,
+                            intent = Intent("action"),
+                        ),
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = endButton(),
+                testConfig =
+                    TestConfig(
+                        isVisible = true,
+                        isClickable = false,
+                        isActivated = false,
+                        icon = icon,
+                        canShowWhileLocked = false,
+                        intent = Intent("action"),
+                        isDimmed = true,
+                    ),
                 configKey = configKey,
             )
-            assertThat(latest()?.isSelected).isTrue()
         }
 
     @Test
@@ -374,7 +481,10 @@
     @Test
     fun `alpha - in preview mode - does not change`() =
         testScope.runTest {
-            underTest.enablePreviewMode(null)
+            underTest.enablePreviewMode(
+                initiallySelectedSlotId = null,
+                shouldHighlightSelectedAffordance = false,
+            )
             val value = collectLastValue(underTest.alpha)
 
             assertThat(value()).isEqualTo(1f)
@@ -636,6 +746,8 @@
         assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
         assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
         assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated)
+        assertThat(viewModel.isSelected).isEqualTo(testConfig.isSelected)
+        assertThat(viewModel.isDimmed).isEqualTo(testConfig.isDimmed)
         if (testConfig.isVisible) {
             assertThat(viewModel.icon).isEqualTo(testConfig.icon)
             viewModel.onClicked.invoke(
@@ -661,6 +773,8 @@
         val icon: Icon? = null,
         val canShowWhileLocked: Boolean = false,
         val intent: Intent? = null,
+        val isSelected: Boolean = false,
+        val isDimmed: Boolean = false,
     ) {
         init {
             check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
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..65e4c10 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,53 +16,101 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+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
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.utils.os.FakeHandler
 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
+    lateinit var bouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var bouncerView: BouncerView
-    @Mock lateinit var bouncerInteractor: PrimaryBouncerInteractor
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+    @Mock private lateinit var primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private val mainHandler = FakeHandler(Looper.getMainLooper())
+    val repository = FakeKeyguardBouncerRepository()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        bouncerInteractor =
+            PrimaryBouncerInteractor(
+                repository,
+                bouncerView,
+                mainHandler,
+                keyguardStateController,
+                keyguardSecurityModel,
+                primaryBouncerCallbackInteractor,
+                falsingCollector,
+                dismissCallbackRegistry,
+                context,
+                keyguardUpdateMonitor,
+                keyguardBypassController,
+            )
         underTest = KeyguardBouncerViewModel(bouncerView, bouncerInteractor)
     }
 
     @Test
-    fun setMessage() =
-        runBlocking(Dispatchers.Main.immediate) {
-            val flow = MutableStateFlow<BouncerShowMessageModel?>(null)
-            var message: BouncerShowMessageModel? = null
-            Mockito.`when`(bouncerInteractor.showMessage)
-                .thenReturn(flow as Flow<BouncerShowMessageModel>)
-            // Reinitialize the view model.
-            underTest = KeyguardBouncerViewModel(bouncerView, bouncerInteractor)
+    fun setMessage() = runTest {
+        var message: BouncerShowMessageModel? = null
+        val job = underTest.bouncerShowMessage.onEach { message = it }.launchIn(this)
 
-            flow.value = BouncerShowMessageModel(message = "abc", colorStateList = null)
+        repository.setShowMessage(BouncerShowMessageModel("abc", null))
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(message?.message).isEqualTo("abc")
+        job.cancel()
+    }
 
-            val job = underTest.bouncerShowMessage.onEach { message = it }.launchIn(this)
-            assertThat(message?.message).isEqualTo("abc")
-            job.cancel()
-        }
+    @Test
+    fun shouldUpdateSideFps() = runTest {
+        var count = 0
+        val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
+        repository.setPrimaryVisible(true)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(count).isEqualTo(1)
+        job.cancel()
+    }
+
+    @Test
+    fun sideFpsShowing() = runTest {
+        var sideFpsIsShowing = false
+        val job = underTest.sideFpsShowing.onEach { sideFpsIsShowing = it }.launchIn(this)
+        repository.setSideFpsShowing(true)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(sideFpsIsShowing).isEqualTo(true)
+        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
new file mode 100644
index 0000000..d94c108
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.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
+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.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: LockscreenToDreamingTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor = KeyguardTransitionInteractor(repository)
+        underTest = LockscreenToDreamingTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun lockscreenFadeOut() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            // ...up to here
+            repository.sendTransitionStep(step(1f))
+
+            // Only three values should be present, since the dream overlay runs for a small
+            // fraction of the overall animation time
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(1f))
+            // And a final reset event on FINISHED
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+
+            assertThat(values.size).isEqualTo(6)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+            // Validate finished value
+            assertThat(values[5]).isEqualTo(0f)
+
+            job.cancel()
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DREAMING,
+            value = value,
+            transitionState = state,
+            ownerName = "LockscreenToDreamingTransitionViewModelTest"
+        )
+    }
+}
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
new file mode 100644
index 0000000..12ec24d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.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
+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.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: LockscreenToOccludedTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor = KeyguardTransitionInteractor(repository)
+        underTest = LockscreenToOccludedTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun lockscreenFadeOut() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.4f))
+            repository.sendTransitionStep(step(0.7f))
+            // ...up to here
+            repository.sendTransitionStep(step(1f))
+
+            // Only 3 values should be present, since the dream overlay runs for a small fraction
+            // of the overall animation time
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(1f))
+            // ...up to here
+
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationYIsCanceled() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED))
+
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+            // Cancel will reset the translation
+            assertThat(values[3]).isEqualTo(0)
+
+            job.cancel()
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING,
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.OCCLUDED,
+            value = value,
+            transitionState = state,
+            ownerName = "LockscreenToOccludedTransitionViewModelTest"
+        )
+    }
+}
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 98d292d..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,18 +16,15 @@
 
 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.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -36,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
@@ -58,21 +54,19 @@
 
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
-            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.1f))
             // Should start running here...
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.4f))
             repository.sendTransitionStep(step(0.5f))
-            // ...up to here
             repository.sendTransitionStep(step(0.6f))
+            // ...up to here
+            repository.sendTransitionStep(step(0.8f))
             repository.sendTransitionStep(step(1f))
 
-            // Only two values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
-            assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -86,58 +80,27 @@
             val job =
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
             repository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[3])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(1f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
 
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_LOCKSCREEN_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
-
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
-
-    private fun step(value: Float): TransitionStep {
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.OCCLUDED,
             to = KeyguardState.LOCKSCREEN,
             value = value,
-            transitionState = TransitionState.RUNNING,
+            transitionState = state,
             ownerName = "OccludedToLockscreenTransitionViewModelTest"
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
index c88f84a..54fc493 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
@@ -71,7 +71,7 @@
         waitUntilComplete(info.animator!!)
     }
 
-    suspend private fun waitUntilComplete(animator: ValueAnimator) {
+    private suspend fun waitUntilComplete(animator: ValueAnimator) {
         withContext(Dispatchers.Main) {
             val startTime = System.currentTimeMillis()
             while (!isTerminated && animator.isRunning()) {
@@ -96,6 +96,6 @@
     override fun setFrameDelay(delay: Long) {}
 
     companion object {
-        private const val MAX_TEST_DURATION = 100L
+        private const val MAX_TEST_DURATION = 200L
     }
 }
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/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
new file mode 100644
index 0000000..411b1bd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class TableLogBufferFactoryTest : SysuiTestCase() {
+    private val dumpManager: DumpManager = mock()
+    private val systemClock = FakeSystemClock()
+    private val underTest = TableLogBufferFactory(dumpManager, systemClock)
+
+    @Test
+    fun `create - always creates new instance`() {
+        val b1 = underTest.create(NAME_1, SIZE)
+        val b1_copy = underTest.create(NAME_1, SIZE)
+        val b2 = underTest.create(NAME_2, SIZE)
+        val b2_copy = underTest.create(NAME_2, SIZE)
+
+        assertThat(b1).isNotSameInstanceAs(b1_copy)
+        assertThat(b1).isNotSameInstanceAs(b2)
+        assertThat(b2).isNotSameInstanceAs(b2_copy)
+    }
+
+    @Test
+    fun `getOrCreate - reuses instance`() {
+        val b1 = underTest.getOrCreate(NAME_1, SIZE)
+        val b1_copy = underTest.getOrCreate(NAME_1, SIZE)
+        val b2 = underTest.getOrCreate(NAME_2, SIZE)
+        val b2_copy = underTest.getOrCreate(NAME_2, SIZE)
+
+        assertThat(b1).isSameInstanceAs(b1_copy)
+        assertThat(b2).isSameInstanceAs(b2_copy)
+        assertThat(b1).isNotSameInstanceAs(b2)
+        assertThat(b1_copy).isNotSameInstanceAs(b2_copy)
+    }
+
+    companion object {
+        const val NAME_1 = "name 1"
+        const val NAME_2 = "name 2"
+
+        const val SIZE = 8
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
index 1d6e980..670f117 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
@@ -113,5 +113,6 @@
         recommendations = emptyList(),
         dismissIntent = null,
         headphoneConnectionTimeMillis = 0,
-        instanceId = InstanceId.fakeInstanceId(-1)
+        instanceId = InstanceId.fakeInstanceId(-1),
+        expiryTimeMs = 0,
     )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index 4d2d0f0..0a5b124 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
                 USER_ID, true, APP, null, ARTIST, TITLE, null,
                 new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
                 MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
-                InstanceId.fakeInstanceId(-1), -1);
+                InstanceId.fakeInstanceId(-1), -1, false, null);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
index 9d33e6f..8532ffe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.pipeline
 
 import android.app.smartspace.SmartspaceAction
+import android.os.Bundle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
@@ -25,13 +26,16 @@
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_RESUME
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import com.android.systemui.media.controls.ui.MediaPlayerData
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
@@ -40,11 +44,11 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 private const val KEY = "TEST_KEY"
@@ -72,6 +76,8 @@
     @Mock private lateinit var smartspaceData: SmartspaceMediaData
     @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
     @Mock private lateinit var logger: MediaUiEventLogger
+    @Mock private lateinit var mediaFlags: MediaFlags
+    @Mock private lateinit var cardAction: SmartspaceAction
 
     private lateinit var mediaDataFilter: MediaDataFilter
     private lateinit var dataMain: MediaData
@@ -82,6 +88,7 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         MediaPlayerData.clear()
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         mediaDataFilter =
             MediaDataFilter(
                 context,
@@ -90,7 +97,8 @@
                 lockscreenUserManager,
                 executor,
                 clock,
-                logger
+                logger,
+                mediaFlags
             )
         mediaDataFilter.mediaDataManager = mediaDataManager
         mediaDataFilter.addListener(listener)
@@ -108,19 +116,21 @@
             )
         dataGuest = dataMain.copy(userId = USER_GUEST)
 
-        `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
-        `when`(smartspaceData.isActive).thenReturn(true)
-        `when`(smartspaceData.isValid()).thenReturn(true)
-        `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
-        `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
-        `when`(smartspaceData.headphoneConnectionTimeMillis)
+        whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
+        whenever(smartspaceData.isActive).thenReturn(true)
+        whenever(smartspaceData.isValid()).thenReturn(true)
+        whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
+        whenever(smartspaceData.recommendations)
+            .thenReturn(listOf(smartspaceMediaRecommendationItem))
+        whenever(smartspaceData.headphoneConnectionTimeMillis)
             .thenReturn(clock.currentTimeMillis() - 100)
-        `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+        whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+        whenever(smartspaceData.cardAction).thenReturn(cardAction)
     }
 
     private fun setUser(id: Int) {
-        `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
-        `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
+        whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+        whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
         mediaDataFilter.handleUserSwitched(id)
     }
 
@@ -277,7 +287,7 @@
 
     @Test
     fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
-        `when`(smartspaceData.isActive).thenReturn(false)
+        whenever(smartspaceData.isActive).thenReturn(false)
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -285,7 +295,7 @@
 
     @Test
     fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
-        `when`(smartspaceData.isValid()).thenReturn(false)
+        whenever(smartspaceData.isValid()).thenReturn(false)
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -293,8 +303,8 @@
 
     @Test
     fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
-        `when`(smartspaceData.isActive).thenReturn(true)
-        `when`(smartspaceData.isValid()).thenReturn(true)
+        whenever(smartspaceData.isActive).thenReturn(true)
+        whenever(smartspaceData.isValid()).thenReturn(true)
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
@@ -349,7 +359,7 @@
 
     @Test
     fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
-        `when`(smartspaceData.isActive).thenReturn(false)
+        whenever(smartspaceData.isActive).thenReturn(false)
 
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
@@ -379,7 +389,7 @@
 
     @Test
     fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
-        `when`(smartspaceData.isActive).thenReturn(false)
+        whenever(smartspaceData.isActive).thenReturn(false)
 
         val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
@@ -395,7 +405,7 @@
 
     @Test
     fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
-        `when`(smartspaceData.isActive).thenReturn(false)
+        whenever(smartspaceData.isActive).thenReturn(false)
 
         // WHEN we have media that was recently played, but not currently active
         val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -418,7 +428,7 @@
 
     @Test
     fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
-        `when`(smartspaceData.isValid()).thenReturn(false)
+        whenever(smartspaceData.isValid()).thenReturn(false)
 
         // WHEN we have media that was recently played, but not currently active
         val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -513,4 +523,110 @@
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
         assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
     }
+
+    @Test
+    fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        whenever(smartspaceData.isActive).thenReturn(false)
+
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+    }
+
+    @Test
+    fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        whenever(smartspaceData.isActive).thenReturn(false)
+
+        // If there is media that was recently played but inactive
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // And an inactive recommendation is loaded
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // Smartspace is loaded but the media stays inactive
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+        verify(listener, never())
+            .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+    }
+
+    @Test
+    fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+        val data =
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                targetId = SMARTSPACE_KEY,
+                isActive = true,
+                packageName = SMARTSPACE_PACKAGE,
+                recommendations = listOf(smartspaceMediaRecommendationItem),
+            )
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
+        mediaDataFilter.onSwipeToDismiss()
+
+        verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
+        verify(mediaDataManager, never())
+            .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
+    }
+
+    @Test
+    fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() {
+        // WHEN we have media that was recently played, but not currently active
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // AND we get a smartspace signal with extra to trigger resume
+        val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) }
+        whenever(cardAction.extras).thenReturn(extras)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // THEN we should tell listeners to treat the media as active instead
+        val dataCurrentAndActive = dataCurrent.copy(active = true)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+        // And send the smartspace data, but not prioritized
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+    }
+
+    @Test
+    fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() {
+        // WHEN we have media that was recently played, but not currently active
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // AND we get a smartspace signal with extra to not trigger resume
+        val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
+        whenever(cardAction.extras).thenReturn(extras)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // THEN listeners are not updated to show media
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
+        // But the smartspace update is still propagated
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+    }
 }
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 52b694f..ab0669a 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
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.pipeline
 
 import android.app.Notification
+import android.app.Notification.FLAG_NO_CLEAR
 import android.app.Notification.MediaStyle
 import android.app.PendingIntent
 import android.app.smartspace.SmartspaceAction
@@ -39,15 +40,19 @@
 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
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
 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
@@ -81,11 +86,15 @@
 private const val KEY = "KEY"
 private const val KEY_2 = "KEY_2"
 private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+private const val SMARTSPACE_CREATION_TIME = 1234L
+private const val SMARTSPACE_EXPIRY_TIME = 5678L
 private const val PACKAGE_NAME = "com.example.app"
 private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
 private const val APP_NAME = "SystemUI"
 private const val SESSION_ARTIST = "artist"
 private const val SESSION_TITLE = "title"
+private const val SESSION_BLANK_TITLE = " "
+private const val SESSION_EMPTY_TITLE = ""
 private const val USER_ID = 0
 private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
 
@@ -120,6 +129,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
@@ -129,11 +139,13 @@
     @Mock private lateinit var logger: MediaUiEventLogger
     lateinit var mediaDataManager: MediaDataManager
     lateinit var mediaNotification: StatusBarNotification
+    lateinit var remoteCastNotification: StatusBarNotification
     @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
     private val clock = FakeSystemClock()
     @Mock private lateinit var tunerService: TunerService
     @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
-    @Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
+    @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
+    @Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
     @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
 
     private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
@@ -180,9 +192,12 @@
                 mediaFlags = mediaFlags,
                 logger = logger,
                 smartspaceManager = smartspaceManager,
+                keyguardUpdateMonitor = keyguardUpdateMonitor
             )
         verify(tunerService)
             .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
+        verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
+        verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
         session = MediaSession(context, "MediaDataManagerTestSession")
         mediaNotification =
             SbnBuilder().run {
@@ -193,6 +208,20 @@
                 }
                 build()
             }
+        remoteCastNotification =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
+            }
         metadataBuilder =
             MediaMetadata.Builder().apply {
                 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
@@ -202,6 +231,7 @@
         whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
         whenever(controller.transportControls).thenReturn(transportControls)
         whenever(controller.playbackInfo).thenReturn(playbackInfo)
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         whenever(playbackInfo.playbackType)
             .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
 
@@ -226,9 +256,15 @@
         whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
         whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
         whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
-        whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
+        whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
+        whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
+        whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
+        whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
         whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
+        whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false)
     }
 
     @After
@@ -300,6 +336,54 @@
     }
 
     @Test
+    fun testLoadMetadata_withExplicitIndicator() {
+        whenever(controller.metadata)
+            .thenReturn(
+                metadataBuilder
+                    .putLong(
+                        MediaConstants.METADATA_KEY_IS_EXPLICIT,
+                        MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+                    )
+                    .build()
+            )
+
+        mediaDataManager.addListener(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.isExplicit).isTrue()
+    }
+
+    @Test
+    fun testOnMetaDataLoaded_withoutExplicitIndicator() {
+        mediaDataManager.addListener(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.isExplicit).isFalse()
+    }
+
+    @Test
     fun testOnMetaDataLoaded_callsListener() {
         addNotificationAndLoad()
         verify(logger)
@@ -314,7 +398,6 @@
     @Test
     fun testOnMetaDataLoaded_conservesActiveFlag() {
         whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.addListener(listener)
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -333,33 +416,8 @@
 
     @Test
     fun testOnNotificationAdded_isRcn_markedRemote() {
-        val rcn =
-            SbnBuilder().run {
-                setPkg(SYSTEM_PACKAGE_NAME)
-                modifyNotification(context).also {
-                    it.setSmallIcon(android.R.drawable.ic_media_pause)
-                    it.setStyle(
-                        MediaStyle().apply {
-                            setMediaSession(session.sessionToken)
-                            setRemotePlaybackInfo("Remote device", 0, null)
-                        }
-                    )
-                }
-                build()
-            }
+        addNotificationAndLoad(remoteCastNotification)
 
-        mediaDataManager.onNotificationAdded(KEY, rcn)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(KEY),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
         assertThat(mediaDataCaptor.value!!.playbackLocation)
             .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
         verify(logger)
@@ -459,9 +517,78 @@
     }
 
     @Test
+    fun testOnNotificationRemoved_emptyTitle_notConverted() {
+        // GIVEN that the manager has a notification with a resume action and empty title.
+        whenever(controller.metadata)
+            .thenReturn(
+                metadataBuilder
+                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
+                    .build()
+            )
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        val instanceId = data.instanceId
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+
+        // WHEN the notification is removed
+        reset(listener)
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // THEN active media is not converted to resume.
+        verify(listener, never())
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        verify(logger, never())
+            .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
+        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
+    }
+
+    @Test
+    fun testOnNotificationRemoved_blankTitle_notConverted() {
+        // GIVEN that the manager has a notification with a resume action and blank title.
+        whenever(controller.metadata)
+            .thenReturn(
+                metadataBuilder
+                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
+                    .build()
+            )
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        val instanceId = data.instanceId
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+
+        // WHEN the notification is removed
+        reset(listener)
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // THEN active media is not converted to resume.
+        verify(listener, never())
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        verify(logger, never())
+            .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
+        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
+    }
+
+    @Test
     fun testOnNotificationRemoved_withResumption() {
         // GIVEN that the manager has a notification with a resume action
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
@@ -486,11 +613,11 @@
     @Test
     fun testOnNotificationRemoved_twoWithResumption() {
         // GIVEN that the manager has two notifications with resume actions
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
+
         verify(listener)
             .onMediaDataLoaded(
                 eq(KEY),
@@ -502,9 +629,21 @@
             )
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
-        val resumableData = data.copy(resumeAction = Runnable {})
-        mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
-        mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
+
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY_2),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        val data2 = mediaDataCaptor.value
+        assertThat(data2.resumption).isFalse()
+
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        mediaDataManager.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
         reset(listener)
         // WHEN the first is removed
         mediaDataManager.onNotificationRemoved(KEY)
@@ -539,7 +678,6 @@
     @Test
     fun testOnNotificationRemoved_withResumption_butNotLocal() {
         // GIVEN that the manager has a notification with a resume action, but is not local
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         whenever(playbackInfo.playbackType)
             .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
         addNotificationAndLoad()
@@ -563,6 +701,108 @@
     }
 
     @Test
+    fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() {
+        // With the flag enabled to allow remote media to resume
+        whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
+
+        // GIVEN that the manager has a notification with a resume action, but is not local
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        val dataRemoteWithResume =
+            data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
+        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+
+        // WHEN the notification is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // THEN the media data is converted to a resume state
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+    }
+
+    @Test
+    fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() {
+        // With the flag enabled to allow remote media to resume
+        whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
+
+        // GIVEN that the manager has a remote cast notification
+        addNotificationAndLoad(remoteCastNotification)
+        val data = mediaDataCaptor.value
+        assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
+        val dataRemoteWithResume = data.copy(resumeAction = Runnable {})
+        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+
+        // WHEN the RCN is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // THEN the media data is removed
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @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
+        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 =
@@ -571,27 +811,8 @@
                 build()
             }
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.addResumptionControls(
-            USER_ID,
-            desc,
-            Runnable {},
-            session.sessionToken,
-            APP_NAME,
-            pendingIntent,
-            PACKAGE_NAME
-        )
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        // THEN the media data indicates that it is for resumption
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(PACKAGE_NAME),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
+        addResumeControlAndLoad(desc)
+
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -603,11 +824,137 @@
     }
 
     @Test
-    fun testResumptionDisabled_dismissesResumeControls() {
-        // WHEN there are resume controls and resumption is switched off
+    fun testAddResumptionControls_withExplicitIndicator() {
+        val bundle = Bundle()
+        // WHEN resumption controls are added with explicit indicator
+        bundle.putLong(
+            MediaConstants.METADATA_KEY_IS_EXPLICIT,
+            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+        )
         val desc =
             MediaDescription.Builder().run {
                 setTitle(SESSION_TITLE)
+                setExtras(bundle)
+                build()
+            }
+        val currentTime = clock.elapsedRealtime()
+        addResumeControlAndLoad(desc)
+
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.song).isEqualTo(SESSION_TITLE)
+        assertThat(data.app).isEqualTo(APP_NAME)
+        assertThat(data.actions).hasSize(1)
+        assertThat(data.semanticActions!!.playOrPause).isNotNull()
+        assertThat(data.lastActive).isAtLeast(currentTime)
+        assertThat(data.isExplicit).isTrue()
+        verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testAddResumptionControls_hasPartialProgress() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added with partial progress
+        val progress = 0.5
+        val extras =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+                )
+                putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress)
+            }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(extras)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(progress)
+    }
+
+    @Test
+    fun testAddResumptionControls_hasNotPlayedProgress() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added that have not been played
+        val extras =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
+                )
+            }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(extras)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(0)
+    }
+
+    @Test
+    fun testAddResumptionControls_hasFullProgress() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added with progress info
+        val extras =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
+                )
+            }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(extras)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        // THEN the media data includes the progress
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(1)
+    }
+
+    @Test
+    fun testAddResumptionControls_hasNoExtras() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added that do not have any extras
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        // Resume progress is null
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(null)
+    }
+
+    @Test
+    fun testAddResumptionControls_hasEmptyTitle() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added that have empty title
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_EMPTY_TITLE)
                 build()
             }
         mediaDataManager.addResumptionControls(
@@ -619,9 +966,11 @@
             pendingIntent,
             PACKAGE_NAME
         )
+
+        // Resumption controls are not added.
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(0)
+        verify(listener, never())
             .onMediaDataLoaded(
                 eq(PACKAGE_NAME),
                 eq(null),
@@ -630,6 +979,52 @@
                 eq(0),
                 eq(false)
             )
+    }
+
+    @Test
+    fun testAddResumptionControls_hasBlankTitle() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added that have a blank title
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_BLANK_TITLE)
+                build()
+            }
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+
+        // Resumption controls are not added.
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(0)
+        verify(listener, never())
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testResumptionDisabled_dismissesResumeControls() {
+        // WHEN there are resume controls and resumption is switched off
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
         val data = mediaDataCaptor.value
         mediaDataManager.setMediaResumptionEnabled(false)
 
@@ -705,8 +1100,9 @@
                         cardAction = mediaSmartspaceBaseAction,
                         recommendations = validRecommendationList,
                         dismissIntent = DISMISS_INTENT,
-                        headphoneConnectionTimeMillis = 1234L,
-                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
                     )
                 ),
                 eq(false)
@@ -728,8 +1124,9 @@
                         targetId = KEY_MEDIA_SMARTSPACE,
                         isActive = true,
                         dismissIntent = DISMISS_INTENT,
-                        headphoneConnectionTimeMillis = 1234L,
-                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
                     )
                 ),
                 eq(false)
@@ -759,8 +1156,9 @@
                         targetId = KEY_MEDIA_SMARTSPACE,
                         isActive = true,
                         dismissIntent = null,
-                        headphoneConnectionTimeMillis = 1234L,
-                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
                     )
                 ),
                 eq(false)
@@ -789,6 +1187,129 @@
     }
 
     @Test
+    fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    SmartspaceMediaData(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        packageName = PACKAGE_NAME,
+                        cardAction = mediaSmartspaceBaseAction,
+                        recommendations = validRecommendationList,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+                    )
+                ),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        val extras =
+            Bundle().apply {
+                putString("package_name", PACKAGE_NAME)
+                putParcelable("dismiss_intent", DISMISS_INTENT)
+                putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
+            }
+        whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    SmartspaceMediaData(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = false,
+                        packageName = PACKAGE_NAME,
+                        cardAction = mediaSmartspaceBaseAction,
+                        recommendations = validRecommendationList,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+                    )
+                ),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf())
+        uiExecutor.advanceClockToLast()
+        uiExecutor.runAllReady()
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    SmartspaceMediaData(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = false,
+                        packageName = PACKAGE_NAME,
+                        cardAction = mediaSmartspaceBaseAction,
+                        recommendations = validRecommendationList,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+                    )
+                ),
+                eq(false)
+            )
+        verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
+    }
+
+    @Test
+    fun testSetRecommendationInactive_notifiesListeners() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+        uiExecutor.advanceClockToLast()
+        uiExecutor.runAllReady()
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    SmartspaceMediaData(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = false,
+                        packageName = PACKAGE_NAME,
+                        cardAction = mediaSmartspaceBaseAction,
+                        recommendations = validRecommendationList,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+                        instanceId = InstanceId.fakeInstanceId(instanceId),
+                        expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+                    )
+                ),
+                eq(false)
+            )
+    }
+
+    @Test
     fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
         // WHEN media recommendation setting is off
         Settings.Secure.putInt(
@@ -863,7 +1384,6 @@
     @Test
     fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() {
         // GIVEN that the manager has a notification with a resume action
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         val instanceId = data.instanceId
@@ -1163,7 +1683,6 @@
         val instanceId = mediaDataCaptor.value.instanceId
 
         // Location is updated to local cast
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         whenever(playbackInfo.playbackType)
             .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
         addNotificationAndLoad()
@@ -1176,22 +1695,7 @@
             )
 
         // update to remote cast
-        val rcn =
-            SbnBuilder().run {
-                setPkg(SYSTEM_PACKAGE_NAME) // System package
-                modifyNotification(context).also {
-                    it.setSmallIcon(android.R.drawable.ic_media_pause)
-                    it.setStyle(
-                        MediaStyle().apply {
-                            setMediaSession(session.sessionToken)
-                            setRemotePlaybackInfo("Remote device", 0, null)
-                        }
-                    )
-                }
-                build()
-            }
-
-        mediaDataManager.onNotificationAdded(KEY, rcn)
+        mediaDataManager.onNotificationAdded(KEY, remoteCastNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(logger)
@@ -1207,11 +1711,10 @@
     fun testPlaybackStateChange_keyExists_callsListener() {
         // Notification has been added
         addNotificationAndLoad()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        callbackCaptor.value.invoke(KEY, state)
+        stateCallbackCaptor.value.invoke(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1229,11 +1732,10 @@
     @Test
     fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
         val state = PlaybackState.Builder().build()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
 
         // No media added with this key
 
-        callbackCaptor.value.invoke(KEY, state)
+        stateCallbackCaptor.value.invoke(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1241,7 +1743,6 @@
     @Test
     fun testPlaybackStateChange_keyHasNullToken_doesNothing() {
         // When we get an update that sets the data's token to null
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
@@ -1249,10 +1750,9 @@
 
         // And then get a state update
         val state = PlaybackState.Builder().build()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
 
         // Then no changes are made
-        callbackCaptor.value.invoke(KEY, state)
+        stateCallbackCaptor.value.invoke(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1264,8 +1764,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-        callbackCaptor.value.invoke(KEY, state)
+        stateCallbackCaptor.value.invoke(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -1307,8 +1806,7 @@
         backgroundExecutor.runAllReady()
         foregroundExecutor.runAllReady()
 
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-        callbackCaptor.value.invoke(PACKAGE_NAME, state)
+        stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -1333,8 +1831,7 @@
                 .build()
 
         addNotificationAndLoad()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-        callbackCaptor.value.invoke(KEY, state)
+        stateCallbackCaptor.value.invoke(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -1349,9 +1846,232 @@
         assertThat(mediaDataCaptor.value.semanticActions).isNull()
     }
 
-    /** Helper function to add a media notification and capture the resulting MediaData */
-    private fun addNotificationAndLoad() {
+    @Test
+    fun testNoClearNotOngoing_canDismiss() {
+        mediaNotification =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    it.setOngoing(false)
+                    it.setFlag(FLAG_NO_CLEAR, true)
+                }
+                build()
+            }
+        addNotificationAndLoad()
+        assertThat(mediaDataCaptor.value.isClearable).isTrue()
+    }
+
+    @Test
+    fun testOngoing_cannotDismiss() {
+        mediaNotification =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    it.setOngoing(true)
+                }
+                build()
+            }
+        addNotificationAndLoad()
+        assertThat(mediaDataCaptor.value.isClearable).isFalse()
+    }
+
+    @Test
+    fun testRetain_notifPlayer_notifRemoved_setToResume() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+        // When a media control based on notification is added, times out, and then removed
+        addNotificationAndLoad()
+        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        assertThat(mediaDataCaptor.value.active).isFalse()
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // It is converted to a resume player
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.active).isFalse()
+        verify(logger)
+            .logActiveConvertedToResume(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId)
+            )
+    }
+
+    @Test
+    fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+        // When a media control based on notification is added and times out
+        addNotificationAndLoad()
+        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        assertThat(mediaDataCaptor.value.active).isFalse()
+
+        // and then the session is destroyed
+        sessionCallbackCaptor.value.invoke(KEY)
+
+        // It remains as a regular player
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never())
+            .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+        // When a media control based on notification is added and then removed, without timing out
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.active).isTrue()
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // It is fully removed
+        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+        verify(listener, never())
+            .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testRetain_canResume_removeWhileActive_setToResume() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+        // When a media control that supports resumption is added
+        addNotificationAndLoad()
+        val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
+        mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+
+        // And then removed while still active
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // It is converted to a resume player
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.active).isFalse()
+        verify(logger)
+            .logActiveConvertedToResume(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId)
+            )
+    }
+
+    @Test
+    fun testRetain_sessionPlayer_notifRemoved_doesNotChange() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        addPlaybackStateAction()
+
+        // When a media control with PlaybackState actions is added, times out,
+        // and then the notification is removed
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.active).isTrue()
+        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // It remains as a regular player
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never())
+            .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testRetain_sessionPlayer_sessionDestroyed_setToResume() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        addPlaybackStateAction()
+
+        // When a media control with PlaybackState actions is added, times out,
+        // and then the session is destroyed
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.active).isTrue()
+        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        sessionCallbackCaptor.value.invoke(KEY)
+
+        // It is converted to a resume player
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.active).isFalse()
+        verify(logger)
+            .logActiveConvertedToResume(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId)
+            )
+    }
+
+    @Test
+    fun testRetain_sessionPlayer_destroyedWhileActive_fullyRemoved() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        addPlaybackStateAction()
+
+        // When a media control using session actions is added, and then the session is destroyed
+        // without timing out first
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.active).isTrue()
+        sessionCallbackCaptor.value.invoke(KEY)
+
+        // It is fully removed
+        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+        verify(listener, never())
+            .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testSessionDestroyed_noNotificationKey_stillRemoved() {
+        whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+
+        // When a notiifcation is added and then removed before it is fully processed
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        backgroundExecutor.runAllReady()
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // We still make sure to remove it
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    /** Helper function to add a basic media notification and capture the resulting MediaData */
+    private fun addNotificationAndLoad() {
+        addNotificationAndLoad(mediaNotification)
+    }
+
+    /** Helper function to add the given notification and capture the resulting MediaData */
+    private fun addNotificationAndLoad(sbn: StatusBarNotification) {
+        mediaDataManager.onNotificationAdded(KEY, sbn)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(listener)
@@ -1364,4 +2084,40 @@
                 eq(false)
             )
     }
+
+    /** Helper function to set up a PlaybackState with action */
+    private fun addPlaybackStateAction() {
+        val stateActions = PlaybackState.ACTION_PLAY_PAUSE
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
+        stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+    }
+
+    /** Helper function to add a resumption control and capture the resulting MediaData */
+    private fun addResumeControlAndLoad(
+        desc: MediaDescription,
+        packageName: String = PACKAGE_NAME
+    ) {
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            packageName
+        )
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(packageName),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
index 344dffa..8baa06a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
@@ -25,13 +25,16 @@
 import com.android.systemui.SysuiTestCase
 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
 import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -48,7 +51,6 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "KEY"
@@ -56,6 +58,7 @@
 private const val SESSION_KEY = "SESSION_KEY"
 private const val SESSION_ARTIST = "SESSION_ARTIST"
 private const val SESSION_TITLE = "SESSION_TITLE"
+private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
 
 private fun <T> anyObject(): T {
     return Mockito.anyObject<T>()
@@ -72,6 +75,7 @@
     private lateinit var executor: FakeExecutor
     @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
     @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
+    @Mock private lateinit var sessionCallback: (String) -> Unit
     @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
     @Captor
     private lateinit var dozingCallbackCaptor:
@@ -84,10 +88,13 @@
     private lateinit var resumeData: MediaData
     private lateinit var mediaTimeoutListener: MediaTimeoutListener
     private var clock = FakeSystemClock()
+    @Mock private lateinit var mediaFlags: MediaFlags
+    @Mock private lateinit var smartspaceData: SmartspaceMediaData
 
     @Before
     fun setup() {
-        `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
+        whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         executor = FakeExecutor(clock)
         mediaTimeoutListener =
             MediaTimeoutListener(
@@ -95,10 +102,12 @@
                 executor,
                 logger,
                 statusBarStateController,
-                clock
+                clock,
+                mediaFlags,
             )
         mediaTimeoutListener.timeoutCallback = timeoutCallback
         mediaTimeoutListener.stateCallback = stateCallback
+        mediaTimeoutListener.sessionCallback = sessionCallback
 
         // Create a media session and notification for testing.
         metadataBuilder =
@@ -131,9 +140,9 @@
     @Test
     fun testOnMediaDataLoaded_registersPlaybackListener() {
         val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+        whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
 
-        `when`(mediaController.playbackState).thenReturn(playingState)
+        whenever(mediaController.playbackState).thenReturn(playingState)
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
         verify(logger).logPlaybackState(eq(KEY), eq(playingState))
@@ -186,8 +195,8 @@
 
         // To playing
         val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
-        `when`(mediaController.playbackState).thenReturn(playingState)
+        whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+        whenever(mediaController.playbackState).thenReturn(playingState)
         mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
         verify(mediaController).unregisterCallback(anyObject())
         verify(mediaController).registerCallback(anyObject())
@@ -206,8 +215,8 @@
 
         // Migrate, still not playing
         val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
-        `when`(mediaController.playbackState).thenReturn(playingState)
+        whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
+        whenever(mediaController.playbackState).thenReturn(playingState)
         mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
 
         // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
@@ -284,6 +293,7 @@
         verify(mediaController).unregisterCallback(anyObject())
         assertThat(executor.numPending()).isEqualTo(0)
         verify(logger).logSessionDestroyed(eq(KEY))
+        verify(sessionCallback).invoke(eq(KEY))
     }
 
     @Test
@@ -293,8 +303,8 @@
 
         // WHEN we get an update with media playing
         val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
-        `when`(mediaController.playbackState).thenReturn(playingState)
+        whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+        whenever(mediaController.playbackState).thenReturn(playingState)
         val mediaPlaying = mediaData.copy(isPlaying = true)
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
 
@@ -322,6 +332,7 @@
         // THEN the controller is unregistered, but the timeout is still scheduled
         verify(mediaController).unregisterCallback(anyObject())
         assertThat(executor.numPending()).isEqualTo(1)
+        verify(sessionCallback, never()).invoke(eq(KEY))
     }
 
     @Test
@@ -343,7 +354,7 @@
         // WHEN regular media is paused
         val pausedState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
-        `when`(mediaController.playbackState).thenReturn(pausedState)
+        whenever(mediaController.playbackState).thenReturn(pausedState)
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         assertThat(executor.numPending()).isEqualTo(1)
 
@@ -375,7 +386,7 @@
         // AND that media is resumed
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
-        `when`(mediaController.playbackState).thenReturn(playingState)
+        whenever(mediaController.playbackState).thenReturn(playingState)
         mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
 
         // THEN the timeout length is changed to a regular media control
@@ -589,8 +600,91 @@
         assertThat(executor.numPending()).isEqualTo(1)
     }
 
+    @Test
+    fun testSmartspaceDataLoaded_schedulesTimeout() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        val duration = 60_000
+        val createTime = 1234L
+        val expireTime = createTime + duration
+        whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+        whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+        mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(executor.advanceClockToNext()).isEqualTo(duration)
+    }
+
+    @Test
+    fun testSmartspaceMediaData_timesOut_invokesCallback() {
+        // Given a pending timeout
+        testSmartspaceDataLoaded_schedulesTimeout()
+
+        executor.runAllReady()
+        verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
+    }
+
+    @Test
+    fun testSmartspaceDataLoaded_alreadyExists_updatesTimeout() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        val duration = 100
+        val createTime = 1234L
+        val expireTime = createTime + duration
+        whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+        whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+        mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        val expiryLonger = expireTime + duration
+        whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
+        mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
+    }
+
+    @Test
+    fun testSmartspaceDataRemoved_cancelTimeout() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+        mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testSmartspaceData_dozedPastTimeout_invokedOnWakeup() {
+        // Given a pending timeout
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+        val duration = 60_000
+        val createTime = 1234L
+        val expireTime = createTime + duration
+        whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+        whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+        mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // And we doze past the scheduled timeout
+        val time = clock.currentTimeMillis()
+        clock.setElapsedRealtime(time + duration * 2)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // Then when no longer dozing, the timeout runs immediately
+        dozingCallbackCaptor.value.onDozingChanged(false)
+        verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
+        verify(logger).logTimeout(eq(SMARTSPACE_KEY))
+
+        // and cancel any later scheduled timeout
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
     private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
-        `when`(mediaController.playbackState).thenReturn(state)
+        whenever(mediaController.playbackState).thenReturn(state)
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
index 136ace1..4dfa626 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.media.controls.models.player.MediaDeviceData
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -92,6 +93,7 @@
     @Mock private lateinit var mockContext: Context
     @Mock private lateinit var pendingIntent: PendingIntent
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var mediaFlags: MediaFlags
 
     @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
     @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
@@ -134,6 +136,7 @@
         whenever(mockContext.packageManager).thenReturn(context.packageManager)
         whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
         whenever(mockContext.userId).thenReturn(context.userId)
+        whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
 
         executor = FakeExecutor(clock)
         resumeListener =
@@ -146,7 +149,8 @@
                 tunerService,
                 resumeBrowserFactory,
                 dumpManager,
-                clock
+                clock,
+                mediaFlags,
             )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
@@ -188,7 +192,8 @@
                 tunerService,
                 resumeBrowserFactory,
                 dumpManager,
-                clock
+                clock,
+                mediaFlags,
             )
         listener.setManager(mediaDataManager)
         verify(broadcastDispatcher, never())
@@ -244,6 +249,32 @@
     }
 
     @Test
+    fun testOnLoad_localCast_remoteResumeAllowed_doesCheck() {
+        // If local cast media is allowed to resume
+        whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
+
+        // When media data is loaded that has not been checked yet, and is a local cast
+        val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCast)
+
+        // Then we report back to the manager
+        verify(mediaDataManager).setResumeAction(KEY, null)
+    }
+
+    @Test
+    fun testOnLoad_remoteCast_remoteResumeAllowed_doesCheck() {
+        // If local cast media is allowed to resume
+        whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
+
+        // When media data is loaded that has not been checked yet, and is a remote cast
+        val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
+        resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
+
+        // Then we do not take action
+        verify(mediaDataManager, never()).setResumeAction(any(), any())
+    }
+
+    @Test
     fun testOnLoad_checksForResume_hasService() {
         setUpMbsWithValidResolveInfo()
 
@@ -389,7 +420,8 @@
                 tunerService,
                 resumeBrowserFactory,
                 dumpManager,
-                clock
+                clock,
+                mediaFlags,
             )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
@@ -421,7 +453,8 @@
                 tunerService,
                 resumeBrowserFactory,
                 dumpManager,
-                clock
+                clock,
+                mediaFlags,
             )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
@@ -463,7 +496,8 @@
                 tunerService,
                 resumeBrowserFactory,
                 dumpManager,
-                clock
+                clock,
+                mediaFlags,
             )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
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 6ca34e0..7f57077 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
@@ -17,45 +17,60 @@
 package com.android.systemui.media.controls.ui
 
 import android.app.PendingIntent
+import android.content.res.ColorStateList
+import android.content.res.Configuration
 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.R
 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
 import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
 import com.android.systemui.media.controls.pipeline.MediaDataManager
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
 import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.PageIndicator
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import javax.inject.Provider
 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
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.floatThat
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -75,7 +90,6 @@
     @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
     @Mock lateinit var mediaHostState: MediaHostState
     @Mock lateinit var activityStarter: ActivityStarter
-    @Mock @Main private lateinit var executor: DelayableExecutor
     @Mock lateinit var mediaDataManager: MediaDataManager
     @Mock lateinit var configurationController: ConfigurationController
     @Mock lateinit var falsingCollector: FalsingCollector
@@ -85,15 +99,29 @@
     @Mock lateinit var debugLogger: MediaCarouselControllerLogger
     @Mock lateinit var mediaViewController: MediaViewController
     @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
+    @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 visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
+    @Captor lateinit var keyguardCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
 
     private val clock = FakeSystemClock()
     private lateinit var mediaCarouselController: MediaCarouselController
+    private lateinit var mainExecutor: FakeExecutor
+    private lateinit var backgroundExecutor: FakeExecutor
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        transitionRepository = FakeKeyguardTransitionRepository()
+        mainExecutor = FakeExecutor(clock)
+        backgroundExecutor = FakeExecutor(clock)
         mediaCarouselController =
             MediaCarouselController(
                 context,
@@ -102,25 +130,31 @@
                 mediaHostStatesManager,
                 activityStarter,
                 clock,
-                executor,
+                mainExecutor,
+                backgroundExecutor,
                 mediaDataManager,
                 configurationController,
                 falsingCollector,
                 falsingManager,
                 dumpManager,
                 logger,
-                debugLogger
+                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)
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         MediaPlayerData.clear()
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testPlayerOrdering() {
         // Test values: key, data, last active time
@@ -297,7 +331,6 @@
         }
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testOrderWithSmartspace_prioritized() {
         testPlayerOrdering()
@@ -305,7 +338,7 @@
         // If smartspace is prioritized
         MediaPlayerData.addMediaRecommendation(
             SMARTSPACE_KEY,
-            EMPTY_SMARTSPACE_MEDIA_DATA,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             true,
             clock
@@ -315,7 +348,6 @@
         assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() {
         testPlayerOrdering()
@@ -332,7 +364,6 @@
         assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testOrderWithSmartspace_notPrioritized() {
         testPlayerOrdering()
@@ -340,7 +371,7 @@
         // If smartspace is not prioritized
         MediaPlayerData.addMediaRecommendation(
             SMARTSPACE_KEY,
-            EMPTY_SMARTSPACE_MEDIA_DATA,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             false,
             clock
@@ -351,7 +382,6 @@
         assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
         testPlayerOrdering()
@@ -376,6 +406,7 @@
                 resumption = true
             )
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex("paused local"),
@@ -389,7 +420,6 @@
         )
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testSwipeDismiss_logged() {
         mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
@@ -397,7 +427,6 @@
         verify(logger).logSwipeDismiss()
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testSettingsButton_logged() {
         mediaCarouselController.settingsButton.callOnClick()
@@ -405,18 +434,16 @@
         verify(logger).logCarouselSettings()
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testLocationChangeQs_logged() {
         mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_QS,
+            LOCATION_QS,
             mediaHostState,
             animate = false
         )
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
+        verify(logger).logCarouselPosition(LOCATION_QS)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testLocationChangeQqs_logged() {
         mediaCarouselController.onDesiredLocationChanged(
@@ -427,7 +454,6 @@
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testLocationChangeLockscreen_logged() {
         mediaCarouselController.onDesiredLocationChanged(
@@ -438,7 +464,6 @@
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testLocationChangeDream_logged() {
         mediaCarouselController.onDesiredLocationChanged(
@@ -449,7 +474,6 @@
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testRecommendationRemoved_logged() {
         val packageName = "smartspace package"
@@ -463,7 +487,6 @@
         verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testMediaLoaded_ScrollToActivePlayer() {
         listener.value.onMediaDataLoaded(
@@ -493,6 +516,8 @@
             false
         )
         mediaCarouselController.shouldScrollToKey = true
+        runAllReady()
+
         // switching between media players.
         listener.value.onMediaDataLoaded(
             "playing local",
@@ -514,6 +539,7 @@
                 resumption = false
             )
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex("paused local"),
@@ -521,7 +547,6 @@
         )
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
         listener.value.onSmartspaceMediaDataLoaded(
@@ -539,6 +564,7 @@
                 resumption = false
             )
         )
+        runAllReady()
 
         var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
         assertEquals(
@@ -561,11 +587,12 @@
                 packageName = "PACKAGE_NAME"
             )
         )
+        runAllReady()
+
         playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
         assertEquals(playerIndex, 0)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
         var result = false
@@ -577,7 +604,6 @@
         assertEquals(true, result)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
         var result = false
@@ -591,7 +617,6 @@
         assertEquals(true, result)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testGetCurrentVisibleMediaContentIntent() {
         val clickIntent1 = mock(PendingIntent::class.java)
@@ -638,28 +663,283 @@
         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
     }
 
-    @Ignore("b/253229241")
     @Test
     fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
         val delta = 0.0001F
-        val paginationSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        val paginationSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation(
-                (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION
-            )
+        mediaCarouselController.mediaCarousel = mediaCarousel
+        mediaCarouselController.pageIndicator = pageIndicator
+        whenever(mediaCarousel.measuredHeight).thenReturn(100)
+        whenever(pageIndicator.translationY).thenReturn(80F)
+        whenever(pageIndicator.height).thenReturn(10)
         whenever(mediaHostStatesManager.mediaHostStates)
             .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
         whenever(mediaHostState.visible).thenReturn(true)
         mediaCarouselController.currentEndLocation = LOCATION_QS
-        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+        whenever(mediaHostState.squishFraction).thenReturn(0.938F)
         mediaCarouselController.updatePageIndicatorAlpha()
-        assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+        verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta }
 
-        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+        whenever(mediaHostState.squishFraction).thenReturn(1.0F)
         mediaCarouselController.updatePageIndicatorAlpha()
-        assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+        verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta }
+    }
+
+    @Test
+    fun testOnConfigChanged_playersAreAddedBack() {
+        mediaCarouselController.pageIndicator = pageIndicator
+
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        runAllReady()
+
+        val playersSize = MediaPlayerData.players().size
+
+        configListener.value.onConfigChanged(Configuration())
+        runAllReady()
+
+        verify(pageIndicator).tintList =
+            ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+        assertEquals(playersSize, MediaPlayerData.players().size)
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("playing local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+    }
+
+    @Test
+    fun testOnUiModeChanged_playersAreAddedBack() {
+        mediaCarouselController.pageIndicator = pageIndicator
+
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        runAllReady()
+
+        val playersSize = MediaPlayerData.players().size
+        configListener.value.onUiModeChanged()
+        runAllReady()
+
+        verify(pageIndicator).tintList =
+            ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+        assertEquals(playersSize, MediaPlayerData.players().size)
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+    }
+
+    @Test
+    fun testOnDensityOrFontScaleChanged_playersAreAddedBack() {
+        mediaCarouselController.pageIndicator = pageIndicator
+
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        runAllReady()
+
+        val playersSize = MediaPlayerData.players().size
+        configListener.value.onDensityOrFontScaleChanged()
+        runAllReady()
+
+        verify(pageIndicator).tintList =
+            ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+        assertEquals(playersSize, MediaPlayerData.players().size)
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+    }
+
+    @Test
+    fun testOnThemeChanged_playersAreAddedBack() {
+        mediaCarouselController.pageIndicator = pageIndicator
+
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        runAllReady()
+
+        val playersSize = MediaPlayerData.players().size
+        configListener.value.onThemeChanged()
+        runAllReady()
+
+        verify(pageIndicator).tintList =
+            ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+        assertEquals(playersSize, MediaPlayerData.players().size)
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+    }
+
+    @Test
+    fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() {
+        testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded()
+
+        // When an update to existing smartspace data is loaded
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
+            true
+        )
+
+        // Then the carousel is updated
+        assertTrue(MediaPlayerData.playerKeys().elementAt(0).data.active)
+        assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
+    }
+
+    @Test
+    fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+        // When inactive smartspace data is loaded
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            false
+        )
+
+        // Then it is added to the carousel with correct state
+        assertTrue(MediaPlayerData.playerKeys().elementAt(0).isSsMediaRec)
+        assertFalse(MediaPlayerData.playerKeys().elementAt(0).data.active)
+
+        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()
+        }
+
+    @Test
+    fun testInvisibleToUserAndExpanded_playersNotListening() {
+        // Add players to carousel.
+        testPlayerOrdering()
+
+        // Make the carousel visible to user in expanded layout.
+        mediaCarouselController.currentlyExpanded = true
+        mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true
+
+        // panel is the player for each MediaPlayerData.
+        // Verify that seekbar listening attribute in media control panel is set to true.
+        verify(panel, times(MediaPlayerData.players().size)).listening = true
+
+        // Make the carousel invisible to user.
+        mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = false
+
+        // panel is the player for each MediaPlayerData.
+        // Verify that seekbar listening attribute in media control panel is set to false.
+        verify(panel, times(MediaPlayerData.players().size)).listening = false
+    }
+
+    @Test
+    fun testVisibleToUserAndExpanded_playersListening() {
+        // Add players to carousel.
+        testPlayerOrdering()
+
+        // Make the carousel visible to user in expanded layout.
+        mediaCarouselController.currentlyExpanded = true
+        mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true
+
+        // panel is the player for each MediaPlayerData.
+        // Verify that seekbar listening attribute in media control panel is set to true.
+        verify(panel, times(MediaPlayerData.players().size)).listening = true
+    }
+
+    @Test
+    fun testUMOCollapsed_playersNotListening() {
+        // Add players to carousel.
+        testPlayerOrdering()
+
+        // Make the carousel in collapsed layout.
+        mediaCarouselController.currentlyExpanded = false
+
+        // panel is the player for each MediaPlayerData.
+        // Verify that seekbar listening attribute in media control panel is set to false.
+        verify(panel, times(MediaPlayerData.players().size)).listening = false
+
+        // Make the carousel visible to user.
+        reset(panel)
+        mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true
+
+        // Verify that seekbar listening attribute in media control panel is set to false.
+        verify(panel, times(MediaPlayerData.players().size)).listening = false
+    }
+
+    private fun runAllReady() {
+        backgroundExecutor.runAllReady()
+        mainExecutor.runAllReady()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index b65f5cb..55a33b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -52,8 +52,10 @@
 import androidx.constraintlayout.widget.Barrier
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.LiveData
+import androidx.media.utils.MediaConstants
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.ActivityIntentHelper
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -81,6 +83,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.surfaceeffects.ripple.MultiRippleView
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -154,6 +157,7 @@
     @Mock private lateinit var albumView: ImageView
     private lateinit var titleText: TextView
     private lateinit var artistText: TextView
+    private lateinit var explicitIndicator: CachingIconView
     private lateinit var seamless: ViewGroup
     private lateinit var seamlessButton: View
     @Mock private lateinit var seamlessBackground: RippleDrawable
@@ -201,6 +205,15 @@
     @Mock private lateinit var coverContainer1: ViewGroup
     @Mock private lateinit var coverContainer2: ViewGroup
     @Mock private lateinit var coverContainer3: ViewGroup
+    @Mock private lateinit var recAppIconItem: CachingIconView
+    @Mock private lateinit var recCardTitle: TextView
+    @Mock private lateinit var recProgressBar1: SeekBar
+    @Mock private lateinit var recProgressBar2: SeekBar
+    @Mock private lateinit var recProgressBar3: SeekBar
+    @Mock private lateinit var recSubtitleMock1: TextView
+    @Mock private lateinit var recSubtitleMock2: TextView
+    @Mock private lateinit var recSubtitleMock3: TextView
+    @Mock private lateinit var coverItem: ImageView
     private lateinit var coverItem1: ImageView
     private lateinit var coverItem2: ImageView
     private lateinit var coverItem3: ImageView
@@ -216,14 +229,16 @@
             this.set(Flags.UMO_SURFACE_RIPPLE, false)
             this.set(Flags.UMO_TURBULENCE_NOISE, false)
             this.set(Flags.MEDIA_FALSING_PENALTY, true)
+            this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true)
+            this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
         }
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
     @Before
     fun setUp() {
-        bgExecutor = FakeExecutor(FakeSystemClock())
-        mainExecutor = FakeExecutor(FakeSystemClock())
+        bgExecutor = FakeExecutor(clock)
+        mainExecutor = FakeExecutor(clock)
         whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
         whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
 
@@ -350,6 +365,7 @@
         appIcon = ImageView(context)
         titleText = TextView(context)
         artistText = TextView(context)
+        explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator }
         seamless = FrameLayout(context)
         seamless.foreground = seamlessBackground
         seamlessButton = View(context)
@@ -396,6 +412,7 @@
         whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
         whenever(viewHolder.titleText).thenReturn(titleText)
         whenever(viewHolder.artistText).thenReturn(artistText)
+        whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator)
         whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
         whenever(viewHolder.seamless).thenReturn(seamless)
         whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
@@ -896,6 +913,17 @@
     }
 
     @Test
+    fun bind_resumeState_withProgress() {
+        val progress = 0.5
+        val state = mediaData.copy(resumption = true, resumeProgress = progress)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        verify(seekBarViewModel).updateStaticProgress(progress)
+    }
+
+    @Test
     fun bindNotificationActions() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
@@ -1019,6 +1047,7 @@
 
     @Test
     fun bindText() {
+        useRealConstraintSets()
         player.attachPlayer(viewHolder)
         player.bindPlayer(mediaData, PACKAGE)
 
@@ -1036,6 +1065,8 @@
         handler.onAnimationEnd(mockAnimator)
         assertThat(titleText.getText()).isEqualTo(TITLE)
         assertThat(artistText.getText()).isEqualTo(ARTIST)
+        assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
 
         // Rebinding should not trigger animation
         player.bindPlayer(mediaData, PACKAGE)
@@ -1043,6 +1074,36 @@
     }
 
     @Test
+    fun bindTextWithExplicitIndicator() {
+        useRealConstraintSets()
+        val mediaDataWitExp = mediaData.copy(isExplicit = true)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaDataWitExp, PACKAGE)
+
+        // Capture animation handler
+        val captor = argumentCaptor<Animator.AnimatorListener>()
+        verify(mockAnimator, times(2)).addListener(captor.capture())
+        val handler = captor.value
+
+        // Validate text views unchanged but animation started
+        assertThat(titleText.getText()).isEqualTo("")
+        assertThat(artistText.getText()).isEqualTo("")
+        verify(mockAnimator, times(1)).start()
+
+        // Binding only after animator runs
+        handler.onAnimationEnd(mockAnimator)
+        assertThat(titleText.getText()).isEqualTo(TITLE)
+        assertThat(artistText.getText()).isEqualTo(ARTIST)
+        assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(collapsedSet.getVisibility(explicitIndicator.id))
+            .isEqualTo(ConstraintSet.VISIBLE)
+
+        // Rebinding should not trigger animation
+        player.bindPlayer(mediaData, PACKAGE)
+        verify(mockAnimator, times(3)).start()
+    }
+
+    @Test
     fun bindTextInterrupted() {
         val data0 = mediaData.copy(artist = "ARTIST_0")
         val data1 = mediaData.copy(artist = "ARTIST_1")
@@ -2020,6 +2081,114 @@
     }
 
     @Test
+    fun bindRecommendation_setAfterExecutors() {
+        fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
+        whenever(recommendationViewHolder.mediaAppIcons)
+            .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+        whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
+        whenever(recommendationViewHolder.mediaCoverItems)
+            .thenReturn(listOf(coverItem, coverItem, coverItem))
+        whenever(recommendationViewHolder.mediaProgressBars)
+            .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+        whenever(recommendationViewHolder.mediaSubtitles)
+            .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
+
+        val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bmp)
+        canvas.drawColor(Color.RED)
+        val albumArt = Icon.createWithBitmap(bmp)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("subtitle1")
+                            .setIcon(albumArt)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("subtitle1")
+                            .setIcon(albumArt)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "title3")
+                            .setSubtitle("subtitle1")
+                            .setIcon(albumArt)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(data)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+
+        verify(recCardTitle).setTextColor(any<Int>())
+        verify(recAppIconItem, times(3)).setImageDrawable(any(Drawable::class.java))
+        verify(coverItem, times(3)).setImageDrawable(any(Drawable::class.java))
+    }
+
+    @Test
+    fun bindRecommendationWithProgressBars() {
+        fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
+        whenever(recommendationViewHolder.mediaAppIcons)
+            .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+        whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
+        whenever(recommendationViewHolder.mediaCoverItems)
+            .thenReturn(listOf(coverItem, coverItem, coverItem))
+        whenever(recommendationViewHolder.mediaProgressBars)
+            .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+        whenever(recommendationViewHolder.mediaSubtitles)
+            .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
+
+        val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bmp)
+        canvas.drawColor(Color.RED)
+        val albumArt = Icon.createWithBitmap(bmp)
+        val bundle =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+                )
+                putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5)
+            }
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("subtitle1")
+                            .setIcon(albumArt)
+                            .setExtras(bundle)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("subtitle1")
+                            .setIcon(albumArt)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "title3")
+                            .setSubtitle("subtitle1")
+                            .setIcon(albumArt)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(data)
+
+        verify(recProgressBar1).setProgress(50)
+        verify(recProgressBar1).visibility = View.VISIBLE
+        verify(recProgressBar2).visibility = View.GONE
+        verify(recProgressBar3).visibility = View.GONE
+        verify(recSubtitleMock1).visibility = View.GONE
+        verify(recSubtitleMock2).visibility = View.VISIBLE
+        verify(recSubtitleMock3).visibility = View.VISIBLE
+    }
+
+    @Test
     fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
         fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
         val semanticActions =
@@ -2083,6 +2252,27 @@
         assertThat(player.mRipplesFinishedListener).isNull()
     }
 
+    @Test
+    fun playTurbulenceNoise_finishesAfterDuration() {
+        fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
+        fakeFeatureFlag.set(Flags.UMO_TURBULENCE_NOISE, true)
+
+        player.attachPlayer(viewHolder)
+
+        mainExecutor.execute {
+            player.mRipplesFinishedListener.onRipplesFinish()
+
+            assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE)
+
+            clock.advanceTime(
+                MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION +
+                    TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong()
+            )
+
+            assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
+        }
+    }
+
     private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
         withArgCaptor {
             verify(seekBarViewModel).setScrubbingChangeListener(capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index 920801f..a579518 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.dream.MediaDreamComplication
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
@@ -76,6 +77,7 @@
     @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
     @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
     @Mock private lateinit var keyguardViewController: KeyguardViewController
+    @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
     @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
     @Captor
@@ -110,6 +112,7 @@
                 keyguardStateController,
                 bypassController,
                 mediaCarouselController,
+                mediaDataManager,
                 keyguardViewController,
                 dreamOverlayStateController,
                 configurationController,
@@ -125,6 +128,7 @@
         setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
         setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        whenever(mediaDataManager.hasActiveMedia()).thenReturn(true)
         whenever(mediaCarouselController.mediaCarouselScrollHandler)
             .thenReturn(mediaCarouselScrollHandler)
         val observer = wakefullnessObserver.value
@@ -357,17 +361,31 @@
     }
 
     @Test
-    fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() {
+    fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() {
         goToLockscreen()
         enterGuidedTransformation()
         whenever(lockHost.visible).thenReturn(false)
         whenever(qsHost.visible).thenReturn(true)
         whenever(qqsHost.visible).thenReturn(true)
+        whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
 
         assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
     }
 
     @Test
+    fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() {
+        // To keep the appearing behavior, we need to be in a guided transition
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(false)
+        whenever(qsHost.visible).thenReturn(true)
+        whenever(qqsHost.visible).thenReturn(true)
+        whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
+
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
+    }
+
+    @Test
     fun testDream() {
         goToDream()
         setMediaDreamComplicationEnabled(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 35b0eb6..af91cdb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -22,13 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionViewState
@@ -39,6 +33,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.floatThat
 import org.mockito.Mock
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -60,9 +55,11 @@
     @Mock private lateinit var controlWidgetState: WidgetState
     @Mock private lateinit var bgWidgetState: WidgetState
     @Mock private lateinit var mediaTitleWidgetState: WidgetState
+    @Mock private lateinit var mediaSubTitleWidgetState: WidgetState
     @Mock private lateinit var mediaContainerWidgetState: WidgetState
+    @Mock private lateinit var mediaFlags: MediaFlags
 
-    val delta = 0.0001F
+    val delta = 0.1F
 
     private lateinit var mediaViewController: MediaViewController
 
@@ -70,16 +67,23 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         mediaViewController =
-            MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+            MediaViewController(
+                context,
+                configurationController,
+                mediaHostStatesManager,
+                logger,
+                mediaFlags,
+            )
     }
 
     @Test
     fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
         mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
-        player.measureState = TransitionViewState().apply {
-            this.height = 100
-            this.measureHeight = 100
-        }
+        player.measureState =
+            TransitionViewState().apply {
+                this.height = 100
+                this.measureHeight = 100
+            }
         mediaHostStateHolder.expansion = 1f
         val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
         val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
@@ -128,30 +132,20 @@
                     R.id.header_artist to detailWidgetState
                 )
             )
-
-        val detailSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
-        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val detailSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
-        mediaViewController.squishViewState(mockViewState, detailSquishEnd)
-        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-
-        val controlSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+        whenever(mockCopiedState.measureHeight).thenReturn(200)
+        // detail widgets occupy [90, 100]
+        whenever(detailWidgetState.y).thenReturn(90F)
+        whenever(detailWidgetState.height).thenReturn(10)
+        // control widgets occupy [150, 170]
+        whenever(controlWidgetState.y).thenReturn(150F)
+        whenever(controlWidgetState.height).thenReturn(20)
+        // in current beizer, when the progress reach 0.38, the result will be 0.5
+        mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
         verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val controlSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
-        mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+        mediaViewController.squishViewState(mockViewState, 200F / 200F)
         verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+        verify(detailWidgetState, times(2)).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
     }
 
     @Test
@@ -161,36 +155,33 @@
             .thenReturn(
                 mutableMapOf(
                     R.id.media_title1 to mediaTitleWidgetState,
+                    R.id.media_subtitle1 to mediaSubTitleWidgetState,
                     R.id.media_cover1_container to mediaContainerWidgetState
                 )
             )
+        whenever(mockCopiedState.measureHeight).thenReturn(360)
+        // media container widgets occupy [20, 300]
+        whenever(mediaContainerWidgetState.y).thenReturn(20F)
+        whenever(mediaContainerWidgetState.height).thenReturn(280)
+        // media title widgets occupy [320, 330]
+        whenever(mediaTitleWidgetState.y).thenReturn(320F)
+        whenever(mediaTitleWidgetState.height).thenReturn(10)
+        // media subtitle widgets occupy [340, 350]
+        whenever(mediaSubTitleWidgetState.y).thenReturn(340F)
+        whenever(mediaSubTitleWidgetState.height).thenReturn(10)
 
-        val containerSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+        // in current beizer, when the progress reach 0.38, the result will be 0.5
+        mediaViewController.squishViewState(mockViewState, 307.6F / 360F)
         verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val containerSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+        mediaViewController.squishViewState(mockViewState, 320F / 360F)
         verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-
-        val titleSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+        // media title and media subtitle are in same widget group, should be calculate together and
+        // have same alpha
+        mediaViewController.squishViewState(mockViewState, 353.8F / 360F)
         verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val titleSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+        verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+        mediaViewController.squishViewState(mockViewState, 360F / 360F)
         verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+        verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 7c3c9d2..8fd15c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -29,6 +29,7 @@
 import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.widget.LinearLayout;
+import android.widget.SeekBar;
 
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.filters.SmallTest;
@@ -43,6 +44,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -57,6 +60,7 @@
     private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
     private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
     private static final String TEST_SESSION_NAME = "test_session_name";
+
     private static final int TEST_MAX_VOLUME = 20;
     private static final int TEST_CURRENT_VOLUME = 10;
 
@@ -69,6 +73,8 @@
     private IconCompat mIconCompat = mock(IconCompat.class);
     private View mDialogLaunchView = mock(View.class);
 
+    @Captor
+    private ArgumentCaptor<SeekBar.OnSeekBarChangeListener> mOnSeekBarChangeListenerCaptor;
     private MediaOutputAdapter mMediaOutputAdapter;
     private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
@@ -78,6 +84,7 @@
     @Before
     public void setUp() {
         when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(false);
+        when(mMediaOutputController.isSubStatusSupported()).thenReturn(false);
         when(mMediaOutputController.getMediaItemList()).thenReturn(mMediaItems);
         when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
         when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
@@ -348,6 +355,24 @@
     }
 
     @Test
+    public void onBindViewHolder_dragSeekbar_setsVolume() {
+        mOnSeekBarChangeListenerCaptor = ArgumentCaptor.forClass(
+                SeekBar.OnSeekBarChangeListener.class);
+        MediaOutputSeekbar mSpySeekbar = spy(mViewHolder.mSeekBar);
+        mViewHolder.mSeekBar = mSpySeekbar;
+        when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+        when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_MAX_VOLUME);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        verify(mViewHolder.mSeekBar).setOnSeekBarChangeListener(
+                mOnSeekBarChangeListenerCaptor.capture());
+
+        mOnSeekBarChangeListenerCaptor.getValue().onStopTrackingTouch(mViewHolder.mSeekBar);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+        verify(mMediaOutputController).logInteractionAdjustVolume(mMediaDevice1);
+    }
+
+    @Test
     public void onBindViewHolder_bindSelectableDevice_verifyView() {
         List<MediaDevice> selectableDevices = new ArrayList<>();
         selectableDevices.add(mMediaDevice2);
@@ -404,6 +429,24 @@
     }
 
     @Test
+    public void subStatusSupported_onBindViewHolder_bindFailedStateDevice_verifyView() {
+        String deviceStatus = "";
+        when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
+        when(mMediaDevice2.hasDisabledReason()).thenReturn(true);
+        when(mMediaDevice2.getDisableReason()).thenReturn(-1);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(deviceStatus);
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+    }
+
+    @Test
     public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
         when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice1.getState()).thenReturn(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 094d69a..9a0bd9e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -243,6 +243,13 @@
     }
 
     @Test
+    public void dismissDialog_closesDialogByBroadcastSender() {
+        mMediaOutputBaseDialogImpl.dismissDialog();
+
+        verify(mBroadcastSender).closeSystemDialogs();
+    }
+
+    @Test
     public void whenBroadcasting_verifyLeBroadcastServiceCallBackIsRegisteredAndUnregistered() {
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
                 mLocalBluetoothLeBroadcast);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index b16a39f..7c36e46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -93,6 +93,11 @@
     private static final String TEST_SONG = "test_song";
     private static final String TEST_SESSION_ID = "test_session_id";
     private static final String TEST_SESSION_NAME = "test_session_name";
+    private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+    private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
+            ActivityLaunchAnimator.Controller.class);
+    private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
+            NearbyMediaDevicesManager.class);
     // Mock
     private MediaController mMediaController = mock(MediaController.class);
     private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
@@ -102,8 +107,6 @@
     private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
     private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
     private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
-    private MediaItem mMediaItem1 = mock(MediaItem.class);
-    private MediaItem mMediaItem2 = mock(MediaItem.class);
     private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class);
     private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class);
     private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
@@ -113,12 +116,7 @@
     private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
     private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
-    private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
     private FeatureFlags mFlags = mock(FeatureFlags.class);
-    private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
-            ActivityLaunchAnimator.Controller.class);
-    private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
-            NearbyMediaDevicesManager.class);
     private View mDialogLaunchView = mock(View.class);
     private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class);
 
@@ -127,7 +125,6 @@
     private LocalMediaManager mLocalMediaManager;
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
-    private List<MediaItem> mMediaItemList = new ArrayList<>();
     private List<NearbyDevice> mNearbyDevices = new ArrayList<>();
     private MediaDescription mMediaDescription;
     private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
@@ -149,7 +146,9 @@
                 Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
                 mKeyguardManager, mFlags);
         when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(false);
+        when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(false);
         mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
+        when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
         builder.setTitle(TEST_SONG);
@@ -160,16 +159,12 @@
         when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
         mMediaDevices.add(mMediaDevice1);
         mMediaDevices.add(mMediaDevice2);
-        when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1));
-        when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2));
-        mMediaItemList.add(mMediaItem1);
-        mMediaItemList.add(mMediaItem2);
 
 
         when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID);
-        when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE);
+        when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR);
         when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID);
-        when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR);
+        when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE);
         mNearbyDevices.add(mNearbyDevice1);
         mNearbyDevices.add(mNearbyDevice2);
     }
@@ -257,6 +252,13 @@
     }
 
     @Test
+    public void tryToLaunchMediaApplication_nullIntent_skip() {
+        mMediaOutputController.tryToLaunchMediaApplication();
+
+        verify(mCb, never()).dismissDialog();
+    }
+
+    @Test
     public void onDevicesUpdated_unregistersNearbyDevicesCallback() throws RemoteException {
         mMediaOutputController.start(mCb);
 
@@ -274,8 +276,20 @@
         mMediaOutputController.onDevicesUpdated(mNearbyDevices);
         mMediaOutputController.onDeviceListUpdate(mMediaDevices);
 
-        verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_CLOSE);
-        verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_FAR);
+        verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR);
+        verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE);
+    }
+
+    @Test
+    public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation()
+            throws RemoteException {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDevicesUpdated(mNearbyDevices);
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID);
     }
 
     @Test
@@ -292,6 +306,22 @@
     }
 
     @Test
+    public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation()
+            throws RemoteException {
+        when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
+        when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(true);
+        when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDevicesUpdated(mNearbyDevices);
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        verify(mMediaDevice1, never()).setRangeZone(anyInt());
+        verify(mMediaDevice2, never()).setRangeZone(anyInt());
+    }
+
+    @Test
     public void advanced_onDeviceListUpdate_verifyDeviceListCallback() {
         when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
         mMediaOutputController.start(mCb);
@@ -307,6 +337,35 @@
 
         assertThat(devices.containsAll(mMediaDevices)).isTrue();
         assertThat(devices.size()).isEqualTo(mMediaDevices.size());
+        assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo(
+                mMediaDevices.size() + 2);
+        verify(mCb).onDeviceListChanged();
+    }
+
+    @Test
+    public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() {
+        when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
+        when(mMediaDevice1.isSuggestedDevice()).thenReturn(true);
+        when(mMediaDevice2.isSuggestedDevice()).thenReturn(false);
+
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.getMediaItemList().clear();
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        final List<MediaDevice> devices = new ArrayList<>();
+        int dividerSize = 0;
+        for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+            if (item.getMediaDevice().isPresent()) {
+                devices.add(item.getMediaDevice().get());
+            }
+            if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) {
+                dividerSize++;
+            }
+        }
+
+        assertThat(devices.containsAll(mMediaDevices)).isTrue();
+        assertThat(devices.size()).isEqualTo(mMediaDevices.size());
+        assertThat(dividerSize).isEqualTo(2);
         verify(mCb).onDeviceListChanged();
     }
 
@@ -463,6 +522,17 @@
     }
 
     @Test
+    public void logInteractionAdjustVolume_triggersFromMetricLogger() {
+        MediaOutputMetricLogger spyMediaOutputMetricLogger = spy(
+                mMediaOutputController.mMetricLogger);
+        mMediaOutputController.mMetricLogger = spyMediaOutputMetricLogger;
+
+        mMediaOutputController.logInteractionAdjustVolume(mMediaDevice1);
+
+        verify(spyMediaOutputMetricLogger).logInteractionAdjustVolume(mMediaDevice1);
+    }
+
+    @Test
     public void getSessionVolumeMax_triggersFromLocalMediaManager() {
         mMediaOutputController.getSessionVolumeMax();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtilsTest.kt
similarity index 70%
copy from packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtilsTest.kt
index 0e7bf8d..8da1c64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtilsTest.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -31,16 +30,15 @@
 import org.mockito.Mockito.mock
 
 @SmallTest
-class MediaTttLoggerTest : SysuiTestCase() {
+class MediaTttLoggerUtilsTest : SysuiTestCase() {
 
     private lateinit var buffer: LogBuffer
-    private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
 
     @Before
-    fun setUp () {
-        buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
-            .create("buffer", 10)
-        logger = MediaTttLogger(DEVICE_TYPE_TAG, buffer)
+    fun setUp() {
+        buffer =
+            LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+                .create("buffer", 10)
     }
 
     @Test
@@ -49,35 +47,33 @@
         val id = "test id"
         val packageName = "this.is.a.package"
 
-        logger.logStateChange(stateName, id, packageName)
+        MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, id, packageName)
 
         val actualString = getStringFromBuffer()
-        assertThat(actualString).contains(DEVICE_TYPE_TAG)
+        assertThat(actualString).contains(TAG)
         assertThat(actualString).contains(stateName)
         assertThat(actualString).contains(id)
         assertThat(actualString).contains(packageName)
     }
 
     @Test
-    fun logPackageNotFound_bufferHasPackageName() {
-        val packageName = "this.is.a.package"
-
-        logger.logPackageNotFound(packageName)
+    fun logStateChangeError_hasState() {
+        MediaTttLoggerUtils.logStateChangeError(buffer, TAG, 3456)
 
         val actualString = getStringFromBuffer()
-        assertThat(actualString).contains(packageName)
+        assertThat(actualString).contains(TAG)
+        assertThat(actualString).contains("3456")
     }
 
     @Test
-    fun logRemovalBypass_bufferHasReasons() {
-        val removalReason = "fakeRemovalReason"
-        val bypassReason = "fakeBypassReason"
+    fun logPackageNotFound_bufferHasPackageName() {
+        val packageName = "this.is.a.package"
 
-        logger.logRemovalBypass(removalReason, bypassReason)
+        MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
 
         val actualString = getStringFromBuffer()
-        assertThat(actualString).contains(removalReason)
-        assertThat(actualString).contains(bypassReason)
+        assertThat(actualString).contains(TAG)
+        assertThat(actualString).contains(packageName)
     }
 
     private fun getStringFromBuffer(): String {
@@ -87,4 +83,4 @@
     }
 }
 
-private const val DEVICE_TYPE_TAG = "TEST TYPE"
+private const val TAG = "TEST TAG"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 561867f..4fc9ca7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -41,7 +40,6 @@
     private lateinit var appIconFromPackageName: Drawable
     @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var applicationInfo: ApplicationInfo
-    @Mock private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
 
     @Before
     fun setUp() {
@@ -68,7 +66,12 @@
     @Test
     fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
         val iconInfo =
-            MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
+            MediaTttUtils.getIconInfoFromPackageName(
+                context,
+                appPackageName = null,
+                isReceiver = false,
+            ) {
+            }
 
         assertThat(iconInfo.isAppIcon).isFalse()
         assertThat(iconInfo.contentDescription.loadContentDescription(context))
@@ -77,8 +80,47 @@
     }
 
     @Test
+    fun getIconInfoFromPackageName_nullPackageName_isReceiver_returnsDefault() {
+        val iconInfo =
+            MediaTttUtils.getIconInfoFromPackageName(
+                context,
+                appPackageName = null,
+                isReceiver = true,
+            ) {
+            }
+
+        assertThat(iconInfo.isAppIcon).isFalse()
+        assertThat(iconInfo.contentDescription.loadContentDescription(context))
+            .isEqualTo(
+                context.getString(R.string.media_transfer_receiver_content_description_unknown_app)
+            )
+        assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Resource(R.drawable.ic_cast))
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_nullPackageName_exceptionFnNotTriggered() {
+        var exceptionTriggered = false
+
+        MediaTttUtils.getIconInfoFromPackageName(
+            context,
+            appPackageName = null,
+            isReceiver = false,
+        ) {
+            exceptionTriggered = true
+        }
+
+        assertThat(exceptionTriggered).isFalse()
+    }
+
+    @Test
     fun getIconInfoFromPackageName_invalidPackageName_returnsDefault() {
-        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, "fakePackageName", logger)
+        val iconInfo =
+            MediaTttUtils.getIconInfoFromPackageName(
+                context,
+                appPackageName = "fakePackageName",
+                isReceiver = false,
+            ) {
+            }
 
         assertThat(iconInfo.isAppIcon).isFalse()
         assertThat(iconInfo.contentDescription.loadContentDescription(context))
@@ -87,8 +129,58 @@
     }
 
     @Test
+    fun getIconInfoFromPackageName_invalidPackageName_isReceiver_returnsDefault() {
+        val iconInfo =
+            MediaTttUtils.getIconInfoFromPackageName(
+                context,
+                appPackageName = "fakePackageName",
+                isReceiver = true,
+            ) {
+            }
+
+        assertThat(iconInfo.isAppIcon).isFalse()
+        assertThat(iconInfo.contentDescription.loadContentDescription(context))
+            .isEqualTo(
+                context.getString(R.string.media_transfer_receiver_content_description_unknown_app)
+            )
+        assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Resource(R.drawable.ic_cast))
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_invalidPackageName_exceptionFnTriggered() {
+        var exceptionTriggered = false
+
+        MediaTttUtils.getIconInfoFromPackageName(
+            context,
+            appPackageName = "fakePackageName",
+            isReceiver = false
+        ) { exceptionTriggered = true }
+
+        assertThat(exceptionTriggered).isTrue()
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_invalidPackageName_isReceiver_exceptionFnTriggered() {
+        var exceptionTriggered = false
+
+        MediaTttUtils.getIconInfoFromPackageName(
+            context,
+            appPackageName = "fakePackageName",
+            isReceiver = true
+        ) { exceptionTriggered = true }
+
+        assertThat(exceptionTriggered).isTrue()
+    }
+
+    @Test
     fun getIconInfoFromPackageName_validPackageName_returnsAppInfo() {
-        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME, logger)
+        val iconInfo =
+            MediaTttUtils.getIconInfoFromPackageName(
+                context,
+                PACKAGE_NAME,
+                isReceiver = false,
+            ) {
+            }
 
         assertThat(iconInfo.isAppIcon).isTrue()
         assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Loaded(appIconFromPackageName))
@@ -96,6 +188,49 @@
     }
 
     @Test
+    fun getIconInfoFromPackageName_validPackageName_isReceiver_returnsAppInfo() {
+        val iconInfo =
+            MediaTttUtils.getIconInfoFromPackageName(
+                context,
+                PACKAGE_NAME,
+                isReceiver = true,
+            ) {
+            }
+
+        assertThat(iconInfo.isAppIcon).isTrue()
+        assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Loaded(appIconFromPackageName))
+        assertThat(iconInfo.contentDescription.loadContentDescription(context))
+            .isEqualTo(
+                context.getString(
+                    R.string.media_transfer_receiver_content_description_with_app_name,
+                    APP_NAME
+                )
+            )
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_validPackageName_exceptionFnNotTriggered() {
+        var exceptionTriggered = false
+
+        MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME, isReceiver = false) {
+            exceptionTriggered = true
+        }
+
+        assertThat(exceptionTriggered).isFalse()
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_validPackageName_isReceiver_exceptionFnNotTriggered() {
+        var exceptionTriggered = false
+
+        MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME, isReceiver = true) {
+            exceptionTriggered = true
+        }
+
+        assertThat(exceptionTriggered).isFalse()
+    }
+
+    @Test
     fun iconInfo_toTintedIcon_loaded() {
         val contentDescription = ContentDescription.Loaded("test")
         val drawable = context.getDrawable(R.drawable.ic_cake)!!
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index 9c4e849..bd042c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -24,7 +24,6 @@
 import android.view.accessibility.AccessibilityManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -35,7 +34,7 @@
 class FakeMediaTttChipControllerReceiver(
     commandQueue: CommandQueue,
     context: Context,
-    logger: MediaTttLogger<ChipReceiverInfo>,
+    logger: MediaTttReceiverLogger,
     windowManager: WindowManager,
     mainExecutor: DelayableExecutor,
     accessibilityManager: AccessibilityManager,
@@ -48,6 +47,7 @@
     viewUtil: ViewUtil,
     wakeLockBuilder: WakeLock.Builder,
     systemClock: SystemClock,
+    rippleController: MediaTttReceiverRippleController,
 ) :
     MediaTttChipControllerReceiver(
         commandQueue,
@@ -65,6 +65,7 @@
         viewUtil,
         wakeLockBuilder,
         systemClock,
+        rippleController,
     ) {
     override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index cefc742..19dd2f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -68,7 +67,7 @@
     @Mock
     private lateinit var applicationInfo: ApplicationInfo
     @Mock
-    private lateinit var logger: MediaTttLogger<ChipReceiverInfo>
+    private lateinit var logger: MediaTttReceiverLogger
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
@@ -85,6 +84,8 @@
     private lateinit var windowManager: WindowManager
     @Mock
     private lateinit var commandQueue: CommandQueue
+    @Mock
+    private lateinit var rippleController: MediaTttReceiverRippleController
     private lateinit var commandQueueCallback: CommandQueue.Callbacks
     private lateinit var fakeAppIconDrawable: Drawable
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
@@ -134,6 +135,7 @@
             viewUtil,
             fakeWakeLockBuilder,
             fakeClock,
+            rippleController,
         )
         controllerReceiver.start()
 
@@ -163,6 +165,7 @@
             viewUtil,
             fakeWakeLockBuilder,
             fakeClock,
+            rippleController,
         )
         controllerReceiver.start()
 
@@ -351,7 +354,11 @@
 
         val view = getChipView()
         assertThat(view.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(view.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(view.getAppIconView().contentDescription)
+            .isEqualTo(context.getString(
+                R.string.media_transfer_receiver_content_description_with_app_name,
+                APP_NAME,
+            ))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLoggerTest.kt
similarity index 71%
rename from packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLoggerTest.kt
index 0e7bf8d..95df484 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLoggerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media.taptotransfer.common
+package com.android.systemui.media.taptotransfer.receiver
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -22,7 +22,6 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -31,16 +30,17 @@
 import org.mockito.Mockito.mock
 
 @SmallTest
-class MediaTttLoggerTest : SysuiTestCase() {
+class MediaTttReceiverLoggerTest : SysuiTestCase() {
 
     private lateinit var buffer: LogBuffer
-    private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
+    private lateinit var logger: MediaTttReceiverLogger
 
     @Before
-    fun setUp () {
-        buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
-            .create("buffer", 10)
-        logger = MediaTttLogger(DEVICE_TYPE_TAG, buffer)
+    fun setUp() {
+        buffer =
+            LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+                .create("buffer", 10)
+        logger = MediaTttReceiverLogger(buffer)
     }
 
     @Test
@@ -52,13 +52,20 @@
         logger.logStateChange(stateName, id, packageName)
 
         val actualString = getStringFromBuffer()
-        assertThat(actualString).contains(DEVICE_TYPE_TAG)
         assertThat(actualString).contains(stateName)
         assertThat(actualString).contains(id)
         assertThat(actualString).contains(packageName)
     }
 
     @Test
+    fun logStateChangeError_hasState() {
+        logger.logStateChangeError(3456)
+
+        val actualString = getStringFromBuffer()
+        assertThat(actualString).contains("3456")
+    }
+
+    @Test
     fun logPackageNotFound_bufferHasPackageName() {
         val packageName = "this.is.a.package"
 
@@ -68,23 +75,9 @@
         assertThat(actualString).contains(packageName)
     }
 
-    @Test
-    fun logRemovalBypass_bufferHasReasons() {
-        val removalReason = "fakeRemovalReason"
-        val bypassReason = "fakeBypassReason"
-
-        logger.logRemovalBypass(removalReason, bypassReason)
-
-        val actualString = getStringFromBuffer()
-        assertThat(actualString).contains(removalReason)
-        assertThat(actualString).contains(bypassReason)
-    }
-
     private fun getStringFromBuffer(): String {
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
         return stringWriter.toString()
     }
 }
-
-private const val DEVICE_TYPE_TAG = "TEST TYPE"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 4cc12c7..db890f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -40,18 +40,21 @@
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
+import com.android.systemui.temporarydisplay.chipbar.ChipbarAnimator
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
-import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
-import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.android.systemui.util.wakelock.WakeLockFake
@@ -61,6 +64,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
+import org.mockito.Mockito.atLeast
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -86,13 +90,14 @@
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var falsingCollector: FalsingCollector
     @Mock private lateinit var chipbarLogger: ChipbarLogger
-    @Mock private lateinit var logger: MediaTttLogger<ChipbarInfo>
+    @Mock private lateinit var logger: MediaTttSenderLogger
     @Mock private lateinit var mediaTttFlags: MediaTttFlags
     @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var powerManager: PowerManager
     @Mock private lateinit var viewUtil: ViewUtil
     @Mock private lateinit var windowManager: WindowManager
     @Mock private lateinit var vibratorHelper: VibratorHelper
+    @Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler
     private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
     private lateinit var fakeWakeLock: WakeLockFake
     private lateinit var chipbarCoordinator: ChipbarCoordinator
@@ -132,7 +137,7 @@
         uiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
         chipbarCoordinator =
-            FakeChipbarCoordinator(
+            ChipbarCoordinator(
                 context,
                 chipbarLogger,
                 windowManager,
@@ -141,8 +146,10 @@
                 configurationController,
                 dumpManager,
                 powerManager,
+                ChipbarAnimator(),
                 falsingManager,
                 falsingCollector,
+                swipeHandler,
                 viewUtil,
                 vibratorHelper,
                 fakeWakeLockBuilder,
@@ -155,15 +162,14 @@
                 chipbarCoordinator,
                 commandQueue,
                 context,
+                dumpManager,
                 logger,
                 mediaTttFlags,
                 uiEventLogger,
             )
         underTest.start()
 
-        val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
-        verify(commandQueue).addCallback(callbackCaptor.capture())
-        commandQueueCallback = callbackCaptor.value!!
+        setCommandQueueCallback()
     }
 
     @Test
@@ -175,6 +181,7 @@
                 chipbarCoordinator,
                 commandQueue,
                 context,
+                dumpManager,
                 logger,
                 mediaTttFlags,
                 uiEventLogger,
@@ -206,6 +213,21 @@
     }
 
     @Test
+    fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            routeInfoWithBlankDeviceName,
+            null,
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .contains(context.getString(R.string.media_ttt_default_device_type))
+        assertThat(chipbarView.getChipText())
+            .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+    }
+
+    @Test
     fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() {
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
@@ -248,6 +270,21 @@
     }
 
     @Test
+    fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            routeInfoWithBlankDeviceName,
+            null,
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .contains(context.getString(R.string.media_ttt_default_device_type))
+        assertThat(chipbarView.getChipText())
+            .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+    }
+
+    @Test
     fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() {
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
@@ -506,6 +543,7 @@
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         verify(windowManager).removeView(viewCaptor.value)
+        verify(logger).logStateMapRemoval(eq(DEFAULT_ID), any())
     }
 
     @Test
@@ -586,7 +624,7 @@
     }
 
     @Test
-    fun commandQueueCallback_receiverSucceededThenReceiverTriggered_invalidTransitionLogged() {
+    fun commandQueueCallback_receiverSucceededThenThisDeviceSucceeded_invalidTransitionLogged() {
         displayReceiverTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
@@ -596,7 +634,7 @@
         reset(windowManager)
 
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
-            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
             null
         )
@@ -606,7 +644,7 @@
     }
 
     @Test
-    fun commandQueueCallback_thisDeviceSucceededThenThisDeviceTriggered_invalidTransitionLogged() {
+    fun commandQueueCallback_thisDeviceSucceededThenReceiverSucceeded_invalidTransitionLogged() {
         displayThisDeviceTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
@@ -616,7 +654,7 @@
         reset(windowManager)
 
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
-            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
             null
         )
@@ -705,6 +743,99 @@
         verify(windowManager, never()).addView(any(), any())
     }
 
+    /** Regression test for b/266217596. */
+    @Test
+    fun toReceiver_triggeredThenFar_thenSucceeded_updatesToSucceeded() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            routeInfo,
+            null,
+        )
+
+        // WHEN a FAR command comes in
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null,
+        )
+
+        // THEN it is ignored and the chipbar is stilled displayed
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        verify(windowManager, never()).removeView(any())
+
+        // WHEN a SUCCEEDED command comes in
+        val succeededRouteInfo =
+            MediaRoute2Info.Builder(DEFAULT_ID, "Tablet Succeeded")
+                .addFeature("feature")
+                .setClientPackageName(PACKAGE_NAME)
+                .build()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            succeededRouteInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        // THEN it is *not* marked as an invalid transition and the chipbar updates to the succeeded
+        // state. (The "invalid transition" would be FAR => SUCCEEDED.)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(
+                ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText(
+                    "Tablet Succeeded"
+                )
+            )
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+    }
+
+    /** Regression test for b/266217596. */
+    @Test
+    fun toThisDevice_triggeredThenFar_thenSucceeded_updatesToSucceeded() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            routeInfo,
+            null,
+        )
+
+        // WHEN a FAR command comes in
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null,
+        )
+
+        // THEN it is ignored and the chipbar is stilled displayed
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        verify(windowManager, never()).removeView(any())
+
+        // WHEN a SUCCEEDED command comes in
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        // THEN it is *not* marked as an invalid transition and the chipbar updates to the succeeded
+        // state. (The "invalid transition" would be FAR => SUCCEEDED.)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(
+                ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText(
+                    "Tablet Succeeded"
+                )
+            )
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+    }
+
     @Test
     fun receivesNewStateFromCommandQueue_isLogged() {
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
@@ -890,6 +1021,341 @@
         verify(windowManager).removeView(any())
     }
 
+    @Test
+    fun newState_viewListenerRegistered() {
+        val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+        underTest =
+            MediaTttSenderCoordinator(
+                mockChipbarCoordinator,
+                commandQueue,
+                context,
+                dumpManager,
+                logger,
+                mediaTttFlags,
+                uiEventLogger,
+            )
+        underTest.start()
+        // Re-set the command queue callback since we've created a new [MediaTttSenderCoordinator]
+        // with a new callback.
+        setCommandQueueCallback()
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null,
+        )
+
+        verify(mockChipbarCoordinator).registerListener(any())
+    }
+
+    @Test
+    fun onInfoPermanentlyRemoved_viewListenerUnregistered() {
+        val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+        underTest =
+            MediaTttSenderCoordinator(
+                mockChipbarCoordinator,
+                commandQueue,
+                context,
+                dumpManager,
+                logger,
+                mediaTttFlags,
+                uiEventLogger,
+            )
+        underTest.start()
+        setCommandQueueCallback()
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null,
+        )
+
+        val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+        verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
+
+        // WHEN the listener is notified that the view has been removed
+        listenerCaptor.value.onInfoPermanentlyRemoved(DEFAULT_ID, "reason")
+
+        // THEN the media coordinator unregisters the listener
+        verify(logger).logStateMapRemoval(DEFAULT_ID, "reason")
+        verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
+    }
+
+    @Test
+    fun onInfoPermanentlyRemoved_wrongId_viewListenerNotUnregistered() {
+        val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+        underTest =
+            MediaTttSenderCoordinator(
+                mockChipbarCoordinator,
+                commandQueue,
+                context,
+                dumpManager,
+                logger,
+                mediaTttFlags,
+                uiEventLogger,
+            )
+        underTest.start()
+        setCommandQueueCallback()
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null,
+        )
+
+        val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+        verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
+
+        // WHEN the listener is notified that a different view has been removed
+        listenerCaptor.value.onInfoPermanentlyRemoved("differentViewId", "reason")
+
+        // THEN the media coordinator doesn't unregister the listener
+        verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
+    }
+
+    @Test
+    fun farFromReceiverState_viewListenerUnregistered() {
+        val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+        underTest =
+            MediaTttSenderCoordinator(
+                mockChipbarCoordinator,
+                commandQueue,
+                context,
+                dumpManager,
+                logger,
+                mediaTttFlags,
+                uiEventLogger,
+            )
+        underTest.start()
+        setCommandQueueCallback()
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null,
+        )
+
+        val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+        verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
+
+        // WHEN we go to the FAR_FROM_RECEIVER state
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+
+        // THEN the media coordinator unregisters the listener
+        verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
+    }
+
+    @Test
+    fun statesWithDifferentIds_onInfoPermanentlyRemovedForOneId_viewListenerNotUnregistered() {
+        val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+        underTest =
+            MediaTttSenderCoordinator(
+                mockChipbarCoordinator,
+                commandQueue,
+                context,
+                dumpManager,
+                logger,
+                mediaTttFlags,
+                uiEventLogger,
+            )
+        underTest.start()
+        setCommandQueueCallback()
+
+        // WHEN there are two different media transfers with different IDs
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME)
+                .addFeature("feature")
+                .setClientPackageName(PACKAGE_NAME)
+                .build(),
+            null,
+        )
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            MediaRoute2Info.Builder("route2", OTHER_DEVICE_NAME)
+                .addFeature("feature")
+                .setClientPackageName(PACKAGE_NAME)
+                .build(),
+            null,
+        )
+
+        val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+        verify(mockChipbarCoordinator, atLeast(1)).registerListener(capture(listenerCaptor))
+
+        // THEN one of them is removed
+        listenerCaptor.value.onInfoPermanentlyRemoved("route1", "reason")
+
+        // THEN the media coordinator doesn't unregister the listener (since route2 is still active)
+        verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
+        verify(logger).logStateMapRemoval("route1", "reason")
+    }
+
+    /** Regression test for b/266218672. */
+    @Test
+    fun twoIdsDisplayed_oldIdIsFar_viewStillDisplayed() {
+        // WHEN there are two different media transfers with different IDs
+        val route1 =
+            MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME)
+                .addFeature("feature")
+                .setClientPackageName(PACKAGE_NAME)
+                .build()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            route1,
+            null,
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            MediaRoute2Info.Builder("route2", "Route 2 name")
+                .addFeature("feature")
+                .setClientPackageName(PACKAGE_NAME)
+                .build(),
+            null,
+        )
+        val newView = getChipbarView()
+
+        // WHEN there's a FAR event for the earlier one
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            route1,
+            null,
+        )
+
+        // THEN it's ignored and the more recent one is still displayed
+        assertThat(newView.getChipText())
+            .isEqualTo(
+                ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText("Route 2 name")
+            )
+    }
+
+    /** Regression test for b/266218672. */
+    @Test
+    fun receiverSucceededThenTimedOut_internalStateResetAndCanDisplayAlmostCloseToEnd() {
+        displayReceiverTriggered()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            null,
+        )
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+        verify(windowManager).removeView(any())
+
+        reset(windowManager)
+
+        // WHEN we try to show ALMOST_CLOSE_TO_END
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null,
+        )
+
+        // THEN it succeeds
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText())
+    }
+
+    /** Regression test for b/266218672. */
+    @Test
+    fun receiverSucceededThenTimedOut_internalStateResetAndCanDisplayReceiverTriggered() {
+        displayReceiverTriggered()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            null,
+        )
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+        verify(windowManager).removeView(any())
+
+        reset(windowManager)
+
+        // WHEN we try to show RECEIVER_TRIGGERED
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            routeInfo,
+            null,
+        )
+
+        // THEN it succeeds
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+    }
+
+    /** Regression test for b/266218672. */
+    @Test
+    fun toThisDeviceSucceededThenTimedOut_internalStateResetAndCanDisplayAlmostCloseToStart() {
+        displayThisDeviceTriggered()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            null,
+        )
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+        verify(windowManager).removeView(any())
+
+        reset(windowManager)
+
+        // WHEN we try to show ALMOST_CLOSE_TO_START
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            routeInfo,
+            null,
+        )
+
+        // THEN it succeeds
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+    }
+
+    /** Regression test for b/266218672. */
+    @Test
+    fun toThisDeviceSucceededThenTimedOut_internalStateResetAndCanDisplayThisDeviceTriggered() {
+        displayThisDeviceTriggered()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            null,
+        )
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+        verify(windowManager).removeView(any())
+
+        reset(windowManager)
+
+        // WHEN we try to show THIS_DEVICE_TRIGGERED
+        val newRouteInfo =
+            MediaRoute2Info.Builder(DEFAULT_ID, "New Name")
+                .addFeature("feature")
+                .setClientPackageName(PACKAGE_NAME)
+                .build()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            newRouteInfo,
+            null,
+        )
+
+        // THEN it succeeds
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(
+                ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText("New Name")
+            )
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+    }
+
     private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
@@ -907,8 +1373,10 @@
 
     private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
 
-    private fun ChipStateSender.getExpectedStateText(): String? {
-        return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
+    private fun ChipStateSender.getExpectedStateText(
+        otherDeviceName: String = OTHER_DEVICE_NAME,
+    ): String? {
+        return this.getChipTextString(context, otherDeviceName).loadText(context)
     }
 
     // display receiver triggered state helper method to make sure we start from a valid state
@@ -930,15 +1398,30 @@
             null
         )
     }
+
+    private fun setCommandQueueCallback() {
+        val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+        verify(commandQueue).addCallback(capture(callbackCaptor))
+        commandQueueCallback = callbackCaptor.value
+        reset(commandQueue)
+    }
 }
 
+private const val DEFAULT_ID = "defaultId"
 private const val APP_NAME = "Fake app name"
 private const val OTHER_DEVICE_NAME = "My Tablet"
+private const val BLANK_DEVICE_NAME = " "
 private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
 
 private val routeInfo =
-    MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
+    MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME)
+        .addFeature("feature")
+        .setClientPackageName(PACKAGE_NAME)
+        .build()
+
+private val routeInfoWithBlankDeviceName =
+    MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME)
         .addFeature("feature")
         .setClientPackageName(PACKAGE_NAME)
         .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLoggerTest.kt
similarity index 62%
copy from packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLoggerTest.kt
index 0e7bf8d..0033757 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLoggerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media.taptotransfer.common
+package com.android.systemui.media.taptotransfer.sender
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -22,7 +22,6 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -31,16 +30,17 @@
 import org.mockito.Mockito.mock
 
 @SmallTest
-class MediaTttLoggerTest : SysuiTestCase() {
+class MediaTttSenderLoggerTest : SysuiTestCase() {
 
     private lateinit var buffer: LogBuffer
-    private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
+    private lateinit var logger: MediaTttSenderLogger
 
     @Before
-    fun setUp () {
-        buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
-            .create("buffer", 10)
-        logger = MediaTttLogger(DEVICE_TYPE_TAG, buffer)
+    fun setUp() {
+        buffer =
+            LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+                .create("buffer", 10)
+        logger = MediaTttSenderLogger(buffer)
     }
 
     @Test
@@ -52,13 +52,20 @@
         logger.logStateChange(stateName, id, packageName)
 
         val actualString = getStringFromBuffer()
-        assertThat(actualString).contains(DEVICE_TYPE_TAG)
         assertThat(actualString).contains(stateName)
         assertThat(actualString).contains(id)
         assertThat(actualString).contains(packageName)
     }
 
     @Test
+    fun logStateChangeError_hasState() {
+        logger.logStateChangeError(3456)
+
+        val actualString = getStringFromBuffer()
+        assertThat(actualString).contains("3456")
+    }
+
+    @Test
     fun logPackageNotFound_bufferHasPackageName() {
         val packageName = "this.is.a.package"
 
@@ -80,11 +87,35 @@
         assertThat(actualString).contains(bypassReason)
     }
 
+    @Test
+    fun logStateMap_bufferHasInfo() {
+        val map =
+            mapOf(
+                "123" to ChipStateSender.ALMOST_CLOSE_TO_START_CAST,
+                "456" to ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            )
+
+        logger.logStateMap(map)
+
+        val actualString = getStringFromBuffer()
+        assertThat(actualString).contains("123")
+        assertThat(actualString).contains(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.name)
+        assertThat(actualString).contains("456")
+        assertThat(actualString).contains(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.name)
+    }
+
+    @Test
+    fun logStateMapRemoval_bufferHasInfo() {
+        logger.logStateMapRemoval("456", "testReason")
+
+        val actualString = getStringFromBuffer()
+        assertThat(actualString).contains("456")
+        assertThat(actualString).contains("testReason")
+    }
+
     private fun getStringFromBuffer(): String {
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
         return stringWriter.toString()
     }
 }
-
-private const val DEVICE_TYPE_TAG = "TEST TYPE"
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 19d2d33..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
@@ -1,12 +1,16 @@
 package com.android.systemui.mediaprojection.appselector
 
 import android.content.ComponentName
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
 import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import org.junit.Test
@@ -20,15 +24,25 @@
     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)
 
     private val view: MediaProjectionAppSelectorView = mock()
+    private val featureFlags: FeatureFlags = mock()
 
-    private val controller = MediaProjectionAppSelectorController(
-        taskListProvider,
-        view,
-        scope,
-        appSelectorComponentName
-    )
+    private val controller =
+        MediaProjectionAppSelectorController(
+            taskListProvider,
+            view,
+            featureFlags,
+            hostUserHandle,
+            scope,
+            appSelectorComponentName,
+            callerPackageName
+        )
 
     @Test
     fun initNoRecentTasks_bindsEmptyList() {
@@ -41,72 +55,152 @@
 
     @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(
+                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) {
+        whenever(featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
+            .thenReturn(enabled)
     }
 
     private fun createRecentTask(
         taskId: Int,
-        topActivityComponent: ComponentName? = null
+        topActivityComponent: ComponentName? = null,
+        userId: Int = hostUserHandle.identifier
     ): RecentTask {
         return RecentTask(
             taskId = taskId,
             topActivityComponent = topActivityComponent,
             baseIntentComponent = ComponentName("com", "Test"),
-            userId = 0,
+            userId = userId,
             colorBackground = 0
         )
     }
@@ -116,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/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
new file mode 100644
index 0000000..e8b6f9b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
@@ -0,0 +1,703 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.app.admin.DevicePolicyManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.ArgumentMatchers.any
+
+abstract class BaseScreenCaptureDevicePolicyResolverTest(private val precondition: Preconditions) :
+    SysuiTestCase() {
+
+    abstract class Preconditions(
+        val personalScreenCaptureDisabled: Boolean,
+        val workScreenCaptureDisabled: Boolean,
+        val disallowShareIntoManagedProfile: Boolean
+    )
+
+    protected val devicePolicyManager: DevicePolicyManager = mock()
+    protected val userManager: UserManager = mock()
+
+    protected val personalUserHandle: UserHandle = UserHandle.of(123)
+    protected val workUserHandle: UserHandle = UserHandle.of(456)
+
+    protected val policyResolver =
+        ScreenCaptureDevicePolicyResolver(
+            devicePolicyManager,
+            userManager,
+            personalUserHandle,
+            workUserHandle
+        )
+
+    @Before
+    fun setUp() {
+        setUpPolicies()
+    }
+
+    private fun setUpPolicies() {
+        whenever(
+                devicePolicyManager.getScreenCaptureDisabled(
+                    any(),
+                    eq(personalUserHandle.identifier)
+                )
+            )
+            .thenReturn(precondition.personalScreenCaptureDisabled)
+
+        whenever(devicePolicyManager.getScreenCaptureDisabled(any(), eq(workUserHandle.identifier)))
+            .thenReturn(precondition.workScreenCaptureDisabled)
+
+        whenever(
+                userManager.hasUserRestrictionForUser(
+                    eq(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE),
+                    eq(workUserHandle)
+                )
+            )
+            .thenReturn(precondition.disallowShareIntoManagedProfile)
+    }
+}
+
+@RunWith(Parameterized::class)
+@SmallTest
+class IsAllowedScreenCaptureDevicePolicyResolverTest(
+    private val test: IsScreenCaptureAllowedTestCase
+) : BaseScreenCaptureDevicePolicyResolverTest(test.given) {
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() =
+            listOf(
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false,
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true,
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false,
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true,
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = true,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+                IsScreenCaptureAllowedTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            isTargetInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureAllowed = false,
+                ),
+            )
+    }
+
+    class Preconditions(
+        personalScreenCaptureDisabled: Boolean,
+        workScreenCaptureDisabled: Boolean,
+        disallowShareIntoManagedProfile: Boolean,
+        val isHostInWorkProfile: Boolean,
+        val isTargetInWorkProfile: Boolean,
+    ) :
+        BaseScreenCaptureDevicePolicyResolverTest.Preconditions(
+            personalScreenCaptureDisabled,
+            workScreenCaptureDisabled,
+            disallowShareIntoManagedProfile
+        )
+
+    data class IsScreenCaptureAllowedTestCase(
+        val given: Preconditions,
+        val expectedScreenCaptureAllowed: Boolean
+    ) {
+        override fun toString(): String =
+            "isScreenCaptureAllowed: " +
+                "host[${if (given.isHostInWorkProfile) "work" else "personal"} profile], " +
+                "target[${if (given.isTargetInWorkProfile) "work" else "personal"} profile], " +
+                "personal screen capture disabled = ${given.personalScreenCaptureDisabled}, " +
+                "work screen capture disabled = ${given.workScreenCaptureDisabled}, " +
+                "disallow share into managed profile = ${given.disallowShareIntoManagedProfile}, " +
+                "expected screen capture allowed = $expectedScreenCaptureAllowed"
+    }
+
+    @Test
+    fun test() {
+        val targetAppUserHandle =
+            if (test.given.isTargetInWorkProfile) workUserHandle else personalUserHandle
+        val hostAppUserHandle =
+            if (test.given.isHostInWorkProfile) workUserHandle else personalUserHandle
+
+        val screenCaptureAllowed =
+            policyResolver.isScreenCaptureAllowed(targetAppUserHandle, hostAppUserHandle)
+
+        assertWithMessage("Screen capture policy resolved incorrectly")
+            .that(screenCaptureAllowed)
+            .isEqualTo(test.expectedScreenCaptureAllowed)
+    }
+}
+
+@RunWith(Parameterized::class)
+@SmallTest
+class IsCompletelyNotAllowedScreenCaptureDevicePolicyResolverTest(
+    private val test: IsScreenCaptureCompletelyDisabledTestCase
+) : BaseScreenCaptureDevicePolicyResolverTest(test.given) {
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() =
+            listOf(
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = false,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = false,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = false,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = false,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = false
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                ),
+                IsScreenCaptureCompletelyDisabledTestCase(
+                    given =
+                        Preconditions(
+                            isHostInWorkProfile = true,
+                            personalScreenCaptureDisabled = true,
+                            workScreenCaptureDisabled = true,
+                            disallowShareIntoManagedProfile = true
+                        ),
+                    expectedScreenCaptureCompletelyDisabled = true,
+                )
+            )
+    }
+
+    class Preconditions(
+        personalScreenCaptureDisabled: Boolean,
+        workScreenCaptureDisabled: Boolean,
+        disallowShareIntoManagedProfile: Boolean,
+        val isHostInWorkProfile: Boolean,
+    ) :
+        BaseScreenCaptureDevicePolicyResolverTest.Preconditions(
+            personalScreenCaptureDisabled,
+            workScreenCaptureDisabled,
+            disallowShareIntoManagedProfile
+        )
+
+    data class IsScreenCaptureCompletelyDisabledTestCase(
+        val given: Preconditions,
+        val expectedScreenCaptureCompletelyDisabled: Boolean
+    ) {
+        override fun toString(): String =
+            "isScreenCaptureCompletelyDisabled: " +
+                "host[${if (given.isHostInWorkProfile) "work" else "personal"} profile], " +
+                "personal screen capture disabled = ${given.personalScreenCaptureDisabled}, " +
+                "work screen capture disabled = ${given.workScreenCaptureDisabled}, " +
+                "disallow share into managed profile = ${given.disallowShareIntoManagedProfile}, " +
+                "expected screen capture completely disabled = $expectedScreenCaptureCompletelyDisabled"
+    }
+
+    @Test
+    fun test() {
+        val hostAppUserHandle =
+            if (test.given.isHostInWorkProfile) workUserHandle else personalUserHandle
+
+        val completelyDisabled = policyResolver.isScreenCaptureCompletelyDisabled(hostAppUserHandle)
+
+        assertWithMessage("Screen capture policy resolved incorrectly")
+            .that(completelyDisabled)
+            .isEqualTo(test.expectedScreenCaptureCompletelyDisabled)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
index 9bcfd5b..1a93adc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
@@ -26,6 +26,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.FakeDisplayTracker;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,7 +47,8 @@
 
     @Before
     public void setup() {
-        mFlagsContainer = new SysUiState();
+        FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext);
+        mFlagsContainer = new SysUiState(displayTracker);
         mCallback = mock(SysUiState.SysUiStateCallback.class);
         mFlagsContainer.addCallback(mCallback);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
index 1bc4719..1a35502 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -31,10 +31,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -69,7 +66,7 @@
         // Expressive applies hue rotations to the theme color. The input theme color has hue
         // 117, ensuring the hue changed significantly is a strong signal styles are being applied.
         ColorScheme colorScheme = new ColorScheme(wallpaperColors, false, Style.EXPRESSIVE);
-        Assert.assertEquals(357.77, Cam.fromInt(colorScheme.getAccent1().get(6)).getHue(), 0.1);
+        Assert.assertEquals(357.77, Cam.fromInt(colorScheme.getAccent1().getS500()).getHue(), 0.1);
     }
 
 
@@ -111,7 +108,8 @@
     public void testTertiaryHueWrapsProperly() {
         int colorInt = 0xffB3588A; // H350 C50 T50
         ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */);
-        int tertiaryMid = colorScheme.getAccent3().get(colorScheme.getAccent3().size() / 2);
+        int tertiaryMid = colorScheme.getAccent3().getAllShades().get(
+                colorScheme.getShadeCount() / 2);
         Cam cam = Cam.fromInt(tertiaryMid);
         Assert.assertEquals(cam.getHue(), 50.0, 10.0);
     }
@@ -121,7 +119,8 @@
         int colorInt = 0xffB3588A; // H350 C50 T50
         ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
                 Style.SPRITZ /* style */);
-        int primaryMid = colorScheme.getAccent1().get(colorScheme.getAccent1().size() / 2);
+        int primaryMid = colorScheme.getAccent1().getAllShades().get(
+                colorScheme.getShadeCount() / 2);
         Cam cam = Cam.fromInt(primaryMid);
         Assert.assertEquals(cam.getChroma(), 12.0, 1.0);
     }
@@ -131,7 +130,8 @@
         int colorInt = 0xffB3588A; // H350 C50 T50
         ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
                 Style.VIBRANT /* style */);
-        int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+        int neutralMid = colorScheme.getNeutral1().getAllShades().get(
+                colorScheme.getShadeCount() / 2);
         Cam cam = Cam.fromInt(neutralMid);
         Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 12.0);
     }
@@ -141,7 +141,8 @@
         int colorInt = 0xffB3588A; // H350 C50 T50
         ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
                 Style.EXPRESSIVE /* style */);
-        int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+        int neutralMid = colorScheme.getNeutral1().getAllShades().get(
+                colorScheme.getShadeCount() / 2);
         Cam cam = Cam.fromInt(neutralMid);
         Assert.assertTrue(cam.getChroma() <= 8.0);
     }
@@ -151,10 +152,11 @@
         int colorInt = 0xffB3588A; // H350 C50 T50
         ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
                 Style.MONOCHROMATIC /* style */);
-        int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+        int neutralMid = colorScheme.getNeutral1().getAllShades().get(
+                colorScheme.getShadeCount() / 2);
         Assert.assertTrue(
                 Color.red(neutralMid) == Color.green(neutralMid)
-                && Color.green(neutralMid) == Color.blue(neutralMid)
+                        && Color.green(neutralMid) == Color.blue(neutralMid)
         );
     }
 
@@ -190,15 +192,14 @@
                 xml.append("        <").append(styleName).append(">");
 
                 List<String> colors = new ArrayList<>();
-                for (Stream<Integer> stream: Arrays.asList(colorScheme.getAccent1().stream(),
-                        colorScheme.getAccent2().stream(),
-                        colorScheme.getAccent3().stream(),
-                        colorScheme.getNeutral1().stream(),
-                        colorScheme.getNeutral2().stream())) {
+
+                colorScheme.getAllHues().forEach(schemeHue -> {
                     colors.add("ffffff");
-                    colors.addAll(stream.map(Integer::toHexString).map(s -> s.substring(2)).collect(
-                            Collectors.toList()));
-                }
+                    schemeHue.getAllShades().forEach(tone -> {
+                        colors.add(Integer.toHexString(tone).substring(2));
+                    });
+                });
+
                 xml.append(String.join(",", colors));
                 xml.append("</").append(styleName).append(">\n");
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java
index 92652a7..3eb7329 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java
@@ -40,6 +40,7 @@
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.After;
@@ -65,6 +66,7 @@
     private ImageReader mReader;
     private NavigationBarView mNavBar;
     private VirtualDisplay mVirtualDisplay;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Before
     public void setup() {
@@ -83,6 +85,7 @@
         mDependency.injectTestDependency(EdgeBackGestureHandler.Factory.class,
                 mEdgeBackGestureHandlerFactory);
         mNavBar = new NavigationBarView(context, null);
+        mNavBar.setDisplayTracker(mDisplayTracker);
     }
 
     private Display createVirtualDisplay() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
index 9bf27a2..aacbf8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
+import android.hardware.display.DisplayManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.SparseArray;
@@ -50,12 +51,14 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.settings.SecureSettings;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.pip.Pip;
 
@@ -80,6 +83,7 @@
     private NavigationBar mDefaultNavBar;
     private NavigationBar mSecondaryNavBar;
     private StaticMockitoSession mMockitoSession;
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Mock
     private CommandQueue mCommandQueue;
@@ -91,6 +95,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
         mNavigationBarController = spy(
                 new NavigationBarController(mContext,
                         mock(OverviewProxyService.class),
@@ -102,13 +107,15 @@
                         mock(NavBarHelper.class),
                         mTaskbarDelegate,
                         mNavigationBarFactory,
-                        mock(StatusBarKeyguardViewManager.class),
                         mock(DumpManager.class),
                         mock(AutoHideController.class),
                         mock(LightBarController.class),
+                        TaskStackChangeListeners.getTestInstance(),
                         Optional.of(mock(Pip.class)),
                         Optional.of(mock(BackAnimation.class)),
-                        mock(FeatureFlags.class)));
+                        mock(FeatureFlags.class),
+                        mock(SecureSettings.class),
+                        mDisplayTracker));
         initializeNavigationBars();
         mMockitoSession = mockitoSession().mockStatic(Utilities.class).startMocking();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 80adbf0..764ddc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -28,6 +28,7 @@
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
 import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -44,6 +45,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.hardware.display.DisplayManagerGlobal;
@@ -85,11 +87,13 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -203,6 +207,8 @@
     private ViewRootImpl mViewRootImpl;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
+    private TaskStackChangeListeners mTaskStackChangeListeners =
+            TaskStackChangeListeners.getTestInstance();
 
     @Rule
     public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
@@ -437,6 +443,14 @@
         verify(mNavBarHelper, times(1)).getCurrentSysuiState();
     }
 
+    @Test
+    public void testScreenPinningEnabled_updatesSysuiState() {
+        mNavigationBar.init();
+        mTaskStackChangeListeners.getListenerImpl().onLockTaskModeChanged(
+                ActivityManager.LOCK_TASK_MODE_PINNED);
+        verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_SCREEN_PINNING), eq(true));
+    }
+
     private NavigationBar createNavBar(Context context) {
         DeviceProvisionedController deviceProvisionedController =
                 mock(DeviceProvisionedController.class);
@@ -481,7 +495,9 @@
                 mEdgeBackGestureHandler,
                 Optional.of(mock(BackAnimation.class)),
                 mUserContextProvider,
-                mWakefulnessLifecycle));
+                mWakefulnessLifecycle,
+                mTaskStackChangeListeners,
+                new FakeDisplayTracker(mContext)));
     }
 
     private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
index cafd2cf..5270737 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
@@ -36,6 +36,7 @@
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -63,6 +64,7 @@
     IWindowManager mIWindowManager;
 
     private NavigationBarTransitions mTransitions;
+    private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Before
     public void setup() {
@@ -86,7 +88,7 @@
         when(navBar.getCurrentView()).thenReturn(navBar);
         when(navBar.findViewById(anyInt())).thenReturn(navBar);
         mTransitions = new NavigationBarTransitions(
-                navBar, mIWindowManager, mLightBarTransitionsFactory);
+                navBar, mIWindowManager, mLightBarTransitionsFactory, mDisplayTracker);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index 1742c69..1c9336a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -1,19 +1,24 @@
 package com.android.systemui.navigationbar
 
+import android.app.ActivityManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.model.SysUiState
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler
 import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.shared.system.QuickStepContract
+import com.android.systemui.shared.system.TaskStackChangeListeners
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.phone.AutoHideController
 import com.android.systemui.statusbar.phone.LightBarController
 import com.android.systemui.statusbar.phone.LightBarTransitionsController
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.wm.shell.back.BackAnimation
 import com.android.wm.shell.pip.Pip
 import org.junit.Before
 import org.junit.Test
+import org.mockito.ArgumentMatchers
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
@@ -30,6 +35,7 @@
     val MODE_GESTURE = 0;
     val MODE_THREE_BUTTON = 1;
 
+    private lateinit var mTaskStackChangeListeners: TaskStackChangeListeners
     private lateinit var mTaskbarDelegate: TaskbarDelegate
     @Mock
     lateinit var mEdgeBackGestureHandlerFactory : EdgeBackGestureHandler.Factory
@@ -61,6 +67,8 @@
     lateinit var mBackAnimation: BackAnimation
     @Mock
     lateinit var mCurrentSysUiState: NavBarHelper.CurrentSysuiState
+    @Mock
+    lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager
 
     @Before
     fun setup() {
@@ -69,11 +77,12 @@
         `when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController)
         `when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState)
         `when`(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState)
+        mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
         mTaskbarDelegate = TaskbarDelegate(context, mEdgeBackGestureHandlerFactory,
-                mLightBarControllerFactory)
+                mLightBarControllerFactory, mStatusBarKeyguardViewManager)
         mTaskbarDelegate.setDependencies(mCommandQueue, mOverviewProxyService, mNavBarHelper,
         mNavigationModeController, mSysUiState, mDumpManager, mAutoHideController,
-                mLightBarController, mOptionalPip, mBackAnimation)
+                mLightBarController, mOptionalPip, mBackAnimation, mTaskStackChangeListeners)
     }
 
     @Test
@@ -90,4 +99,15 @@
         mTaskbarDelegate.init(DISPLAY_ID)
         verify(mEdgeBackGestureHandler, times(1)).onNavigationModeChanged(MODE_GESTURE)
     }
+
+    @Test
+    fun screenPinningEnabled_updatesSysuiState() {
+        mTaskbarDelegate.init(DISPLAY_ID)
+        mTaskStackChangeListeners.listenerImpl.onLockTaskModeChanged(
+            ActivityManager.LOCK_TASK_MODE_PINNED)
+        verify(mSysUiState, times(1)).setFlag(
+            ArgumentMatchers.eq(QuickStepContract.SYSUI_STATE_SCREEN_PINNING),
+            ArgumentMatchers.eq(true)
+        )
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
new file mode 100644
index 0000000..bc31a0e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.navigationbar.gestural
+
+import android.os.Handler
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.ViewConfiguration
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.NavigationEdgeBackPlugin
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BackPanelControllerTest : SysuiTestCase() {
+    companion object {
+        private const val START_X: Float = 0f
+    }
+    private lateinit var mBackPanelController: BackPanelController
+    private lateinit var testableLooper: TestableLooper
+    private var triggerThreshold: Float = 0.0f
+    private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop
+    @Mock private lateinit var vibratorHelper: VibratorHelper
+    @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var latencyTracker: LatencyTracker
+    @Mock private lateinit var layoutParams: WindowManager.LayoutParams
+    @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mBackPanelController =
+            BackPanelController(
+                context,
+                windowManager,
+                ViewConfiguration.get(context),
+                Handler.createAsync(Looper.myLooper()),
+                vibratorHelper,
+                configurationController,
+                latencyTracker
+            )
+        mBackPanelController.setLayoutParams(layoutParams)
+        mBackPanelController.setBackCallback(backCallback)
+        mBackPanelController.setIsLeftPanel(true)
+        testableLooper = TestableLooper.get(this)
+        triggerThreshold = mBackPanelController.params.staticTriggerThreshold
+    }
+
+    @Test
+    fun handlesActionDown() {
+        startTouch()
+
+        assertThat(mBackPanelController.currentState)
+            .isEqualTo(BackPanelController.GestureState.GONE)
+    }
+
+    @Test
+    fun staysHiddenBeforeSlopCrossed() {
+        startTouch()
+        // Move just enough to not cross the touch slop
+        continueTouch(START_X + touchSlop - 1)
+
+        assertThat(mBackPanelController.currentState)
+            .isEqualTo(BackPanelController.GestureState.GONE)
+    }
+
+    @Test
+    fun handlesBackCommitted() {
+        startTouch()
+        // Move once to cross the touch slop
+        continueTouch(START_X + touchSlop.toFloat() + 1)
+        // Move again to cross the back trigger threshold
+        continueTouch(START_X + touchSlop + triggerThreshold + 1)
+
+        assertThat(mBackPanelController.currentState)
+            .isEqualTo(BackPanelController.GestureState.ACTIVE)
+        verify(backCallback).setTriggerBack(true)
+        testableLooper.moveTimeForward(100)
+        testableLooper.processAllMessages()
+        verify(vibratorHelper).vibrate(VIBRATE_ACTIVATED_EFFECT)
+
+        finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
+        assertThat(mBackPanelController.currentState)
+            .isEqualTo(BackPanelController.GestureState.FLUNG)
+        verify(backCallback).triggerBack()
+    }
+
+    @Test
+    fun handlesBackCancelled() {
+        startTouch()
+        continueTouch(START_X + touchSlop.toFloat() + 1)
+        continueTouch(
+            START_X + touchSlop + triggerThreshold -
+                mBackPanelController.params.deactivationSwipeTriggerThreshold
+        )
+        clearInvocations(backCallback)
+        Thread.sleep(MIN_DURATION_ACTIVE_ANIMATION)
+        // Move in the opposite direction to cross the deactivation threshold and cancel back
+        continueTouch(START_X)
+
+        assertThat(mBackPanelController.currentState)
+            .isEqualTo(BackPanelController.GestureState.INACTIVE)
+        verify(backCallback).setTriggerBack(false)
+        verify(vibratorHelper).vibrate(VIBRATE_DEACTIVATED_EFFECT)
+
+        finishTouchActionUp(START_X)
+        verify(backCallback).cancelBack()
+    }
+
+    private fun startTouch() {
+        mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
+    }
+
+    private fun continueTouch(x: Float) {
+        mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f))
+    }
+
+    private fun finishTouchActionUp(x: Float) {
+        mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f))
+    }
+
+    private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
+        return MotionEvent.obtain(0L, 0L, action, x, y, 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
index 36e02cb..5f206b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
@@ -13,43 +13,61 @@
 
 @RunWith(Parameterized::class)
 @SmallTest
-internal class FloatingRotationButtonPositionCalculatorTest(private val testCase: TestCase)
-    : SysuiTestCase() {
-
-    private val calculator = FloatingRotationButtonPositionCalculator(
-        MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM
-    )
+internal class FloatingRotationButtonPositionCalculatorTest(
+        private val testCase: TestCase,
+) : SysuiTestCase() {
 
     @Test
     fun calculatePosition() {
-        val position = calculator.calculatePosition(
+        val position = testCase.calculator.calculatePosition(
             testCase.rotation,
             testCase.taskbarVisible,
             testCase.taskbarStashed
         )
-
         assertThat(position).isEqualTo(testCase.expectedPosition)
     }
 
     internal class TestCase(
+        val calculator: FloatingRotationButtonPositionCalculator,
         val rotation: Int,
         val taskbarVisible: Boolean,
         val taskbarStashed: Boolean,
         val expectedPosition: Position
     ) {
         override fun toString(): String =
-            "when rotation = $rotation, " +
-                "taskbarVisible = $taskbarVisible, " +
-                "taskbarStashed = $taskbarStashed - " +
-                "expected $expectedPosition"
+                buildString {
+                    append("when calculator = ")
+                    append(when (calculator) {
+                        posLeftCalculator -> "LEFT"
+                        posRightCalculator -> "RIGHT"
+                        else -> error("Unknown calculator: $calculator")
+                    })
+                    append(", rotation = $rotation")
+                    append(", taskbarVisible = $taskbarVisible")
+                    append(", taskbarStashed = $taskbarStashed")
+                    append(" - expected $expectedPosition")
+                }
     }
 
     companion object {
+        private const val MARGIN_DEFAULT = 10
+        private const val MARGIN_TASKBAR_LEFT = 20
+        private const val MARGIN_TASKBAR_BOTTOM = 30
+
+        private val posLeftCalculator = FloatingRotationButtonPositionCalculator(
+            MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM, true
+        )
+        private val posRightCalculator = FloatingRotationButtonPositionCalculator(
+            MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM, false
+        )
+
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<TestCase> =
             listOf(
+                // Position left
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_0,
                     taskbarVisible = false,
                     taskbarStashed = false,
@@ -60,6 +78,7 @@
                     )
                 ),
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_90,
                     taskbarVisible = false,
                     taskbarStashed = false,
@@ -70,6 +89,7 @@
                     )
                 ),
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_180,
                     taskbarVisible = false,
                     taskbarStashed = false,
@@ -80,6 +100,7 @@
                     )
                 ),
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_270,
                     taskbarVisible = false,
                     taskbarStashed = false,
@@ -90,6 +111,7 @@
                     )
                 ),
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_0,
                     taskbarVisible = true,
                     taskbarStashed = false,
@@ -100,6 +122,7 @@
                     )
                 ),
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_0,
                     taskbarVisible = true,
                     taskbarStashed = true,
@@ -110,6 +133,7 @@
                     )
                 ),
                 TestCase(
+                    calculator = posLeftCalculator,
                     rotation = Surface.ROTATION_90,
                     taskbarVisible = true,
                     taskbarStashed = false,
@@ -118,11 +142,86 @@
                         translationX = -MARGIN_TASKBAR_LEFT,
                         translationY = -MARGIN_TASKBAR_BOTTOM
                     )
+                ),
+
+                // Position right
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_0,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.RIGHT,
+                        translationX = -MARGIN_DEFAULT,
+                        translationY = -MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_90,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.TOP or Gravity.RIGHT,
+                        translationX = -MARGIN_DEFAULT,
+                        translationY = MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_180,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.TOP or Gravity.LEFT,
+                        translationX = MARGIN_DEFAULT,
+                        translationY = MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_270,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.LEFT,
+                        translationX = MARGIN_DEFAULT,
+                        translationY = -MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_0,
+                    taskbarVisible = true,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.RIGHT,
+                        translationX = -MARGIN_TASKBAR_LEFT,
+                        translationY = -MARGIN_TASKBAR_BOTTOM
+                    )
+                ),
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_0,
+                    taskbarVisible = true,
+                    taskbarStashed = true,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.RIGHT,
+                        translationX = -MARGIN_DEFAULT,
+                        translationY = -MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    calculator = posRightCalculator,
+                    rotation = Surface.ROTATION_90,
+                    taskbarVisible = true,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.TOP or Gravity.RIGHT,
+                        translationX = -MARGIN_TASKBAR_LEFT,
+                        translationY = MARGIN_TASKBAR_BOTTOM
+                    )
                 )
             )
-
-        private const val MARGIN_DEFAULT = 10
-        private const val MARGIN_TASKBAR_LEFT = 20
-        private const val MARGIN_TASKBAR_BOTTOM = 30
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 4a9c750..39c4e06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -23,10 +23,14 @@
 import android.os.UserManager
 import android.test.suitebuilder.annotation.SmallTest
 import androidx.test.runner.AndroidJUnit4
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskController.Companion.INTENT_EXTRA_USE_STYLUS_MODE
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
+import com.android.systemui.notetask.NoteTaskInfoResolver.NoteTaskInfo
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.android.wm.shell.bubbles.Bubbles
@@ -36,8 +40,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
 /**
@@ -50,24 +54,23 @@
 @RunWith(AndroidJUnit4::class)
 internal class NoteTaskControllerTest : SysuiTestCase() {
 
-    private val notesIntent = Intent(NOTES_ACTION)
-
     @Mock lateinit var context: Context
     @Mock lateinit var packageManager: PackageManager
-    @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+    @Mock lateinit var resolver: NoteTaskInfoResolver
     @Mock lateinit var bubbles: Bubbles
     @Mock lateinit var optionalBubbles: Optional<Bubbles>
     @Mock lateinit var keyguardManager: KeyguardManager
     @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
     @Mock lateinit var optionalUserManager: Optional<UserManager>
     @Mock lateinit var userManager: UserManager
+    @Mock lateinit var uiEventLogger: UiEventLogger
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
         whenever(context.packageManager).thenReturn(packageManager)
-        whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+        whenever(resolver.resolveInfo()).thenReturn(NoteTaskInfo(NOTES_PACKAGE_NAME, NOTES_UID))
         whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
         whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
         whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
@@ -77,101 +80,182 @@
     private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
         return NoteTaskController(
             context = context,
-            intentResolver = noteTaskIntentResolver,
+            resolver = resolver,
             optionalBubbles = optionalBubbles,
             optionalKeyguardManager = optionalKeyguardManager,
             optionalUserManager = optionalUserManager,
             isEnabled = isEnabled,
+            uiEventLogger = uiEventLogger,
         )
     }
 
     // region showNoteTask
     @Test
-    fun showNoteTask_keyguardIsLocked_shouldStartActivity() {
+    fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() {
         whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+            )
 
-        verify(context).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(context).startActivity(capture(intentCaptor))
+        intentCaptor.value.let { intent ->
+            assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+            assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+            assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+            assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+        }
+        verifyZeroInteractions(bubbles)
+        verify(uiEventLogger)
+            .log(
+                ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+                NOTES_UID,
+                NOTES_PACKAGE_NAME
+            )
     }
 
     @Test
-    fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() {
+    fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesAndLogUiEvent() {
         whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+            )
 
-        verify(bubbles).showAppBubble(notesIntent)
-        verify(context, never()).startActivity(notesIntent)
+        verifyZeroInteractions(context)
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+        intentCaptor.value.let { intent ->
+            assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+            assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+            assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+            assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+        }
+        verify(uiEventLogger)
+            .log(
+                ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+                NOTES_UID,
+                NOTES_PACKAGE_NAME
+            )
     }
 
     @Test
-    fun showNoteTask_isInMultiWindowMode_shouldStartActivity() {
+    fun showNoteTask_keyguardIsUnlocked_uiEventIsNull_shouldStartBubblesWithoutLoggingUiEvent() {
         whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false, uiEvent = null)
 
-        verify(context).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context)
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+        intentCaptor.value.let { intent ->
+            assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+            assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+            assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+            assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+        }
+        verifyZeroInteractions(uiEventLogger)
+    }
+
+    @Test
+    fun showNoteTask_isInMultiWindowMode_shouldStartActivityAndLogUiEvent() {
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = true,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+            )
+
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(context).startActivity(capture(intentCaptor))
+        intentCaptor.value.let { intent ->
+            assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+            assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+            assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+            assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+        }
+        verifyZeroInteractions(bubbles)
+        verify(uiEventLogger)
+            .log(ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, NOTES_UID, NOTES_PACKAGE_NAME)
     }
 
     @Test
     fun showNoteTask_bubblesIsNull_shouldDoNothing() {
         whenever(optionalBubbles.orElse(null)).thenReturn(null)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+            )
 
-        verify(context, never()).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context, bubbles, uiEventLogger)
     }
 
     @Test
     fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
         whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+            )
 
-        verify(context, never()).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context, bubbles, uiEventLogger)
     }
 
     @Test
     fun showNoteTask_userManagerIsNull_shouldDoNothing() {
         whenever(optionalUserManager.orElse(null)).thenReturn(null)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+            )
 
-        verify(context, never()).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context, bubbles, uiEventLogger)
     }
 
     @Test
     fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
-        whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+        whenever(resolver.resolveInfo()).thenReturn(null)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+            )
 
-        verify(context, never()).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context, bubbles, uiEventLogger)
     }
 
     @Test
     fun showNoteTask_flagDisabled_shouldDoNothing() {
-        createNoteTaskController(isEnabled = false).showNoteTask()
+        createNoteTaskController(isEnabled = false)
+            .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
 
-        verify(context, never()).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context, bubbles, uiEventLogger)
     }
 
     @Test
     fun showNoteTask_userIsLocked_shouldDoNothing() {
         whenever(userManager.isUserUnlocked).thenReturn(false)
 
-        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+        createNoteTaskController()
+            .showNoteTask(
+                isInMultiWindowMode = false,
+                uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+            )
 
-        verify(context, never()).startActivity(notesIntent)
-        verify(bubbles, never()).showAppBubble(notesIntent)
+        verifyZeroInteractions(context, bubbles, uiEventLogger)
     }
     // endregion
 
@@ -206,4 +290,9 @@
         assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
     }
     // endregion
+
+    private companion object {
+        const val NOTES_PACKAGE_NAME = "com.android.note.app"
+        const val NOTES_UID = 123456
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
new file mode 100644
index 0000000..d6495d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.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.notetask
+
+import android.app.role.RoleManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskInfoResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInfoResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInfoResolverTest : SysuiTestCase() {
+
+    @Mock lateinit var packageManager: PackageManager
+    @Mock lateinit var roleManager: RoleManager
+
+    private lateinit var underTest: NoteTaskInfoResolver
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = NoteTaskInfoResolver(context, roleManager, packageManager)
+    }
+
+    @Test
+    fun resolveInfo_shouldReturnInfo() {
+        val packageName = "com.android.note.app"
+        val uid = 123456
+        whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+            .then { listOf(packageName) }
+        whenever(
+                packageManager.getApplicationInfoAsUser(
+                    eq(packageName),
+                    any<PackageManager.ApplicationInfoFlags>(),
+                    eq(context.user)
+                )
+            )
+            .thenReturn(ApplicationInfo().apply { this.uid = uid })
+
+        val actual = underTest.resolveInfo()
+
+        requireNotNull(actual) { "Note task info must not be null" }
+        assertThat(actual.packageName).isEqualTo(packageName)
+        assertThat(actual.uid).isEqualTo(uid)
+    }
+
+    @Test
+    fun resolveInfo_packageManagerThrowsException_shouldReturnInfoWithZeroUid() {
+        val packageName = "com.android.note.app"
+        whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+            .then { listOf(packageName) }
+        whenever(
+                packageManager.getApplicationInfoAsUser(
+                    eq(packageName),
+                    any<PackageManager.ApplicationInfoFlags>(),
+                    eq(context.user)
+                )
+            )
+            .thenThrow(PackageManager.NameNotFoundException(packageName))
+
+        val actual = underTest.resolveInfo()
+
+        requireNotNull(actual) { "Note task info must not be null" }
+        assertThat(actual.packageName).isEqualTo(packageName)
+        assertThat(actual.uid).isEqualTo(0)
+    }
+
+    @Test
+    fun resolveInfo_noRoleHolderIsSet_shouldReturnNull() {
+        whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskInfoResolver.ROLE_NOTES), any()))
+            .then { listOf<String>() }
+
+        val actual = underTest.resolveInfo()
+
+        assertThat(actual).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 538131a..53720ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -15,11 +15,14 @@
  */
 package com.android.systemui.notetask
 
+import android.app.KeyguardManager
 import android.test.suitebuilder.annotation.SmallTest
 import android.view.KeyEvent
 import androidx.test.runner.AndroidJUnit4
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
@@ -30,6 +33,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
 /**
@@ -55,12 +59,16 @@
         whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
     }
 
-    private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+    private fun createNoteTaskInitializer(
+        isEnabled: Boolean = true,
+        optionalKeyguardManager: Optional<KeyguardManager> = Optional.empty(),
+    ): NoteTaskInitializer {
         return NoteTaskInitializer(
             optionalBubbles = optionalBubbles,
             noteTaskController = noteTaskController,
             commandQueue = commandQueue,
             isEnabled = isEnabled,
+            optionalKeyguardManager = optionalKeyguardManager,
         )
     }
 
@@ -105,17 +113,44 @@
 
     // region handleSystemKey
     @Test
-    fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
-        createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+    fun handleSystemKey_receiveValidSystemKey_keyguardNotLocked_shouldShowNoteTaskWithUnlocked() {
+        val keyguardManager =
+            mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(false) }
+        createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
+            .callbacks
+            .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
 
-        verify(noteTaskController).showNoteTask()
+        verify(noteTaskController)
+            .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
+    }
+
+    @Test
+    fun handleSystemKey_receiveValidSystemKey_keyguardLocked_shouldShowNoteTaskWithLocked() {
+        val keyguardManager =
+            mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(true) }
+        createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
+            .callbacks
+            .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+        verify(noteTaskController)
+            .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED)
+    }
+
+    @Test
+    fun handleSystemKey_receiveValidSystemKey_nullKeyguardManager_shouldShowNoteTaskWithUnlocked() {
+        createNoteTaskInitializer(optionalKeyguardManager = Optional.empty())
+            .callbacks
+            .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+        verify(noteTaskController)
+            .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
     }
 
     @Test
     fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
         createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
 
-        verify(noteTaskController, never()).showNoteTask()
+        verifyZeroInteractions(noteTaskController)
     }
     // endregion
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
deleted file mode 100644
index dd2cc2f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.notetask
-
-import android.content.ComponentName
-import android.content.Intent
-import android.content.pm.ActivityInfo
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.ResolveInfoFlags
-import android.content.pm.ResolveInfo
-import android.content.pm.ServiceInfo
-import android.test.suitebuilder.annotation.SmallTest
-import androidx.test.runner.AndroidJUnit4
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.MockitoAnnotations
-
-/**
- * Tests for [NoteTaskIntentResolver].
- *
- * Build/Install/Run:
- * - atest SystemUITests:NoteTaskIntentResolverTest
- */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-internal class NoteTaskIntentResolverTest : SysuiTestCase() {
-
-    @Mock lateinit var packageManager: PackageManager
-
-    private lateinit var resolver: NoteTaskIntentResolver
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        resolver = NoteTaskIntentResolver(packageManager)
-    }
-
-    private fun createResolveInfo(
-        packageName: String = "PackageName",
-        activityInfo: ActivityInfo? = null,
-    ): ResolveInfo {
-        return ResolveInfo().apply {
-            serviceInfo =
-                ServiceInfo().apply {
-                    applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
-                }
-            this.activityInfo = activityInfo
-        }
-    }
-
-    private fun createActivityInfo(
-        name: String? = "ActivityName",
-        exported: Boolean = true,
-        enabled: Boolean = true,
-        showWhenLocked: Boolean = true,
-        turnScreenOn: Boolean = true,
-    ): ActivityInfo {
-        return ActivityInfo().apply {
-            this.name = name
-            this.exported = exported
-            this.enabled = enabled
-            if (showWhenLocked) {
-                flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED
-            }
-            if (turnScreenOn) {
-                flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
-            }
-        }
-    }
-
-    private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) {
-        whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>()))
-            .thenReturn(block())
-    }
-
-    private fun givenResolveActivity(block: () -> ResolveInfo?) {
-        whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block())
-    }
-
-    @Test
-    fun resolveIntent_shouldReturnNotesIntent() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) }
-
-        val actual = resolver.resolveIntent()
-
-        val expected =
-            Intent(NOTES_ACTION)
-                .setComponent(ComponentName("PackageName", "ActivityName"))
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        // Compares the string representation of both intents, as they are different instances.
-        assertThat(actual.toString()).isEqualTo(expected.toString())
-    }
-
-    @Test
-    fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity {
-            createResolveInfo(activityInfo = createActivityInfo(enabled = false))
-        }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity {
-            createResolveInfo(activityInfo = createActivityInfo(exported = false))
-        }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity {
-            createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false))
-        }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity {
-            createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false))
-        }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityInfoIsNull_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity { createResolveInfo(activityInfo = null) }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_resolveActivityIsNull_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo()) }
-        givenResolveActivity { null }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-
-    @Test
-    fun resolveIntent_activityNotFoundForAction_shouldReturnNull() {
-        givenQueryIntentActivities { emptyList() }
-
-        val actual = resolver.resolveIntent()
-
-        assertThat(actual).isNull()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..cdc683f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.notetask.quickaffordance
+
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.R
+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.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskQuickAffordanceConfig].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskQuickAffordanceConfigTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock lateinit var noteTaskController: NoteTaskController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    private fun createUnderTest(isEnabled: Boolean) =
+        NoteTaskQuickAffordanceConfig(
+            context = context,
+            noteTaskController = noteTaskController,
+            isEnabled = isEnabled,
+        )
+
+    @Test
+    fun lockScreenState_isNotEnabled_shouldEmitHidden() = runTest {
+        val underTest = createUnderTest(isEnabled = false)
+
+        val actual = collectLastValue(underTest.lockScreenState)
+
+        assertThat(actual()).isEqualTo(LockScreenState.Hidden)
+    }
+
+    @Test
+    fun lockScreenState_isEnabled_shouldEmitVisible() = runTest {
+        val stringResult = "Notetaking"
+        val underTest = createUnderTest(isEnabled = true)
+
+        val actual = collectLastValue(underTest.lockScreenState)
+
+        val expected =
+            LockScreenState.Visible(
+                icon =
+                    Icon.Resource(
+                        res = R.drawable.ic_note_task_shortcut_keyguard,
+                        contentDescription = ContentDescription.Loaded(stringResult),
+                    )
+            )
+        assertThat(actual()).isEqualTo(expected)
+    }
+
+    @Test
+    fun onTriggered_shouldLaunchNoteTask() {
+        val underTest = createUnderTest(isEnabled = false)
+
+        underTest.onTriggered(expandable = null)
+
+        verify(noteTaskController)
+            .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index a56990f..4a6158f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
@@ -53,6 +54,7 @@
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.NotificationChannels;
 
@@ -82,6 +84,8 @@
     @Mock
     private UiEventLogger mUiEventLogger;
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private View mView;
 
     private BroadcastReceiver mReceiver;
@@ -103,8 +107,12 @@
         mContext.addMockSystemService(NotificationManager.class, mMockNotificationManager);
         ActivityStarter starter = mDependency.injectMockDependency(ActivityStarter.class);
         BroadcastSender broadcastSender = mDependency.injectMockDependency(BroadcastSender.class);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+        when(mUserTracker.getUserHandle()).thenReturn(
+                UserHandle.of(ActivityManager.getCurrentUser()));
         mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter,
-                broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger);
+                broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger,
+                mUserTracker);
         BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1,
                 BatteryManager.BATTERY_HEALTH_GOOD, 5, 15);
         mPowerNotificationWarnings.updateSnapshot(snapshot);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
new file mode 100644
index 0000000..fb71977
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.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 SystemProcessConditionTest extends SysuiTestCase {
+    @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 the process is being ran by the
+     * system user.
+     */
+    @Test
+    public void testConditionFailsWithNonSystemProcess() {
+
+        final Condition condition = new SystemProcessCondition(mProcessWrapper);
+        when(mProcessWrapper.isSystemUser()).thenReturn(false);
+
+        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 true when tracker reports the process is being ran by the
+     * system user.
+     */
+    @Test
+    public void testConditionSucceedsWithSystemProcess() {
+
+        final Condition condition = new SystemProcessCondition(mProcessWrapper);
+        when(mProcessWrapper.isSystemUser()).thenReturn(true);
+
+        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/qs/AutoAddTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java
index 43fb1bd..dee1cc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java
@@ -59,6 +59,7 @@
 @SmallTest
 public class AutoAddTrackerTest extends SysuiTestCase {
 
+    private static final int END_POSITION = -1;
     private static final int USER = 0;
 
     @Mock
@@ -142,6 +143,29 @@
     }
 
     @Test
+    public void testRestoredTilePositionPreserved() {
+        verify(mBroadcastDispatcher).registerReceiver(
+                mBroadcastReceiverArgumentCaptor.capture(), any(), any(), any(), anyInt(), any());
+        String restoredTiles = "saver,internet,work,cast";
+        Intent restoreTilesIntent = makeRestoreIntent(Secure.QS_TILES, null, restoredTiles);
+
+        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, restoreTilesIntent);
+
+        assertEquals(2, mAutoTracker.getRestoredTilePosition("work"));
+    }
+
+    @Test
+    public void testNoRestoredTileReturnsEndPosition() {
+        verify(mBroadcastDispatcher).registerReceiver(
+                mBroadcastReceiverArgumentCaptor.capture(), any(), any(), any(), anyInt(), any());
+        Intent restoreTilesIntent = makeRestoreIntent(Secure.QS_TILES, null, null);
+
+        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, restoreTilesIntent);
+
+        assertEquals(END_POSITION, mAutoTracker.getRestoredTilePosition("work"));
+    }
+
+    @Test
     public void testBroadcastReceiverRegistered() {
         verify(mBroadcastDispatcher).registerReceiver(
                 any(), mIntentFilterArgumentCaptor.capture(), any(), eq(UserHandle.of(USER)),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 42ef9c2..89606bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -52,14 +52,14 @@
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.media.controls.ui.MediaHost;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
+import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -85,7 +85,6 @@
     @Mock private MediaHost mQSMediaHost;
     @Mock private MediaHost mQQSMediaHost;
     @Mock private KeyguardBypassController mBypassController;
-    @Mock private FalsingManager mFalsingManager;
     @Mock private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder;
     @Mock private TileServiceRequestController mTileServiceRequestController;
     @Mock private QSCustomizerController mQsCustomizerController;
@@ -494,7 +493,7 @@
     @Override
     protected Fragment instantiate(Context context, String className, Bundle arguments) {
         MockitoAnnotations.initMocks(this);
-        CommandQueue commandQueue = new CommandQueue(context);
+        CommandQueue commandQueue = new CommandQueue(context, new FakeDisplayTracker(context));
 
         setupQsComponent();
         setUpViews();
@@ -502,11 +501,9 @@
         setUpMedia();
         setUpOther();
 
-        FakeFeatureFlags featureFlags = new FakeFeatureFlags();
         return new QSFragment(
                 new RemoteInputQuickSettingsDisabler(
                         context, commandQueue, mock(ConfigurationController.class)),
-                mock(QSTileHost.class),
                 mStatusBarStateController,
                 commandQueue,
                 mQSMediaHost,
@@ -514,9 +511,8 @@
                 mBypassController,
                 mQsComponentFactory,
                 mock(QSFragmentDisableFlagsLogger.class),
-                mFalsingManager,
                 mock(DumpManager.class),
-                featureFlags,
+                mock(QSLogger.class),
                 mock(FooterActionsController.class),
                 mFooterActionsViewModelFactory);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 5058373..3d55c51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -71,7 +71,7 @@
     @Mock
     private QSPanel mQSPanel;
     @Mock
-    private QSTileHost mQSTileHost;
+    private QSHost mQSHost;
     @Mock
     private QSCustomizerController mQSCustomizerController;
     @Mock
@@ -105,7 +105,7 @@
 
     /** Implementation needed to ensure we have a reflectively-available class name. */
     private class TestableQSPanelControllerBase extends QSPanelControllerBase<QSPanel> {
-        protected TestableQSPanelControllerBase(QSPanel view, QSTileHost host,
+        protected TestableQSPanelControllerBase(QSPanel view, QSHost host,
                 QSCustomizerController qsCustomizerController, MediaHost mediaHost,
                 MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
                 DumpManager dumpManager) {
@@ -130,8 +130,8 @@
         when(mQSPanel.getOrCreateTileLayout()).thenReturn(mPagedTileLayout);
         when(mQSPanel.getTileLayout()).thenReturn(mPagedTileLayout);
         when(mQSTile.getTileSpec()).thenReturn("dnd");
-        when(mQSTileHost.getTiles()).thenReturn(Collections.singleton(mQSTile));
-        when(mQSTileHost.createTileView(any(), eq(mQSTile), anyBoolean())).thenReturn(mQSTileView);
+        when(mQSHost.getTiles()).thenReturn(Collections.singleton(mQSTile));
+        when(mQSHost.createTileView(any(), eq(mQSTile), anyBoolean())).thenReturn(mQSTileView);
         when(mQSTileRevealControllerFactory.create(any(), any()))
                 .thenReturn(mQSTileRevealController);
         when(mMediaHost.getDisappearParameters()).thenReturn(new DisappearParameters());
@@ -142,7 +142,7 @@
             return null;
         }).when(mQSPanel).setListening(anyBoolean());
 
-        mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost,
+        mController = new TestableQSPanelControllerBase(mQSPanel, mQSHost,
                 mQSCustomizerController, mMediaHost,
                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
 
@@ -155,7 +155,7 @@
         mController.onViewDetached();
 
         QSPanelControllerBase<QSPanel> controller = new TestableQSPanelControllerBase(mQSPanel,
-                mQSTileHost, mQSCustomizerController, mMediaHost,
+                mQSHost, mQSCustomizerController, mMediaHost,
                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager) {
             @Override
             protected QSTileRevealController createTileRevealController() {
@@ -250,7 +250,7 @@
 
         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
         when(mQSPanel.getDumpableTag()).thenReturn("QSPanelLandscape");
-        mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost,
+        mController = new TestableQSPanelControllerBase(mQSPanel, mQSHost,
                 mQSCustomizerController, mMediaHost,
                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
         mController.init();
@@ -259,7 +259,7 @@
 
         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true);
         when(mQSPanel.getDumpableTag()).thenReturn("QSPanelPortrait");
-        mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost,
+        mController = new TestableQSPanelControllerBase(mQSPanel, mQSHost,
                 mQSCustomizerController, mMediaHost,
                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
         mController.init();
@@ -291,7 +291,7 @@
 
     @Test
     public void testRefreshAllTilesDoesntRefreshListeningTiles() {
-        when(mQSTileHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
+        when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
         mController.setTiles();
 
         when(mQSTile.isListening()).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 6cf642c..a0d8f98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -9,7 +9,6 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.plugins.FalsingManager
@@ -41,7 +40,7 @@
 
     @Mock private lateinit var qsPanel: QSPanel
     @Mock private lateinit var tunerService: TunerService
-    @Mock private lateinit var qsTileHost: QSTileHost
+    @Mock private lateinit var qsHost: QSHost
     @Mock private lateinit var qsCustomizerController: QSCustomizerController
     @Mock private lateinit var qsTileRevealControllerFactory: QSTileRevealController.Factory
     @Mock private lateinit var dumpManager: DumpManager
@@ -57,7 +56,6 @@
     @Mock private lateinit var tile: QSTile
     @Mock private lateinit var otherTile: QSTile
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
-    @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var configuration: Configuration
     @Mock private lateinit var pagedTileLayout: PagedTileLayout
 
@@ -70,7 +68,7 @@
 
         whenever(brightnessSliderFactory.create(any(), any())).thenReturn(brightnessSlider)
         whenever(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
-        testableResources.addOverride(R.bool.config_use_split_notification_shade, false)
+        setShouldUseSplitShade(false)
         whenever(qsPanel.resources).thenReturn(testableResources.resources)
         whenever(qsPanel.getOrCreateTileLayout()).thenReturn(pagedTileLayout)
         whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
@@ -81,7 +79,7 @@
         controller = QSPanelController(
             qsPanel,
             tunerService,
-            qsTileHost,
+            qsHost,
             qsCustomizerController,
             /* usingMediaPlayer= */ true,
             mediaHost,
@@ -93,8 +91,7 @@
             brightnessControllerFactory,
             brightnessSliderFactory,
             falsingManager,
-            statusBarKeyguardViewManager,
-            featureFlags
+            statusBarKeyguardViewManager
         )
     }
 
@@ -112,7 +109,7 @@
 
     @Test
     fun testSetListeningDoesntRefreshListeningTiles() {
-        whenever(qsTileHost.getTiles()).thenReturn(listOf(tile, otherTile))
+        whenever(qsHost.getTiles()).thenReturn(listOf(tile, otherTile))
         controller.setTiles()
         whenever(tile.isListening()).thenReturn(false)
         whenever(otherTile.isListening()).thenReturn(true)
@@ -133,12 +130,31 @@
 
     @Test
     fun configurationChange_onlySplitShadeConfigChanges_tileAreRedistributed() {
-        testableResources.addOverride(R.bool.config_use_split_notification_shade, false)
+        setShouldUseSplitShade(false)
         controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
         verify(pagedTileLayout, never()).forceTilesRedistribution(any())
 
-        testableResources.addOverride(R.bool.config_use_split_notification_shade, true)
+        setShouldUseSplitShade(true)
         controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
         verify(pagedTileLayout).forceTilesRedistribution("Split shade state changed")
     }
+
+    @Test
+    fun configurationChange_onlySplitShadeConfigChanges_qsPanelCanBeCollapsed() {
+        setShouldUseSplitShade(false)
+        controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+        verify(qsPanel, never()).setCanCollapse(anyBoolean())
+
+        setShouldUseSplitShade(true)
+        controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+        verify(qsPanel).setCanCollapse(false)
+
+        setShouldUseSplitShade(false)
+        controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+        verify(qsPanel).setCanCollapse(true)
+    }
+
+    private fun setShouldUseSplitShade(shouldUse: Boolean) {
+        testableResources.addOverride(R.bool.config_use_split_notification_shade, shouldUse)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index d52b296..93cebe2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -37,6 +37,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -164,26 +165,11 @@
     }
 
     @Test
-    fun testTopPadding_notCombinedHeaders() {
-        qsPanel.setUsingCombinedHeaders(false)
+    fun testTopPadding() {
         val padding = 10
         val paddingCombined = 100
         context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding)
-        context.orCreateTestableResources.addOverride(
-                R.dimen.qs_panel_padding_top_combined_headers, paddingCombined)
-
-        qsPanel.updatePadding()
-        assertThat(qsPanel.paddingTop).isEqualTo(padding)
-    }
-
-    @Test
-    fun testTopPadding_combinedHeaders() {
-        qsPanel.setUsingCombinedHeaders(true)
-        val padding = 10
-        val paddingCombined = 100
-        context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding)
-        context.orCreateTestableResources.addOverride(
-                R.dimen.qs_panel_padding_top_combined_headers, paddingCombined)
+        context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, paddingCombined)
 
         qsPanel.updatePadding()
         assertThat(qsPanel.paddingTop).isEqualTo(paddingCombined)
@@ -196,6 +182,16 @@
         qsPanel.setSquishinessFraction(0.5f)
     }
 
+    @Test
+    fun testSplitShade_CollapseAccessibilityActionNotAnnounced() {
+        qsPanel.setCanCollapse(false)
+        val accessibilityInfo = mock(AccessibilityNodeInfo::class.java)
+        qsPanel.onInitializeAccessibilityNodeInfo(accessibilityInfo)
+
+        val actionCollapse = AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE
+        verify(accessibilityInfo, never()).addAction(actionCollapse)
+    }
+
     private infix fun View.isLeftOf(other: View): Boolean {
         val rect = Rect()
         getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index fb1a720..34d2b14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -69,7 +69,6 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.FakeSharedPreferences;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -100,11 +99,9 @@
     private static ComponentName CUSTOM_TILE =
             ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS");
     private static final String CUSTOM_TILE_SPEC = CustomTile.toSpec(CUSTOM_TILE);
-    private static final String SETTING = QSTileHost.TILES_SETTING;
+    private static final String SETTING = QSHost.TILES_SETTING;
 
     @Mock
-    private StatusBarIconController mIconController;
-    @Mock
     private QSFactory mDefaultFactory;
     @Mock
     private PluginManager mPluginManager;
@@ -167,7 +164,7 @@
 
         mSecureSettings = new FakeSettings();
         saveSetting("");
-        mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mMainExecutor,
+        mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor,
                 mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces,
                 mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
                 mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory,
@@ -248,44 +245,44 @@
     public void testRemoveWifiAndCellularWithoutInternet() {
         saveSetting("wifi, spec1, cell, spec2");
 
-        assertEquals("internet", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(2));
+        assertEquals("internet", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec1", mQSTileHost.getSpecs().get(1));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
     public void testRemoveWifiAndCellularWithInternet() {
         saveSetting("wifi, spec1, cell, spec2, internet");
 
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("internet", mQSTileHost.mTileSpecs.get(2));
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(1));
+        assertEquals("internet", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
     public void testRemoveWifiWithoutInternet() {
         saveSetting("spec1, wifi, spec2");
 
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("internet", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(2));
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("internet", mQSTileHost.getSpecs().get(1));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
     public void testRemoveCellWithInternet() {
         saveSetting("spec1, spec2, cell, internet");
 
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("internet", mQSTileHost.mTileSpecs.get(2));
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(1));
+        assertEquals("internet", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
     public void testNoWifiNoCellularNoInternet() {
         saveSetting("spec1,spec2");
 
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(1));
     }
 
     @Test
@@ -332,9 +329,9 @@
 
         mQSTileHost.addTile("spec1");
 
-        assertEquals(2, mQSTileHost.mTileSpecs.size());
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
+        assertEquals(2, mQSTileHost.getSpecs().size());
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(1));
     }
 
     @Test
@@ -346,10 +343,10 @@
         mQSTileHost.addTile("spec2", 1);
         mMainExecutor.runAllReady();
 
-        assertEquals(3, mQSTileHost.mTileSpecs.size());
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("spec3", mQSTileHost.mTileSpecs.get(2));
+        assertEquals(3, mQSTileHost.getSpecs().size());
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(1));
+        assertEquals("spec3", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
@@ -361,10 +358,10 @@
         mQSTileHost.addTile("spec2", 100);
         mMainExecutor.runAllReady();
 
-        assertEquals(3, mQSTileHost.mTileSpecs.size());
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec3", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(2));
+        assertEquals(3, mQSTileHost.getSpecs().size());
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec3", mQSTileHost.getSpecs().get(1));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
@@ -376,10 +373,10 @@
         mQSTileHost.addTile("spec2", QSTileHost.POSITION_AT_END);
         mMainExecutor.runAllReady();
 
-        assertEquals(3, mQSTileHost.mTileSpecs.size());
-        assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
-        assertEquals("spec3", mQSTileHost.mTileSpecs.get(1));
-        assertEquals("spec2", mQSTileHost.mTileSpecs.get(2));
+        assertEquals(3, mQSTileHost.getSpecs().size());
+        assertEquals("spec1", mQSTileHost.getSpecs().get(0));
+        assertEquals("spec3", mQSTileHost.getSpecs().get(1));
+        assertEquals("spec2", mQSTileHost.getSpecs().get(2));
     }
 
     @Test
@@ -389,8 +386,8 @@
         mQSTileHost.addTile(CUSTOM_TILE, /* end */ false);
         mMainExecutor.runAllReady();
 
-        assertEquals(1, mQSTileHost.mTileSpecs.size());
-        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
+        assertEquals(1, mQSTileHost.getSpecs().size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.getSpecs().get(0));
     }
 
     @Test
@@ -400,8 +397,8 @@
         mQSTileHost.addTile(CUSTOM_TILE);
         mMainExecutor.runAllReady();
 
-        assertEquals(2, mQSTileHost.mTileSpecs.size());
-        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
+        assertEquals(2, mQSTileHost.getSpecs().size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.getSpecs().get(0));
     }
 
     @Test
@@ -411,8 +408,8 @@
         mQSTileHost.addTile(CUSTOM_TILE, /* end */ false);
         mMainExecutor.runAllReady();
 
-        assertEquals(2, mQSTileHost.mTileSpecs.size());
-        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
+        assertEquals(2, mQSTileHost.getSpecs().size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.getSpecs().get(0));
     }
 
     @Test
@@ -422,8 +419,8 @@
         mQSTileHost.addTile(CUSTOM_TILE, /* end */ true);
         mMainExecutor.runAllReady();
 
-        assertEquals(2, mQSTileHost.mTileSpecs.size());
-        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(1));
+        assertEquals(2, mQSTileHost.getSpecs().size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.getSpecs().get(1));
     }
 
     @Test
@@ -478,7 +475,7 @@
         mQSTileHost.removeTiles(List.of("spec1", "spec2"));
 
         mMainExecutor.runAllReady();
-        assertEquals(List.of("spec3"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec3"), mQSTileHost.getSpecs());
     }
 
     @Test
@@ -488,7 +485,7 @@
         mQSTileHost.removeTile("spec3");
 
         mMainExecutor.runAllReady();
-        assertEquals(List.of("spec2"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec2"), mQSTileHost.getSpecs());
         assertEquals("spec2", getSetting());
     }
 
@@ -497,10 +494,10 @@
         saveSetting("spec1,spec2");
 
         mQSTileHost.addTile("spec3");
-        assertEquals(List.of("spec1", "spec2"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1", "spec2"), mQSTileHost.getSpecs());
 
         mMainExecutor.runAllReady();
-        assertEquals(List.of("spec1", "spec2", "spec3"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1", "spec2", "spec3"), mQSTileHost.getSpecs());
     }
 
     @Test
@@ -508,10 +505,10 @@
         saveSetting("spec1,spec2");
 
         mQSTileHost.removeTile("spec1");
-        assertEquals(List.of("spec1", "spec2"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1", "spec2"), mQSTileHost.getSpecs());
 
         mMainExecutor.runAllReady();
-        assertEquals(List.of("spec2"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec2"), mQSTileHost.getSpecs());
     }
 
     @Test
@@ -519,10 +516,10 @@
         saveSetting("spec1,spec2,spec3");
 
         mQSTileHost.removeTiles(List.of("spec3", "spec1"));
-        assertEquals(List.of("spec1", "spec2", "spec3"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1", "spec2", "spec3"), mQSTileHost.getSpecs());
 
         mMainExecutor.runAllReady();
-        assertEquals(List.of("spec2"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec2"), mQSTileHost.getSpecs());
     }
 
     @Test
@@ -530,17 +527,17 @@
         saveSetting("spec1," + CUSTOM_TILE_SPEC);
 
         mQSTileHost.removeTileByUser(CUSTOM_TILE);
-        assertEquals(List.of("spec1", CUSTOM_TILE_SPEC), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1", CUSTOM_TILE_SPEC), mQSTileHost.getSpecs());
 
         mMainExecutor.runAllReady();
-        assertEquals(List.of("spec1"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1"), mQSTileHost.getSpecs());
     }
 
     @Test
     public void testNonValidTileNotStoredInSettings() {
         saveSetting("spec1,not-valid");
 
-        assertEquals(List.of("spec1"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1"), mQSTileHost.getSpecs());
         assertEquals("spec1", getSetting());
     }
 
@@ -548,14 +545,14 @@
     public void testNotAvailableTileNotStoredInSettings() {
         saveSetting("spec1,na");
 
-        assertEquals(List.of("spec1"), mQSTileHost.mTileSpecs);
+        assertEquals(List.of("spec1"), mQSTileHost.getSpecs());
         assertEquals("spec1", getSetting());
     }
 
     @Test
     public void testIsTileAdded_true() {
         int user = mUserTracker.getUserId();
-        getSharedPreferenecesForUser(user)
+        getSharedPreferencesForUser(user)
                 .edit()
                 .putBoolean(CUSTOM_TILE.flattenToString(), true)
                 .apply();
@@ -566,7 +563,7 @@
     @Test
     public void testIsTileAdded_false() {
         int user = mUserTracker.getUserId();
-        getSharedPreferenecesForUser(user)
+        getSharedPreferencesForUser(user)
                 .edit()
                 .putBoolean(CUSTOM_TILE.flattenToString(), false)
                 .apply();
@@ -597,7 +594,7 @@
         int user = mUserTracker.getUserId();
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
 
-        assertTrue(getSharedPreferenecesForUser(user)
+        assertTrue(getSharedPreferencesForUser(user)
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
@@ -606,7 +603,7 @@
         int user = mUserTracker.getUserId();
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, false);
 
-        assertFalse(getSharedPreferenecesForUser(user)
+        assertFalse(getSharedPreferencesForUser(user)
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
@@ -615,7 +612,7 @@
         int user = mUserTracker.getUserId();
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
 
-        assertFalse(getSharedPreferenecesForUser(user + 1)
+        assertFalse(getSharedPreferencesForUser(user + 1)
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
@@ -627,8 +624,8 @@
         // This will be done by TileServiceManager
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
 
-        mQSTileHost.changeTilesByUser(mQSTileHost.mTileSpecs, List.of("spec1"));
-        assertFalse(getSharedPreferenecesForUser(user)
+        mQSTileHost.changeTilesByUser(mQSTileHost.getSpecs(), List.of("spec1"));
+        assertFalse(getSharedPreferencesForUser(user)
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
@@ -642,7 +639,7 @@
 
         mQSTileHost.removeTileByUser(CUSTOM_TILE);
         mMainExecutor.runAllReady();
-        assertFalse(getSharedPreferenecesForUser(user)
+        assertFalse(getSharedPreferencesForUser(user)
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
@@ -656,7 +653,7 @@
 
         mQSTileHost.removeTile(CUSTOM_TILE_SPEC);
         mMainExecutor.runAllReady();
-        assertFalse(getSharedPreferenecesForUser(user)
+        assertFalse(getSharedPreferencesForUser(user)
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
@@ -681,12 +678,12 @@
         assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className);
     }
 
-    private SharedPreferences getSharedPreferenecesForUser(int user) {
+    private SharedPreferences getSharedPreferencesForUser(int user) {
         return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user);
     }
 
     private class TestQSTileHost extends QSTileHost {
-        TestQSTileHost(Context context, StatusBarIconController iconController,
+        TestQSTileHost(Context context,
                 QSFactory defaultFactory, Executor mainExecutor,
                 PluginManager pluginManager, TunerService tunerService,
                 Provider<AutoTileManager> autoTiles, DumpManager dumpManager,
@@ -696,7 +693,7 @@
                 TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
                 TileLifecycleManager.Factory tileLifecycleManagerFactory,
                 UserFileManager userFileManager) {
-            super(context, iconController, defaultFactory, mainExecutor, pluginManager,
+            super(context, defaultFactory, mainExecutor, pluginManager,
                     tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger,
                     uiEventLogger, userTracker, secureSettings, customTileStatePersister,
                     tileServiceRequestControllerBuilder, tileLifecycleManagerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index f53e997..71ea831 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -50,7 +50,7 @@
 class QuickQSPanelControllerTest : SysuiTestCase() {
 
     @Mock private lateinit var quickQSPanel: QuickQSPanel
-    @Mock private lateinit var qsTileHost: QSTileHost
+    @Mock private lateinit var qsHost: QSHost
     @Mock private lateinit var qsCustomizerController: QSCustomizerController
     @Mock private lateinit var mediaHost: MediaHost
     @Mock private lateinit var metricsLogger: MetricsLogger
@@ -75,12 +75,12 @@
         whenever(quickQSPanel.isAttachedToWindow).thenReturn(true)
         whenever(quickQSPanel.dumpableTag).thenReturn("")
         whenever(quickQSPanel.resources).thenReturn(mContext.resources)
-        whenever(qsTileHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
+        whenever(qsHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
 
         controller =
             TestQuickQSPanelController(
                 quickQSPanel,
-                qsTileHost,
+                qsHost,
                 qsCustomizerController,
                 /* usingMediaPlayer = */ false,
                 mediaHost,
@@ -102,7 +102,7 @@
     fun testTileSublistWithFewerTiles_noCrash() {
         whenever(quickQSPanel.numQuickTiles).thenReturn(3)
 
-        whenever(qsTileHost.tiles).thenReturn(listOf(tile, tile))
+        whenever(qsHost.tiles).thenReturn(listOf(tile, tile))
 
         controller.setTiles()
     }
@@ -111,7 +111,7 @@
     fun testTileSublistWithTooManyTiles() {
         val limit = 3
         whenever(quickQSPanel.numQuickTiles).thenReturn(limit)
-        whenever(qsTileHost.tiles).thenReturn(listOf(tile, tile, tile, tile))
+        whenever(qsHost.tiles).thenReturn(listOf(tile, tile, tile, tile))
 
         controller.setTiles()
 
@@ -147,7 +147,7 @@
 
     class TestQuickQSPanelController(
         view: QuickQSPanel,
-        qsTileHost: QSTileHost,
+        qsHost: QSHost,
         qsCustomizerController: QSCustomizerController,
         usingMediaPlayer: Boolean,
         mediaHost: MediaHost,
@@ -159,7 +159,7 @@
     ) :
         QuickQSPanelController(
             view,
-            qsTileHost,
+            qsHost,
             qsCustomizerController,
             usingMediaPlayer,
             mediaHost,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index 39d89bf..555484c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -18,37 +18,16 @@
 
 import android.content.Context
 import android.testing.AndroidTestingRunner
-import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.battery.BatteryMeterViewController
-import com.android.systemui.colorextraction.SysuiColorExtractor
-import com.android.systemui.demomode.DemoModeController
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.qs.carrier.QSCarrierGroup
-import com.android.systemui.qs.carrier.QSCarrierGroupController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.phone.StatusIconContainer
-import com.android.systemui.statusbar.policy.Clock
-import com.android.systemui.statusbar.policy.VariableDateView
-import com.android.systemui.statusbar.policy.VariableDateViewController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Answers
-import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -58,78 +37,21 @@
     @Mock
     private lateinit var view: QuickStatusBarHeader
     @Mock
-    private lateinit var privacyIconsController: HeaderPrivacyIconsController
-    @Mock
-    private lateinit var statusBarIconController: StatusBarIconController
-    @Mock
-    private lateinit var demoModeController: DemoModeController
-    @Mock
     private lateinit var quickQSPanelController: QuickQSPanelController
-    @Mock(answer = Answers.RETURNS_SELF)
-    private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
-    @Mock
-    private lateinit var qsCarrierGroupController: QSCarrierGroupController
-    @Mock
-    private lateinit var colorExtractor: SysuiColorExtractor
-    @Mock
-    private lateinit var iconContainer: StatusIconContainer
-    @Mock
-    private lateinit var qsCarrierGroup: QSCarrierGroup
-    @Mock
-    private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
-    @Mock
-    private lateinit var variableDateViewController: VariableDateViewController
-    @Mock
-    private lateinit var batteryMeterViewController: BatteryMeterViewController
-    @Mock
-    private lateinit var clock: Clock
-    @Mock
-    private lateinit var variableDateView: VariableDateView
-    @Mock
-    private lateinit var mockView: View
+
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private lateinit var context: Context
-    @Mock
-    private lateinit var featureFlags: FeatureFlags
-    @Mock
-    private lateinit var insetsProvider: StatusBarContentInsetsProvider
-    @Mock
-    private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
-    @Mock
-    private lateinit var iconManager: StatusBarIconController.TintedIconManager
-
-    private val qsExpansionPathInterpolator = QSExpansionPathInterpolator()
 
     private lateinit var controller: QuickStatusBarHeaderController
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        stubViews()
-        `when`(iconContainer.context).thenReturn(context)
-        `when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
-        `when`(variableDateViewControllerFactory.create(any()))
-                .thenReturn(variableDateViewController)
-        `when`(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
         `when`(view.resources).thenReturn(mContext.resources)
         `when`(view.isAttachedToWindow).thenReturn(true)
         `when`(view.context).thenReturn(context)
 
-        controller = QuickStatusBarHeaderController(
-                view,
-                privacyIconsController,
-                statusBarIconController,
-                demoModeController,
-                quickQSPanelController,
-                qsCarrierGroupControllerBuilder,
-                colorExtractor,
-                qsExpansionPathInterpolator,
-                featureFlags,
-                variableDateViewControllerFactory,
-                batteryMeterViewController,
-                insetsProvider,
-                iconManagerFactory,
-        )
+        controller = QuickStatusBarHeaderController(view, quickQSPanelController)
     }
 
     @After
@@ -138,74 +60,11 @@
     }
 
     @Test
-    fun testClockNotClickable() {
-        assertThat(clock.isClickable).isFalse()
-    }
+    fun testListeningStatus() {
+        controller.setListening(true)
+        verify(quickQSPanelController).setListening(true)
 
-    @Test
-    fun testSingleCarrierListenerAttachedOnInit() {
-        controller.init()
-
-        verify(qsCarrierGroupController).setOnSingleCarrierChangedListener(any())
-    }
-
-    @Test
-    fun testSingleCarrierSetOnViewOnInit_false() {
-        `when`(qsCarrierGroupController.isSingleCarrier).thenReturn(false)
-        controller.init()
-
-        verify(view).setIsSingleCarrier(false)
-    }
-
-    @Test
-    fun testSingleCarrierSetOnViewOnInit_true() {
-        `when`(qsCarrierGroupController.isSingleCarrier).thenReturn(true)
-        controller.init()
-
-        verify(view).setIsSingleCarrier(true)
-    }
-
-    @Test
-    fun testRSSISlot_notCombined() {
-        controller.init()
-
-        val captor = argumentCaptor<List<String>>()
-        verify(view).onAttach(any(), any(), capture(captor), any(), anyBoolean())
-
-        assertThat(captor.value).containsExactly(
-            mContext.getString(com.android.internal.R.string.status_bar_mobile)
-        )
-    }
-
-    @Test
-    fun testSingleCarrierCallback() {
-        controller.init()
-        reset(view)
-
-        val captor = argumentCaptor<QSCarrierGroupController.OnSingleCarrierChangedListener>()
-        verify(qsCarrierGroupController).setOnSingleCarrierChangedListener(capture(captor))
-
-        captor.value.onSingleCarrierChanged(true)
-        verify(view).setIsSingleCarrier(true)
-
-        captor.value.onSingleCarrierChanged(false)
-        verify(view).setIsSingleCarrier(false)
-    }
-
-    @Test
-    fun testAlarmIconIgnored() {
-        controller.init()
-
-        verify(iconContainer).addIgnoredSlot(
-                mContext.getString(com.android.internal.R.string.status_bar_alarm_clock))
-    }
-
-    private fun stubViews() {
-        `when`(view.findViewById<View>(anyInt())).thenReturn(mockView)
-        `when`(view.findViewById<QSCarrierGroup>(R.id.carrier_group)).thenReturn(qsCarrierGroup)
-        `when`(view.findViewById<StatusIconContainer>(R.id.statusIcons)).thenReturn(iconContainer)
-        `when`(view.findViewById<Clock>(R.id.clock)).thenReturn(clock)
-        `when`(view.requireViewById<VariableDateView>(R.id.date)).thenReturn(variableDateView)
-        `when`(view.requireViewById<VariableDateView>(R.id.date_clock)).thenReturn(variableDateView)
+        controller.setListening(false)
+        verify(quickQSPanelController).setListening(false)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
index d42cbe3..c041cb6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
@@ -25,7 +25,7 @@
 
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -42,19 +42,19 @@
 
     private TileAdapter mTileAdapter;
     @Mock
-    private QSTileHost mQSTileHost;
+    private QSHost mQSHost;
 
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
         TestableLooper.get(this).runWithLooper(() -> mTileAdapter =
-                new TileAdapter(mContext, mQSTileHost, new UiEventLoggerFake()));
+                new TileAdapter(mContext, mQSHost, new UiEventLoggerFake()));
     }
 
     @Test
     public void testResetNotifiesHost() {
         mTileAdapter.resetTileSpecs(Collections.emptyList());
-        verify(mQSTileHost).changeTilesByUser(any(), any());
+        verify(mQSHost).changeTilesByUser(any(), any());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index 040af70..78a0258 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -55,7 +55,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.qs.QSIconView;
 import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -102,7 +102,7 @@
     @Mock
     private TileQueryHelper.TileStateListener mListener;
     @Mock
-    private QSTileHost mQSTileHost;
+    private QSHost mQSHost;
     @Mock
     private PackageManager mPackageManager;
     @Mock
@@ -131,7 +131,7 @@
                         return null;
                     }
                 }
-        ).when(mQSTileHost).createTile(anyString());
+        ).when(mQSHost).createTile(anyString());
         FakeSystemClock clock = new FakeSystemClock();
         mMainExecutor = new FakeExecutor(clock);
         mBgExecutor = new FakeExecutor(clock);
@@ -147,7 +147,7 @@
 
     @Test
     public void testIsFinished_trueAfterQuerying() {
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
@@ -156,7 +156,7 @@
 
     @Test
     public void testQueryTiles_callsListenerTwice() {
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
@@ -170,7 +170,7 @@
             return null;
         }).when(mListener).onTilesChanged(any());
 
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
@@ -184,7 +184,7 @@
         mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
                 STOCK_TILES);
 
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
@@ -204,7 +204,7 @@
         mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
                 STOCK_TILES);
 
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
@@ -224,7 +224,7 @@
         mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
                 STOCK_TILES);
 
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
@@ -240,9 +240,9 @@
     public void testCustomTileNotCreated() {
         Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES,
                 CUSTOM_TILE);
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
-        verify(mQSTileHost, never()).createTile(CUSTOM_TILE);
+        verify(mQSHost, never()).createTile(CUSTOM_TILE);
     }
 
     @Test
@@ -264,7 +264,7 @@
         mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
                 "");
 
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
         verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture());
@@ -278,7 +278,7 @@
         Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, null);
         mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
                 STOCK_TILES);
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
     }
 
     @Test
@@ -286,12 +286,12 @@
         Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, null);
 
         QSTile t = mock(QSTile.class);
-        when(mQSTileHost.createTile("hotspot")).thenReturn(t);
+        when(mQSHost.createTile("hotspot")).thenReturn(t);
 
         mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
                 "hotspot");
 
-        mTileQueryHelper.queryTiles(mQSTileHost);
+        mTileQueryHelper.queryTiles(mQSHost);
 
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
         InOrder verifier = inOrder(t);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
index 2bd068a..8644b5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
@@ -35,12 +35,14 @@
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.LaunchableFrameLayout
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.settings.FakeDisplayTracker
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.nullable
@@ -90,6 +92,7 @@
     private lateinit var customTile: CustomTile
     private lateinit var testableLooper: TestableLooper
     private lateinit var customTileBuilder: CustomTile.Builder
+    private val displayTracker = FakeDisplayTracker(mContext)
 
     @Before
     fun setUp() {
@@ -119,7 +122,8 @@
                 activityStarter,
                 qsLogger,
                 customTileStatePersister,
-                tileServices
+                tileServices,
+                displayTracker
         )
 
         customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
@@ -339,7 +343,7 @@
         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
         tile.qsTile.activityLaunchForClick = pi
 
-        tile.handleClick(mock(View::class.java))
+        tile.handleClick(mock(LaunchableFrameLayout::class.java))
 
         testableLooper.processAllMessages()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
index 8aa625a..46af89e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
@@ -39,7 +39,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.settings.UserTracker;
 
 import org.junit.After;
@@ -61,7 +61,7 @@
     @Mock
     private UserTracker mUserTracker;
     @Mock
-    private QSTileHost mQSTileHost;
+    private QSHost mQSHost;
     @Mock
     private Context mMockContext;
 
@@ -80,7 +80,7 @@
         when(mUserTracker.getUserHandle()).thenReturn(UserHandle.SYSTEM);
 
         when(mTileServices.getContext()).thenReturn(mMockContext);
-        when(mTileServices.getHost()).thenReturn(mQSTileHost);
+        when(mTileServices.getHost()).thenReturn(mQSHost);
         when(mTileLifecycle.getUserId()).thenAnswer(invocation -> mUserTracker.getUserId());
         when(mTileLifecycle.isActiveTile()).thenReturn(false);
 
@@ -98,28 +98,28 @@
 
     @Test
     public void testSetTileAddedIfNotAdded() {
-        when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false);
+        when(mQSHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false);
         mTileServiceManager.startLifecycleManagerAndAddTile();
 
-        verify(mQSTileHost).setTileAdded(mComponentName, mUserTracker.getUserId(), true);
+        verify(mQSHost).setTileAdded(mComponentName, mUserTracker.getUserId(), true);
     }
 
     @Test
     public void testNotSetTileAddedIfAdded() {
-        when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(true);
+        when(mQSHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(true);
         mTileServiceManager.startLifecycleManagerAndAddTile();
 
-        verify(mQSTileHost, never()).setTileAdded(eq(mComponentName), anyInt(), eq(true));
+        verify(mQSHost, never()).setTileAdded(eq(mComponentName), anyInt(), eq(true));
     }
 
     @Test
     public void testSetTileAddedCorrectUser() {
         int user = 10;
         when(mUserTracker.getUserId()).thenReturn(user);
-        when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false);
+        when(mQSHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false);
         mTileServiceManager.startLifecycleManagerAndAddTile();
 
-        verify(mQSTileHost).setTileAdded(mComponentName, user, true);
+        verify(mQSHost).setTileAdded(mComponentName, user, true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt
index bdfbca4..ccfb5cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt
@@ -27,27 +27,27 @@
 import com.android.internal.statusbar.IAddTileResultCallback
 import com.android.systemui.InstanceIdSequenceFake
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.QSHost
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.util.function.Consumer
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -62,7 +62,7 @@
     @Mock
     private lateinit var tileRequestDialog: TileRequestDialog
     @Mock
-    private lateinit var qsTileHost: QSTileHost
+    private lateinit var qsHost: QSHost
     @Mock
     private lateinit var commandRegistry: CommandRegistry
     @Mock
@@ -82,10 +82,10 @@
         `when`(logger.newInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
 
         // Tile not present by default
-        `when`(qsTileHost.indexOf(anyString())).thenReturn(-1)
+        `when`(qsHost.indexOf(anyString())).thenReturn(-1)
 
         controller = TileServiceRequestController(
-                qsTileHost,
+                qsHost,
                 commandQueue,
                 commandRegistry,
                 logger
@@ -107,18 +107,18 @@
 
     @Test
     fun tileAlreadyAdded_correctResult() {
-        `when`(qsTileHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
+        `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
 
         val callback = Callback()
         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
 
         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED)
-        verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
+        verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
     }
 
     @Test
     fun tileAlreadyAdded_logged() {
-        `when`(qsTileHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
+        `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
 
         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
 
@@ -157,7 +157,7 @@
 
         cancelListenerCaptor.value.onCancel(tileRequestDialog)
         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
-        verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
+        verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
     }
 
     @Test
@@ -191,7 +191,7 @@
         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
 
         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.ADD_TILE)
-        verify(qsTileHost).addTile(TEST_COMPONENT, /* end */ true)
+        verify(qsHost).addTile(TEST_COMPONENT, /* end */ true)
     }
 
     @Test
@@ -225,7 +225,7 @@
         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
 
         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DONT_ADD_TILE)
-        verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
+        verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
     }
 
     @Test
@@ -266,7 +266,7 @@
 
     @Test
     fun commandQueueCallback_callbackCalled() {
-        `when`(qsTileHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
+        `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
         val c = Callback()
@@ -365,4 +365,4 @@
             accept(r)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 172c87f..64e9a3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -30,7 +30,6 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Handler;
-import android.os.HandlerExecutor;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.quicksettings.IQSTileService;
@@ -39,24 +38,13 @@
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
-import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.PluginManager;
-import com.android.systemui.qs.QSTileHost;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.tileimpl.QSFactoryImpl;
-import com.android.systemui.settings.UserFileManager;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.phone.AutoTileManager;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
-import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.tuner.TunerService;
-import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -68,8 +56,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
-import java.util.Optional;
-import java.util.concurrent.Executor;
 
 import javax.inject.Provider;
 
@@ -92,26 +78,8 @@
     @Mock
     private StatusBarIconController mStatusBarIconController;
     @Mock
-    private QSFactoryImpl mQSFactory;
-    @Mock
-    private PluginManager mPluginManager;
-    @Mock
-    private  TunerService mTunerService;
-    @Mock
-    private AutoTileManager mAutoTileManager;
-    @Mock
-    private DumpManager mDumpManager;
-    @Mock
-    private CentralSurfaces mCentralSurfaces;
-    @Mock
-    private QSLogger mQSLogger;
-    @Mock
-    private UiEventLogger mUiEventLogger;
-    @Mock
     private UserTracker mUserTracker;
     @Mock
-    private SecureSettings  mSecureSettings;
-    @Mock
     private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder;
     @Mock
     private TileServiceRequestController mTileServiceRequestController;
@@ -122,12 +90,11 @@
     @Mock
     private TileLifecycleManager mTileLifecycleManager;
     @Mock
-    private UserFileManager mUserFileManager;
+    private QSHost mQSHost;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(BluetoothController.class);
         mManagers = new ArrayList<>();
         mTestableLooper = TestableLooper.get(this);
 
@@ -135,34 +102,16 @@
                 .thenReturn(mTileServiceRequestController);
         when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class)))
                 .thenReturn(mTileLifecycleManager);
+        when(mQSHost.getContext()).thenReturn(mContext);
 
         Provider<Handler> provider = () -> new Handler(mTestableLooper.getLooper());
-        Executor executor = new HandlerExecutor(provider.get());
 
-        QSTileHost host = new QSTileHost(mContext,
-                mStatusBarIconController,
-                mQSFactory,
-                executor,
-                mPluginManager,
-                mTunerService,
-                () -> mAutoTileManager,
-                mDumpManager,
-                Optional.of(mCentralSurfaces),
-                mQSLogger,
-                mUiEventLogger,
-                mUserTracker,
-                mSecureSettings,
-                mock(CustomTileStatePersister.class),
-                mTileServiceRequestControllerBuilder,
-                mTileLifecycleManagerFactory,
-                mUserFileManager);
-        mTileService = new TestTileServices(host, provider, mBroadcastDispatcher,
-                mUserTracker, mKeyguardStateController, mCommandQueue);
+        mTileService = new TestTileServices(mQSHost, provider, mBroadcastDispatcher,
+                mUserTracker, mKeyguardStateController, mCommandQueue, mStatusBarIconController);
     }
 
     @After
     public void tearDown() throws Exception {
-        mTileService.getHost().destroy();
         mTileService.destroy();
         TestableLooper.get(this).processAllMessages();
     }
@@ -274,11 +223,12 @@
     }
 
     private class TestTileServices extends TileServices {
-        TestTileServices(QSTileHost host, Provider<Handler> handlerProvider,
+        TestTileServices(QSHost host, Provider<Handler> handlerProvider,
                 BroadcastDispatcher broadcastDispatcher, UserTracker userTracker,
-                KeyguardStateController keyguardStateController, CommandQueue commandQueue) {
+                KeyguardStateController keyguardStateController, CommandQueue commandQueue,
+                StatusBarIconController statusBarIconController) {
             super(host, handlerProvider, broadcastDispatcher, userTracker, keyguardStateController,
-                    commandQueue);
+                    commandQueue, statusBarIconController);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
index ca3182a..e222542 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.qs.tiles.BluetoothTile
 import com.android.systemui.qs.tiles.CameraToggleTile
 import com.android.systemui.qs.tiles.CastTile
-import com.android.systemui.qs.tiles.CellularTile
 import com.android.systemui.qs.tiles.ColorCorrectionTile
 import com.android.systemui.qs.tiles.ColorInversionTile
 import com.android.systemui.qs.tiles.DataSaverTile
@@ -36,6 +35,7 @@
 import com.android.systemui.qs.tiles.DndTile
 import com.android.systemui.qs.tiles.DreamTile
 import com.android.systemui.qs.tiles.FlashlightTile
+import com.android.systemui.qs.tiles.FontScalingTile
 import com.android.systemui.qs.tiles.HotspotTile
 import com.android.systemui.qs.tiles.InternetTile
 import com.android.systemui.qs.tiles.LocationTile
@@ -49,24 +49,22 @@
 import com.android.systemui.qs.tiles.RotationLockTile
 import com.android.systemui.qs.tiles.ScreenRecordTile
 import com.android.systemui.qs.tiles.UiModeNightTile
-import com.android.systemui.qs.tiles.WifiTile
 import com.android.systemui.qs.tiles.WorkModeTile
 import com.android.systemui.util.leak.GarbageMonitor
 import com.google.common.truth.Truth.assertThat
+import javax.inject.Provider
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Answers
 import org.mockito.Mock
 import org.mockito.Mockito.inOrder
-import org.mockito.MockitoAnnotations
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 private val specMap = mapOf(
-        "wifi" to WifiTile::class.java,
         "internet" to InternetTile::class.java,
         "bt" to BluetoothTile::class.java,
-        "cell" to CellularTile::class.java,
         "dnd" to DndTile::class.java,
         "inversion" to ColorInversionTile::class.java,
         "airplane" to AirplaneModeTile::class.java,
@@ -91,7 +89,8 @@
         "qr_code_scanner" to QRCodeScannerTile::class.java,
         "onehanded" to OneHandedModeTile::class.java,
         "color_correction" to ColorCorrectionTile::class.java,
-        "dream" to DreamTile::class.java
+        "dream" to DreamTile::class.java,
+        "font_scaling" to FontScalingTile::class.java
 )
 
 @RunWith(AndroidTestingRunner::class)
@@ -102,10 +101,8 @@
     @Mock(answer = Answers.RETURNS_SELF) private lateinit var customTileBuilder: CustomTile.Builder
     @Mock private lateinit var customTile: CustomTile
 
-    @Mock private lateinit var wifiTile: WifiTile
     @Mock private lateinit var internetTile: InternetTile
     @Mock private lateinit var bluetoothTile: BluetoothTile
-    @Mock private lateinit var cellularTile: CellularTile
     @Mock private lateinit var dndTile: DndTile
     @Mock private lateinit var colorInversionTile: ColorInversionTile
     @Mock private lateinit var airplaneTile: AirplaneModeTile
@@ -132,6 +129,7 @@
     @Mock private lateinit var oneHandedModeTile: OneHandedModeTile
     @Mock private lateinit var colorCorrectionTile: ColorCorrectionTile
     @Mock private lateinit var dreamTile: DreamTile
+    @Mock private lateinit var fontScalingTile: FontScalingTile
 
     private lateinit var factory: QSFactoryImpl
 
@@ -143,41 +141,43 @@
         whenever(qsHost.userContext).thenReturn(mContext)
         whenever(customTileBuilder.build()).thenReturn(customTile)
 
+        val tileMap = mutableMapOf<String, Provider<QSTileImpl<*>>>(
+            "internet" to Provider { internetTile },
+            "bt" to Provider { bluetoothTile },
+            "dnd" to Provider { dndTile },
+            "inversion" to Provider { colorInversionTile },
+            "airplane" to Provider { airplaneTile },
+            "work" to Provider { workTile },
+            "rotation" to Provider { rotationTile },
+            "flashlight" to Provider { flashlightTile },
+            "location" to Provider { locationTile },
+            "cast" to Provider { castTile },
+            "hotspot" to Provider { hotspotTile },
+            "battery" to Provider { batterySaverTile },
+            "saver" to Provider { dataSaverTile },
+            "night" to Provider { nightDisplayTile },
+            "nfc" to Provider { nfcTile },
+            "dark" to Provider { darkModeTile },
+            "screenrecord" to Provider { screenRecordTile },
+            "reduce_brightness" to Provider { reduceBrightColorsTile },
+            "cameratoggle" to Provider { cameraToggleTile },
+            "mictoggle" to Provider { microphoneToggleTile },
+            "controls" to Provider { deviceControlsTile },
+            "alarm" to Provider { alarmTile },
+            "wallet" to Provider { quickAccessWalletTile },
+            "qr_code_scanner" to Provider { qrCodeScannerTile },
+            "onehanded" to Provider { oneHandedModeTile },
+            "color_correction" to Provider { colorCorrectionTile },
+            "dream" to Provider { dreamTile },
+            "font_scaling" to Provider { fontScalingTile }
+        )
+
         factory = QSFactoryImpl(
                 { qsHost },
                 { customTileBuilder },
-                { wifiTile },
-                { internetTile },
-                { bluetoothTile },
-                { cellularTile },
-                { dndTile },
-                { colorInversionTile },
-                { airplaneTile },
-                { workTile },
-                { rotationTile },
-                { flashlightTile },
-                { locationTile },
-                { castTile },
-                { hotspotTile },
-                { batterySaverTile },
-                { dataSaverTile },
-                { nightDisplayTile },
-                { nfcTile },
-                { memoryTile },
-                { darkModeTile },
-                { screenRecordTile },
-                { reduceBrightColorsTile },
-                { cameraToggleTile },
-                { microphoneToggleTile },
-                { deviceControlsTile },
-                { alarmTile },
-                { quickAccessWalletTile },
-                { qrCodeScannerTile },
-                { oneHandedModeTile },
-                { colorCorrectionTile },
-                { dreamTile }
+                tileMap,
         )
-        // When adding/removing tiles, fix also [specMap]
+        // When adding/removing tiles, fix also [specMap] and [tileMap]
     }
 
     @Test
@@ -213,4 +213,4 @@
         inOrder.verify(tile).initialize()
         inOrder.verify(tile).postStale()
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index ba49f3f..36549fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -69,7 +69,6 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSEvent;
 import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.statusbar.StatusBarState;
 
@@ -97,7 +96,7 @@
     private TestableLooper mTestableLooper;
     private TileImpl mTile;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     private final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
index 030c59f..5e0190b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.util.settings.GlobalSettings
 import com.google.common.truth.Truth.assertThat
 import dagger.Lazy
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -94,6 +95,12 @@
             mUserTracker)
     }
 
+    @After
+    fun tearDown() {
+        mTile.destroy()
+        mTestableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenDisabled_showsOffState() {
         val state = QSTile.BooleanState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt
index b4a66297..f1e3e8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -88,6 +89,12 @@
         testableLooper.processAllMessages()
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testAvailable() {
         assertThat(tile.isAvailable).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
index 95e7ad9f..a5c0004 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -103,6 +104,12 @@
         testableLooper.processAllMessages()
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testSettingWithCorrectUser() {
         assertEquals(USER, tile.mSetting.currentUser)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index d65901777..75fd000 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -17,11 +17,12 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.BluetoothController
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -40,7 +41,7 @@
     @Mock
     private lateinit var qsLogger: QSLogger
     @Mock
-    private lateinit var qsHost: QSTileHost
+    private lateinit var qsHost: QSHost
     @Mock
     private lateinit var metricsLogger: MetricsLogger
     private val falsingManager = FalsingManagerFake()
@@ -79,6 +80,12 @@
         testableLooper.processAllMessages()
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testRestrictionChecked() {
         tile.refreshState()
@@ -135,7 +142,7 @@
     }
 
     private class FakeBluetoothTile(
-        qsTileHost: QSTileHost,
+        qsHost: QSHost,
         backgroundLooper: Looper,
         mainHandler: Handler,
         falsingManager: FalsingManager,
@@ -145,7 +152,7 @@
         qsLogger: QSLogger,
         bluetoothController: BluetoothController
     ) : BluetoothTile(
-        qsTileHost,
+        qsHost,
         backgroundLooper,
         mainHandler,
         falsingManager,
@@ -187,4 +194,4 @@
         `when`(bluetoothController.isBluetoothConnected).thenReturn(false)
         `when`(bluetoothController.isBluetoothConnecting).thenReturn(true)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
index cfbb82f..4193854 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -90,6 +91,12 @@
                 keyguardStateController)
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenCameraAccessEnabled_isOnState() {
         val state = QSTile.BooleanState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
index b40a20c..64fd09d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
@@ -42,7 +42,7 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.statusbar.connectivity.IconState;
 import com.android.systemui.statusbar.connectivity.NetworkController;
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -78,7 +79,7 @@
     @Mock
     private NetworkController mNetworkController;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     SignalCallback mSignalCallback;
     @Mock
@@ -141,6 +142,12 @@
         mHotspotCallback = hotspotCallbackArgumentCaptor.getValue();
     }
 
+    @After
+    public void tearDown() {
+        mCastTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     // -------------------------------------------------
     // All these tests for enabled/disabled wifi have hotspot not enabled
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
index debe41c..13c30e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
@@ -37,12 +37,13 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -56,7 +57,7 @@
 public class ColorCorrectionTileTest extends SysuiTestCase {
 
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -101,6 +102,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void longClick_expectedAction() {
         final ArgumentCaptor<Intent> IntentCaptor = ArgumentCaptor.forClass(Intent.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
index 3fd2501..ff27e02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
@@ -39,13 +39,14 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -61,7 +62,7 @@
     private static final Integer COLOR_INVERSION_ENABLED = 1;
 
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -106,6 +107,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void longClick_expectedAction() {
         final ArgumentCaptor<Intent> IntentCaptor = ArgumentCaptor.forClass(Intent.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
index ce62f2d..b048643 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.DataSaverController
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -84,6 +85,12 @@
             )
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenDataSaverEnabled_isOnState() {
         val state = QSTile.BooleanState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
index e0b3125..b51c378 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
@@ -67,6 +67,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
 import java.util.Optional
+import org.junit.After
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -129,6 +130,12 @@
         tile = createTile()
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     private fun setupControlsComponent() {
         `when`(controlsComponent.getControlsController()).thenAnswer {
             if (featureEnabled) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index ce5edb1..6c0904e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -136,7 +136,8 @@
 
     @After
     fun tearDown() {
-        tile.handleSetListening(false)
+        tile.destroy()
+        testableLooper.processAllMessages()
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
index a13bece..13e4702 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
@@ -47,13 +47,14 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -69,7 +70,7 @@
     @Mock
     private ActivityStarter mActivityStarter;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -112,6 +113,12 @@
         mTile.initialize();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testNotAvailable() throws RemoteException {
         // Should not be available if screensaver is disabled
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
index d0f851b..692a644 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
@@ -13,11 +13,12 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.FlashlightController
 import com.google.common.truth.Truth
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -34,7 +35,7 @@
 
     @Mock private lateinit var qsLogger: QSLogger
 
-    @Mock private lateinit var qsHost: QSTileHost
+    @Mock private lateinit var qsHost: QSHost
 
     @Mock private lateinit var metricsLogger: MetricsLogger
 
@@ -71,6 +72,12 @@
             )
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenFlashlightEnabled_isOnState() {
         Mockito.`when`(flashlightController.isAvailable).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
new file mode 100644
index 0000000..1dd05c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.qs.tiles
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class FontScalingTileTest : SysuiTestCase() {
+    @Mock private lateinit var qsHost: QSTileHost
+    @Mock private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var qsLogger: QSLogger
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fontScalingTile: FontScalingTile
+
+    val featureFlags = FakeFeatureFlags()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+        `when`(qsHost.getContext()).thenReturn(mContext)
+        `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger)
+
+        fontScalingTile =
+            FontScalingTile(
+                qsHost,
+                testableLooper.looper,
+                Handler(testableLooper.looper),
+                FalsingManagerFake(),
+                metricsLogger,
+                statusBarStateController,
+                activityStarter,
+                qsLogger,
+                dialogLaunchAnimator,
+                FakeSettings(),
+                featureFlags
+            )
+        fontScalingTile.initialize()
+        testableLooper.processAllMessages()
+    }
+
+    @After
+    fun tearDown() {
+        fontScalingTile.destroy()
+        testableLooper.processAllMessages()
+    }
+
+    @Test
+    fun isAvailable_whenFlagIsFalse_returnsFalse() {
+        featureFlags.set(Flags.ENABLE_FONT_SCALING_TILE, false)
+
+        val isAvailable = fontScalingTile.isAvailable()
+
+        assertThat(isAvailable).isFalse()
+    }
+
+    @Test
+    fun isAvailable_whenFlagIsTrue_returnsTrue() {
+        featureFlags.set(Flags.ENABLE_FONT_SCALING_TILE, true)
+
+        val isAvailable = fontScalingTile.isAvailable()
+
+        assertThat(isAvailable).isTrue()
+    }
+
+    @Test
+    fun clickTile_showDialog() {
+        val view = View(context)
+        fontScalingTile.click(view)
+        testableLooper.processAllMessages()
+
+        verify(dialogLaunchAnimator).showFromView(any(), eq(view), nullable(), anyBoolean())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
index 451e911..959e750 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
@@ -37,12 +37,13 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.statusbar.policy.DataSaverController;
 import com.android.systemui.statusbar.policy.HotspotController;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -60,7 +61,7 @@
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private HotspotController mHotspotController;
     @Mock
@@ -94,6 +95,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void handleUpdateState_wifiTetheringIsAllowed_stateIsNotUnavailable() {
         MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
index 80c39cf..adfd7f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -21,7 +21,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-
 import android.os.Handler;
 import android.service.quicksettings.Tile;
 import android.testing.AndroidTestingRunner;
@@ -35,13 +34,16 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.statusbar.connectivity.AccessPointController;
 import com.android.systemui.statusbar.connectivity.IconState;
 import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.WifiIndicators;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,7 +56,7 @@
 public class InternetTileTest extends SysuiTestCase {
 
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private NetworkController mNetworkController;
     @Mock
@@ -90,6 +92,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void setConnectivityStatus_defaultNetworkNotExists_updateTile() {
         mTile.mSignalCallback.setConnectivityStatus(
@@ -135,4 +143,24 @@
         assertThat(mTile.getState().secondaryLabel)
             .isNotEqualTo(mContext.getString(R.string.status_bar_airplane));
     }
+
+    @Test
+    public void setIsAirplaneMode_APM_enabled_after_wifi_disconnected() {
+        WifiIndicators wifiIndicators = new WifiIndicators(
+            /* enabled= */ true,
+            /* statusIcon= */ null,
+            /* qsIcon= */ null,
+            /* activityIn= */ false,
+            /* activityOut= */ false,
+            /* description= */ null,
+            /* isTransient= */ false,
+            /* statusLabel= */ null
+        );
+        mTile.mSignalCallback.setWifiIndicators(wifiIndicators);
+        IconState state = new IconState(true, 0, "");
+        mTile.mSignalCallback.setIsAirplaneMode(state);
+        mTestableLooper.processAllMessages();
+        assertThat(mTile.getState().icon).isEqualTo(
+                QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
index d2bbc8c..33921c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
@@ -29,12 +29,13 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.policy.LocationController
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -52,7 +53,7 @@
     @Mock
     private lateinit var qsLogger: QSLogger
     @Mock
-    private lateinit var qsHost: QSTileHost
+    private lateinit var qsHost: QSHost
     @Mock
     private lateinit var metricsLogger: MetricsLogger
     private val falsingManager = FalsingManagerFake()
@@ -88,6 +89,12 @@
             keyguardStateController)
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenDisabled_isOffState() {
         val state = QSTile.BooleanState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
index 1ab601c..e2f64b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -90,6 +91,12 @@
                 keyguardStateController)
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenMicrophoneAccessEnabled_isOnState() {
         val state = QSTile.BooleanState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java
index cfd3735..c7dae83 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java
@@ -36,9 +36,10 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -60,7 +61,7 @@
     @Mock
     private ActivityStarter mActivityStarter;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -97,6 +98,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mNfcTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testIsAvailable_stockWithoutNfc_returnsFalse() {
         when(mMockContext.getString(R.string.quick_settings_tiles_stock)).thenReturn(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
index 188c3a3..04af69c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.LocationController
 import com.google.common.truth.Truth
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,32 +50,23 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 class NightDisplayTileTest : SysuiTestCase() {
-    @Mock
-    private lateinit var mHost: QSHost
+    @Mock private lateinit var mHost: QSHost
 
-    @Mock
-    private lateinit var mMetricsLogger: MetricsLogger
+    @Mock private lateinit var mMetricsLogger: MetricsLogger
 
-    @Mock
-    private lateinit var mStatusBarStateController: StatusBarStateController
+    @Mock private lateinit var mStatusBarStateController: StatusBarStateController
 
-    @Mock
-    private lateinit var mActivityStarter: ActivityStarter
+    @Mock private lateinit var mActivityStarter: ActivityStarter
 
-    @Mock
-    private lateinit var mQsLogger: QSLogger
+    @Mock private lateinit var mQsLogger: QSLogger
 
-    @Mock
-    private lateinit var mLocationController: LocationController
+    @Mock private lateinit var mLocationController: LocationController
 
-    @Mock
-    private lateinit var mColorDisplayManager: ColorDisplayManager
+    @Mock private lateinit var mColorDisplayManager: ColorDisplayManager
 
-    @Mock
-    private lateinit var mNightDisplayListenerBuilder: NightDisplayListenerModule.Builder
+    @Mock private lateinit var mNightDisplayListenerBuilder: NightDisplayListenerModule.Builder
 
-    @Mock
-    private lateinit var mNightDisplayListener: NightDisplayListener
+    @Mock private lateinit var mNightDisplayListener: NightDisplayListener
 
     private lateinit var mTestableLooper: TestableLooper
     private lateinit var mTile: NightDisplayTile
@@ -88,24 +80,30 @@
         whenever(mHost.context).thenReturn(mContext)
         whenever(mHost.uiEventLogger).thenReturn(mUiEventLogger)
         whenever(mHost.userContext).thenReturn(mContext)
-        whenever(mNightDisplayListenerBuilder.setUser(anyInt())).thenReturn(
-            mNightDisplayListenerBuilder
-        )
+        whenever(mNightDisplayListenerBuilder.setUser(anyInt()))
+            .thenReturn(mNightDisplayListenerBuilder)
         whenever(mNightDisplayListenerBuilder.build()).thenReturn(mNightDisplayListener)
 
-        mTile = NightDisplayTile(
-            mHost,
-            mTestableLooper.looper,
-            Handler(mTestableLooper.looper),
-            FalsingManagerFake(),
-            mMetricsLogger,
-            mStatusBarStateController,
-            mActivityStarter,
-            mQsLogger,
-            mLocationController,
-            mColorDisplayManager,
-            mNightDisplayListenerBuilder
-        )
+        mTile =
+            NightDisplayTile(
+                mHost,
+                mTestableLooper.looper,
+                Handler(mTestableLooper.looper),
+                FalsingManagerFake(),
+                mMetricsLogger,
+                mStatusBarStateController,
+                mActivityStarter,
+                mQsLogger,
+                mLocationController,
+                mColorDisplayManager,
+                mNightDisplayListenerBuilder
+            )
+    }
+
+    @After
+    fun tearDown() {
+        mTile.destroy()
+        mTestableLooper.processAllMessages()
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
index 8031875..652c138 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
@@ -32,11 +32,12 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,7 +54,7 @@
     @Mock
     private ActivityStarter mActivityStarter;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -91,6 +92,12 @@
         mTile.initialize();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testIsAvailable_unsupportOneHandedProperty_shouldReturnsFalse() {
         when(mTile.isSupportOneHandedMode()).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
index a1be2f3..3125d45 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
@@ -39,10 +39,11 @@
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,7 +55,7 @@
 @SmallTest
 public class QRCodeScannerTileTest extends SysuiTestCase {
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -91,6 +92,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testNewTile() {
         assertFalse(mTile.newTileState().handlesLongClick);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
index 4f6475f..596df78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
@@ -66,13 +66,14 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.wallet.controller.QuickAccessWalletController;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -99,7 +100,7 @@
             .setComponent(new ComponentName(mContext.getPackageName(), "WalletActivity"));
 
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -161,6 +162,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testNewTile() {
         assertFalse(mTile.newTileState().handlesLongClick);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
index 8601d6c..7913628 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
@@ -38,12 +38,13 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserTracker;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -55,7 +56,7 @@
 @SmallTest
 public class ReduceBrightColorsTileTest extends SysuiTestCase {
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -97,6 +98,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testNotActive() {
         when(mReduceBrightColorsController.isReduceBrightColorsActivated()).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
index e9dfd3e..5b94cfe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
@@ -38,7 +38,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -48,6 +48,7 @@
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.wrapper.RotationPolicyWrapper;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -71,7 +72,7 @@
     @Mock
     private ActivityStarter mActivityStarter;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
@@ -139,6 +140,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mLockTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testSecondaryString_cameraRotateOn_returnsFaceBased() {
         assertEquals(mContext.getString(R.string.rotation_lock_camera_rotation_on),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index 30debdf..5aef758 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -43,13 +43,14 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -65,7 +66,7 @@
     @Mock
     private RecordingController mController;
     @Mock
-    private QSTileHost mHost;
+    private QSHost mHost;
     @Mock
     private KeyguardDismissUtil mKeyguardDismissUtil;
     @Mock
@@ -114,6 +115,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     // Test that the tile is inactive and labeled correctly when the controller is neither starting
     // or recording, and that clicking on the tile in this state brings up the record prompt
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
index 0c070da..b556571 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
@@ -32,13 +32,14 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.qs.QSTileHost
+import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.LocationController
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -55,7 +56,7 @@
     @Mock private lateinit var uiModeManager: UiModeManager
     @Mock private lateinit var resources: Resources
     @Mock private lateinit var qsLogger: QSLogger
-    @Mock private lateinit var qsHost: QSTileHost
+    @Mock private lateinit var qsHost: QSHost
     @Mock private lateinit var metricsLogger: MetricsLogger
     @Mock private lateinit var statusBarStateController: StatusBarStateController
     @Mock private lateinit var activityStarter: ActivityStarter
@@ -98,6 +99,12 @@
             )
     }
 
+    @After
+    fun tearDown() {
+        tile.destroy()
+        testableLooper.processAllMessages()
+    }
+
     @Test
     fun testIcon_whenNightModeOn_isOnState() {
         val state = QSTile.BooleanState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index 08a90b7..18e40f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.qs.QSUserSwitcherEvent
-import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
@@ -42,7 +41,6 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -152,15 +150,6 @@
         assertNull(adapter.users.find { it.isManageUsers })
     }
 
-    @Test
-    fun clickDismissDialog() {
-        val shower: UserSwitchDialogController.DialogShower =
-            mock(UserSwitchDialogController.DialogShower::class.java)
-        adapter.injectDialogShower(shower)
-        adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
-        verify(shower).dismiss()
-    }
-
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
index 6d2972d..508327f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
@@ -879,6 +879,26 @@
         }
     }
 
+    @Test
+    public void getMobileNetworkSummary_withCarrierNetworkChange() {
+        Resources res = mock(Resources.class);
+        doReturn("Carrier network changing").when(res).getString(anyInt());
+        when(SubscriptionManager.getResourcesForSubId(any(), eq(SUB_ID))).thenReturn(res);
+        InternetDialogController spyController = spy(mInternetDialogController);
+        Map<Integer, TelephonyDisplayInfo> mSubIdTelephonyDisplayInfoMap =
+                spyController.mSubIdTelephonyDisplayInfoMap;
+        TelephonyDisplayInfo info = new TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_LTE,
+                TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
+
+        mSubIdTelephonyDisplayInfoMap.put(SUB_ID, info);
+        doReturn(true).when(spyController).isMobileDataEnabled();
+        doReturn(true).when(spyController).activeNetworkIsCellular();
+        spyController.mCarrierNetworkChangeMode = true;
+        String dds = spyController.getMobileNetworkSummary(SUB_ID);
+
+        assertThat(dds).contains(mContext.getString(R.string.carrier_network_change_mode));
+    }
+
     private String getResourcesString(String name) {
         return mContext.getResources().getString(getResourcesId(name));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index ea0e454..9acd47e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -16,14 +16,13 @@
 
 package com.android.systemui.reardisplay;
 
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
 import android.hardware.devicestate.DeviceStateManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.View;
+import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 
@@ -37,8 +36,6 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 
-import java.util.concurrent.Executor;
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -63,9 +60,11 @@
 
         controller.showRearDisplayDialog(CLOSED_BASE_STATE);
         assertTrue(controller.mRearDisplayEducationDialog.isShowing());
-        View deviceOpenedWarningTextView = controller.mRearDisplayEducationDialog.findViewById(
-                R.id.rear_display_warning_text_view);
-        assertNull(deviceOpenedWarningTextView);
+        TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+                R.id.rear_display_title_text_view);
+        assertEquals(deviceClosedTitleTextView.getText().toString(),
+                getContext().getResources().getString(
+                        R.string.rear_display_folded_bottom_sheet_title));
     }
 
     @Test
@@ -79,9 +78,11 @@
         controller.showRearDisplayDialog(OPEN_BASE_STATE);
 
         assertTrue(controller.mRearDisplayEducationDialog.isShowing());
-        View deviceOpenedWarningTextView = controller.mRearDisplayEducationDialog.findViewById(
-                R.id.rear_display_warning_text_view);
-        assertNotNull(deviceOpenedWarningTextView);
+        TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+                R.id.rear_display_title_text_view);
+        assertEquals(deviceClosedTitleTextView.getText().toString(),
+                getContext().getResources().getString(
+                        R.string.rear_display_unfolded_bottom_sheet_title));
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 69f3e987..33aaa3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -16,13 +16,15 @@
 
 package com.android.systemui.screenrecord;
 
+import static com.google.common.truth.Truth.assertThat;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.Intent;
 import android.os.Looper;
@@ -31,7 +33,13 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -61,8 +69,15 @@
     @Mock
     private UserContextProvider mUserContextProvider;
     @Mock
+    private ScreenCaptureDevicePolicyResolver mDevicePolicyResolver;
+    @Mock
+    private DialogLaunchAnimator mDialogLaunchAnimator;
+    @Mock
+    private ActivityStarter mActivityStarter;
+    @Mock
     private UserTracker mUserTracker;
 
+    private FakeFeatureFlags mFeatureFlags;
     private RecordingController mController;
 
     private static final int USER_ID = 10;
@@ -70,8 +85,9 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mController = new RecordingController(mMainExecutor, mBroadcastDispatcher,
-                mUserContextProvider, mUserTracker);
+        mFeatureFlags = new FakeFeatureFlags();
+        mController = new RecordingController(mMainExecutor, mBroadcastDispatcher, mContext,
+                mFeatureFlags, mUserContextProvider, () -> mDevicePolicyResolver, mUserTracker);
         mController.addCallback(mCallback);
     }
 
@@ -190,4 +206,67 @@
         verify(mCallback).onRecordingEnd();
         assertFalse(mController.isRecording());
     }
+
+    @Test
+    public void testPoliciesFlagDisabled_screenCapturingNotAllowed_returnsNullDevicePolicyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+        when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+    }
+
+    @Test
+    public void testPartialScreenSharingDisabled_returnsLegacyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, false);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenRecordDialog.class);
+    }
+
+    @Test
+    public void testPoliciesFlagEnabled_screenCapturingNotAllowed_returnsDevicePolicyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+        when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenCaptureDisabledDialog.class);
+    }
+
+    @Test
+    public void testPoliciesFlagEnabled_screenCapturingAllowed_returnsNullDevicePolicyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+        when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
index 0aa3621..5b094c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenrecord
 
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -59,6 +60,7 @@
         dialog =
             ScreenRecordPermissionDialog(
                 context,
+                UserHandle.of(0),
                 controller,
                 starter,
                 dialogLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
index b6a595b..7ba2cf7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
@@ -35,9 +35,33 @@
     @Test
     fun testCreateShareIntent() {
         val uri = Uri.parse("content://fake")
+
+        val output = ActionIntentCreator.createShareIntent(uri)
+
+        assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
+        assertFlagsSet(
+            Intent.FLAG_ACTIVITY_NEW_TASK or
+                Intent.FLAG_ACTIVITY_CLEAR_TASK or
+                Intent.FLAG_GRANT_READ_URI_PERMISSION,
+            output.flags
+        )
+
+        val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+        assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND)
+        assertThat(wrappedIntent?.data).isEqualTo(uri)
+        assertThat(wrappedIntent?.type).isEqualTo("image/png")
+        assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull()
+        assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isNull()
+        assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
+            .isEqualTo(uri)
+    }
+
+    @Test
+    fun testCreateShareIntentWithSubject() {
+        val uri = Uri.parse("content://fake")
         val subject = "Example subject"
 
-        val output = ActionIntentCreator.createShareIntent(uri, subject)
+        val output = ActionIntentCreator.createShareIntentWithSubject(uri, subject)
 
         assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
         assertFlagsSet(
@@ -52,16 +76,34 @@
         assertThat(wrappedIntent?.data).isEqualTo(uri)
         assertThat(wrappedIntent?.type).isEqualTo("image/png")
         assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo(subject)
+        assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isNull()
         assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
             .isEqualTo(uri)
     }
 
     @Test
-    fun testCreateShareIntent_noSubject() {
+    fun testCreateShareIntentWithExtraText() {
         val uri = Uri.parse("content://fake")
-        val output = ActionIntentCreator.createShareIntent(uri, null)
+        val extraText = "Extra text"
+
+        val output = ActionIntentCreator.createShareIntentWithExtraText(uri, extraText)
+
+        assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
+        assertFlagsSet(
+            Intent.FLAG_ACTIVITY_NEW_TASK or
+                Intent.FLAG_ACTIVITY_CLEAR_TASK or
+                Intent.FLAG_GRANT_READ_URI_PERMISSION,
+            output.flags
+        )
+
         val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+        assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND)
+        assertThat(wrappedIntent?.data).isEqualTo(uri)
+        assertThat(wrappedIntent?.type).isEqualTo("image/png")
         assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull()
+        assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(extraText)
+        assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
+            .isEqualTo(uri)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java
index e1eda11..d5014fa36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java
@@ -39,6 +39,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
@@ -67,6 +68,7 @@
     private PendingIntent mMockPendingIntent;
 
     private Intent mIntent;
+    private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Before
     public void setup() throws InterruptedException, ExecutionException, TimeoutException {
@@ -135,10 +137,11 @@
         if (withStatusBar) {
             return new ActionProxyReceiver(
                     Optional.of(mMockCentralSurfaces), mMockActivityManagerWrapper,
-                    mMockScreenshotSmartActions);
+                    mMockScreenshotSmartActions, mDisplayTracker);
         } else {
             return new ActionProxyReceiver(
-                    Optional.empty(), mMockActivityManagerWrapper, mMockScreenshotSmartActions);
+                    Optional.empty(), mMockActivityManagerWrapper, mMockScreenshotSmartActions,
+                    mDisplayTracker);
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
new file mode 100644
index 0000000..9f0a803
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -0,0 +1,143 @@
+package com.android.systemui.screenshot
+
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Guideline
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MessageContainerControllerTest : SysuiTestCase() {
+    lateinit var messageContainer: MessageContainerController
+
+    @Mock lateinit var workProfileMessageController: WorkProfileMessageController
+
+    @Mock lateinit var screenshotDetectionController: ScreenshotDetectionController
+
+    @Mock lateinit var icon: Drawable
+
+    lateinit var workProfileFirstRunView: ViewGroup
+    lateinit var detectionNoticeView: ViewGroup
+    lateinit var container: FrameLayout
+
+    var featureFlags = FakeFeatureFlags()
+    lateinit var screenshotView: ViewGroup
+
+    val userHandle = UserHandle.of(5)
+    val screenshotData = ScreenshotData.forTesting()
+
+    val appName = "app name"
+    lateinit var workProfileData: WorkProfileMessageController.WorkProfileFirstRunData
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        messageContainer =
+            MessageContainerController(
+                workProfileMessageController,
+                screenshotDetectionController,
+                featureFlags
+            )
+        screenshotView = ConstraintLayout(mContext)
+        workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
+
+        val guideline = Guideline(mContext)
+        guideline.id = com.android.systemui.R.id.guideline
+        screenshotView.addView(guideline)
+
+        container = FrameLayout(mContext)
+        container.id = com.android.systemui.R.id.screenshot_message_container
+        screenshotView.addView(container)
+
+        workProfileFirstRunView = FrameLayout(mContext)
+        workProfileFirstRunView.id = com.android.systemui.R.id.work_profile_first_run
+        container.addView(workProfileFirstRunView)
+
+        detectionNoticeView = FrameLayout(mContext)
+        detectionNoticeView.id = com.android.systemui.R.id.screenshot_detection_notice
+        container.addView(detectionNoticeView)
+
+        messageContainer.setView(screenshotView)
+
+        screenshotData.userHandle = userHandle
+    }
+
+    @Test
+    fun testOnScreenshotTakenUserHandle_noWorkProfileFirstRun() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+        // (just being explicit here)
+        whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(null)
+
+        messageContainer.onScreenshotTaken(userHandle)
+
+        verify(workProfileMessageController, never()).populateView(any(), any(), any())
+    }
+
+    @Test
+    fun testOnScreenshotTakenUserHandle_noWorkProfileFlag() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+        messageContainer.onScreenshotTaken(userHandle)
+
+        verify(workProfileMessageController, never()).onScreenshotTaken(any())
+        verify(workProfileMessageController, never()).populateView(any(), any(), any())
+    }
+
+    @Test
+    fun testOnScreenshotTakenUserHandle_withWorkProfileFirstRun() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+        whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle)))
+            .thenReturn(workProfileData)
+        messageContainer.onScreenshotTaken(userHandle)
+
+        verify(workProfileMessageController)
+            .populateView(eq(workProfileFirstRunView), eq(workProfileData), any())
+        assertEquals(View.VISIBLE, workProfileFirstRunView.visibility)
+        assertEquals(View.GONE, detectionNoticeView.visibility)
+    }
+
+    @Test
+    fun testOnScreenshotTakenScreenshotData_flagsOff() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+        featureFlags.set(Flags.SCREENSHOT_DETECTION, false)
+
+        messageContainer.onScreenshotTaken(screenshotData)
+
+        verify(workProfileMessageController, never()).onScreenshotTaken(any())
+        verify(screenshotDetectionController, never()).maybeNotifyOfScreenshot(any())
+
+        assertEquals(View.GONE, container.visibility)
+    }
+
+    @Test
+    fun testOnScreenshotTakenScreenshotData_nothingToShow() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+        featureFlags.set(Flags.SCREENSHOT_DETECTION, true)
+
+        messageContainer.onScreenshotTaken(screenshotData)
+
+        verify(workProfileMessageController, never()).populateView(any(), any(), any())
+        verify(screenshotDetectionController, never()).populateView(any(), any())
+
+        assertEquals(View.GONE, container.visibility)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 46a502a..2e73c0b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -22,15 +22,12 @@
 import android.graphics.Insets
 import android.graphics.Rect
 import android.hardware.HardwareBuffer
-import android.os.Bundle
 import android.os.UserHandle
-import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
-import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap
-import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
+import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
@@ -38,6 +35,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
+import org.junit.Assert
 import org.junit.Test
 
 private const val USER_ID = 1
@@ -49,7 +47,6 @@
     private val bounds = Rect(25, 25, 75, 75)
 
     private val scope = CoroutineScope(Dispatchers.Unconfined)
-    private val dispatcher = Dispatchers.Unconfined
     private val policy = FakeScreenshotPolicy()
     private val flags = FakeFeatureFlags()
 
@@ -58,7 +55,8 @@
     fun testProcessAsync() {
         flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
         var result: ScreenshotRequest? = null
@@ -76,17 +74,49 @@
         assertThat(result).isEqualTo(request)
     }
 
+    /** Tests the Java-compatible function wrapper, ensures callback is invoked. */
+    @Test
+    fun testProcessAsync_ScreenshotData() {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+        val request =
+            ScreenshotData.fromRequest(
+                ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
+            )
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        var result: ScreenshotData? = null
+        var callbackCount = 0
+        val callback: (ScreenshotData) -> Unit = { processedRequest: ScreenshotData ->
+            result = processedRequest
+            callbackCount++
+        }
+
+        // runs synchronously, using Unconfined Dispatcher
+        processor.processAsync(request, callback)
+
+        // Callback invoked once returning the same request (no changes)
+        assertThat(callbackCount).isEqualTo(1)
+        assertThat(result).isEqualTo(request)
+    }
+
     @Test
     fun testFullScreenshot_workProfilePolicyDisabled() = runBlocking {
         flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
         val processedRequest = processor.process(request)
 
         // No changes
         assertThat(processedRequest).isEqualTo(request)
+
+        val screenshotData = ScreenshotData.fromRequest(request)
+        val processedData = processor.process(screenshotData)
+
+        assertThat(processedData).isEqualTo(screenshotData)
     }
 
     @Test
@@ -97,9 +127,11 @@
         policy.setManagedProfile(USER_ID, false)
         policy.setDisplayContentInfo(
             policy.getDefaultDisplayId(),
-            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+        )
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build()
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
         val processedRequest = processor.process(request)
@@ -108,6 +140,13 @@
         assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
         assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER)
         assertThat(processedRequest.topComponent).isEqualTo(component)
+
+        val processedData = processor.process(ScreenshotData.fromRequest(request))
+
+        // Request has topComponent added, but otherwise unchanged.
+        assertThat(processedData.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+        assertThat(processedData.source).isEqualTo(SCREENSHOT_OTHER)
+        assertThat(processedData.topComponent).isEqualTo(component)
     }
 
     @Test
@@ -120,23 +159,64 @@
 
         // Indicate that the primary content belongs to a manged profile
         policy.setManagedProfile(USER_ID, true)
-        policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
-            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
+        policy.setDisplayContentInfo(
+            policy.getDefaultDisplayId(),
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+        )
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
         val processedRequest = processor.process(request)
 
         // Expect a task snapshot is taken, overriding the full screen mode
         assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
-        assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
+        assertThat(bitmap.equalsHardwareBitmap(processedRequest.bitmap)).isTrue()
         assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
         assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
         assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
         assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
         assertThat(processedRequest.userId).isEqualTo(USER_ID)
         assertThat(processedRequest.topComponent).isEqualTo(component)
+
+        val processedData = processor.process(ScreenshotData.fromRequest(request))
+
+        // Expect a task snapshot is taken, overriding the full screen mode
+        assertThat(processedData.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
+        assertThat(processedData.bitmap).isEqualTo(bitmap)
+        assertThat(processedData.screenBounds).isEqualTo(bounds)
+        assertThat(processedData.insets).isEqualTo(Insets.NONE)
+        assertThat(processedData.taskId).isEqualTo(TASK_ID)
+        assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
+        assertThat(processedRequest.userId).isEqualTo(USER_ID)
+        assertThat(processedRequest.topComponent).isEqualTo(component)
+    }
+
+    @Test
+    fun testFullScreenshot_managedProfile_nullBitmap() {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+        // Provide a null task bitmap when asked
+        imageCapture.image = null
+
+        // Indicate that the primary content belongs to a manged profile
+        policy.setManagedProfile(USER_ID, true)
+        policy.setDisplayContentInfo(
+            policy.getDefaultDisplayId(),
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+        )
+
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        Assert.assertThrows(IllegalStateException::class.java) {
+            runBlocking { processor.process(request) }
+        }
+        Assert.assertThrows(IllegalStateException::class.java) {
+            runBlocking { processor.process(ScreenshotData.fromRequest(request)) }
+        }
     }
 
     @Test
@@ -147,15 +227,26 @@
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
         val bitmap = makeHardwareBitmap(100, 100)
-        val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
-            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+                .setTopComponent(component)
+                .setTaskId(TASK_ID)
+                .setUserId(USER_ID)
+                .setBitmap(bitmap)
+                .setBoundsOnScreen(bounds)
+                .setInsets(Insets.NONE)
+                .build()
 
         val processedRequest = processor.process(request)
 
         // No changes
         assertThat(processedRequest).isEqualTo(request)
+
+        val screenshotData = ScreenshotData.fromRequest(request)
+        val processedData = processor.process(screenshotData)
+
+        assertThat(processedData).isEqualTo(screenshotData)
     }
 
     @Test
@@ -168,15 +259,26 @@
         policy.setManagedProfile(USER_ID, false)
 
         val bitmap = makeHardwareBitmap(100, 100)
-        val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
-            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+                .setTopComponent(component)
+                .setTaskId(TASK_ID)
+                .setUserId(USER_ID)
+                .setBitmap(bitmap)
+                .setBoundsOnScreen(bounds)
+                .setInsets(Insets.NONE)
+                .build()
 
         val processedRequest = processor.process(request)
 
         // No changes
         assertThat(processedRequest).isEqualTo(request)
+
+        val screenshotData = ScreenshotData.fromRequest(request)
+        val processedData = processor.process(screenshotData)
+
+        assertThat(processedData).isEqualTo(screenshotData)
     }
 
     @Test
@@ -190,26 +292,41 @@
         policy.setManagedProfile(USER_ID, true)
 
         val bitmap = makeHardwareBitmap(100, 100)
-        val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
-            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+                .setTopComponent(component)
+                .setTaskId(TASK_ID)
+                .setUserId(USER_ID)
+                .setBitmap(bitmap)
+                .setBoundsOnScreen(bounds)
+                .setInsets(Insets.NONE)
+                .build()
 
         val processedRequest = processor.process(request)
 
         // Work profile, but already a task snapshot, so no changes
         assertThat(processedRequest).isEqualTo(request)
+
+        val screenshotData = ScreenshotData.fromRequest(request)
+        val processedData = processor.process(screenshotData)
+
+        assertThat(processedData).isEqualTo(screenshotData)
     }
 
     private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
-        val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
-            HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
+        val buffer =
+            HardwareBuffer.create(
+                width,
+                height,
+                HardwareBuffer.RGBA_8888,
+                1,
+                HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+            )
         return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
     }
 
-    private fun Bitmap.equalsHardwareBitmapBundle(bundle: Bundle): Boolean {
-        val provided = bundleToHardwareBitmap(bundle)
-        return provided.hardwareBuffer == this.hardwareBuffer &&
-                provided.colorSpace == this.colorSpace
+    private fun Bitmap.equalsHardwareBitmap(bitmap: Bitmap): Boolean {
+        return bitmap.hardwareBuffer == this.hardwareBuffer && bitmap.colorSpace == this.colorSpace
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
new file mode 100644
index 0000000..43e9939
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.screenshot
+
+import android.content.ComponentName
+import android.graphics.Insets
+import android.graphics.Rect
+import android.os.UserHandle
+import android.view.WindowManager
+import com.android.internal.util.ScreenshotRequest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ScreenshotDataTest {
+    private val type = WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+    private val source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
+    private val bounds = Rect(1, 2, 3, 4)
+    private val taskId = 123
+    private val userId = 1
+    private val insets = Insets.of(1, 2, 3, 4)
+    private val component = ComponentName("android.test", "android.test.Component")
+
+    @Test
+    fun testConstruction() {
+        val request =
+            ScreenshotRequest.Builder(type, source)
+                .setBoundsOnScreen(bounds)
+                .setInsets(insets)
+                .setTaskId(taskId)
+                .setUserId(userId)
+                .setTopComponent(component)
+                .build()
+
+        val data = ScreenshotData.fromRequest(request)
+
+        assertThat(data.source).isEqualTo(source)
+        assertThat(data.type).isEqualTo(type)
+        assertThat(data.screenBounds).isEqualTo(bounds)
+        assertThat(data.insets).isEqualTo(insets)
+        assertThat(data.taskId).isEqualTo(taskId)
+        assertThat(data.userHandle).isEqualTo(UserHandle.of(userId))
+        assertThat(data.topComponent).isEqualTo(component)
+    }
+
+    @Test
+    fun testNegativeUserId() {
+        val request = ScreenshotRequest.Builder(type, source).setUserId(-1).build()
+
+        val data = ScreenshotData.fromRequest(request)
+
+        assertThat(data.userHandle).isNull()
+    }
+
+    @Test
+    fun testPackageNameAsString() {
+        val request = ScreenshotRequest.Builder(type, source).setTopComponent(component).build()
+
+        val data = ScreenshotData.fromRequest(request)
+
+        assertThat(data.packageNameString).isEqualTo("android.test")
+    }
+
+    @Test
+    fun testPackageNameAsString_null() {
+        val request = ScreenshotRequest.Builder(type, source).build()
+
+        val data = ScreenshotData.fromRequest(request)
+
+        assertThat(data.packageNameString).isEqualTo("")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
index 17396b1..e70fa2f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
@@ -31,6 +31,7 @@
 import android.testing.AndroidTestingRunner
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+import com.android.systemui.settings.FakeDisplayTracker
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
@@ -126,8 +127,10 @@
         val userManager = mock<UserManager>()
         val atmService = mock<IActivityTaskManager>()
         val dispatcher = Dispatchers.Unconfined
+        val displayTracker = FakeDisplayTracker(mContext)
 
-        return object : ScreenshotPolicyImpl(context, userManager, atmService, dispatcher) {
+        return object : ScreenshotPolicyImpl(context, userManager, atmService, dispatcher,
+                displayTracker) {
             override suspend fun isManagedProfile(userId: Int) = (userId == MANAGED_PROFILE_USER)
             override suspend fun getAllRootTaskInfosOnDisplay(displayId: Int) = tasks
             override suspend fun isNotificationShadeExpanded() = shadeExpanded
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index fa1fedb..c40c287 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -29,25 +29,26 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.testing.AndroidTestingRunner
-import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.util.ScreenshotHelper
-import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
+import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR
+import com.android.systemui.flags.Flags.SCREENSHOT_METADATA_REFACTOR
 import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
-import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argThat
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import java.util.function.Consumer
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -56,10 +57,10 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doThrow
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
 
 private const val USER_ID = 1
 private const val TASK_ID = 11
@@ -81,28 +82,51 @@
     private val flags = FakeFeatureFlags()
     private val topComponent = ComponentName(mContext, TakeScreenshotServiceTest::class.java)
 
-    private val service = TakeScreenshotService(
-        controller, userManager, devicePolicyManager, eventLogger,
-        notificationsController, mContext, Runnable::run, flags, requestProcessor)
+    private val service =
+        TakeScreenshotService(
+            controller,
+            userManager,
+            devicePolicyManager,
+            eventLogger,
+            notificationsController,
+            mContext,
+            Runnable::run,
+            flags,
+            requestProcessor
+        )
 
     @Before
     fun setUp() {
         whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
-        whenever(devicePolicyManager.getScreenCaptureDisabled(
-            /* admin component (null: any admin) */ isNull(), eq(UserHandle.USER_ALL)))
+        whenever(
+                devicePolicyManager.getScreenCaptureDisabled(
+                    /* admin component (null: any admin) */ isNull(),
+                    eq(UserHandle.USER_ALL)
+                )
+            )
             .thenReturn(false)
         whenever(userManager.isUserUnlocked).thenReturn(true)
 
         // Stub request processor as a synchronous no-op for tests with the flag enabled
         doAnswer {
-            val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
-            val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
-            consumer.accept(request)
-        }.`when`(requestProcessor).processAsync(/* request= */ any(), /* callback= */ any())
+                val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
+                val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
+                consumer.accept(request)
+            }
+            .whenever(requestProcessor)
+            .processAsync(/* request= */ any(ScreenshotRequest::class.java), /* callback= */ any())
+
+        doAnswer {
+                val request: ScreenshotData = it.getArgument(0) as ScreenshotData
+                val consumer: Consumer<ScreenshotData> = it.getArgument(1)
+                consumer.accept(request)
+            }
+            .whenever(requestProcessor)
+            .processAsync(/* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
 
         // Flipped in selected test cases
-        flags.set(SCREENSHOT_REQUEST_PROCESSOR, false)
         flags.set(SCREENSHOT_WORK_PROFILE_POLICY, false)
+        flags.set(SCREENSHOT_METADATA_REFACTOR, false)
 
         service.attach(
             mContext,
@@ -110,7 +134,8 @@
             /* className = */ null,
             /* token = */ null,
             application,
-            /* activityManager = */ null)
+            /* activityManager = */ null
+        )
     }
 
     @Test
@@ -127,128 +152,399 @@
 
     @Test
     fun takeScreenshotFullscreen() {
-        val request = ScreenshotRequest(
-            TAKE_SCREENSHOT_FULLSCREEN,
-            SCREENSHOT_KEY_CHORD,
-            topComponent)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
 
-        service.handleRequest(request, { /* onSaved */ }, callback)
+        service.handleRequest(request, { /* onSaved */}, callback)
 
-        verify(controller, times(1)).takeScreenshotFullscreen(
-            eq(topComponent),
-            /* onSavedListener = */ any(),
-            /* requestCallback = */ any())
+        verify(controller, times(1))
+            .takeScreenshotFullscreen(
+                eq(topComponent),
+                /* onSavedListener = */ any(),
+                /* requestCallback = */ any()
+            )
 
-        assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
+        assertEquals("Expected one UiEvent", 1, eventLogger.numLogs())
         val logEvent = eventLogger.get(0)
 
-        assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
-            logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id)
-        assertEquals("Expected supplied package name",
-            topComponent.packageName, eventLogger.get(0).packageName)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED UiEvent",
+            logEvent.eventId,
+            SCREENSHOT_REQUESTED_KEY_OTHER.id
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            eventLogger.get(0).packageName
+        )
     }
 
     @Test
-    fun takeScreenshot_requestProcessorEnabled() {
-        flags.set(SCREENSHOT_REQUEST_PROCESSOR, true)
+    fun takeScreenshotFullscreen_screenshotDataEnabled() {
+        flags.set(SCREENSHOT_METADATA_REFACTOR, true)
 
-        val request = ScreenshotRequest(
-            TAKE_SCREENSHOT_FULLSCREEN,
-            SCREENSHOT_KEY_CHORD,
-            topComponent)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
 
-        service.handleRequest(request, { /* onSaved */ }, callback)
+        service.handleRequest(request, { /* onSaved */}, callback)
 
-        verify(controller, times(1)).takeScreenshotFullscreen(
-            eq(topComponent),
-            /* onSavedListener = */ any(),
-            /* requestCallback = */ any())
+        verify(controller, times(1))
+            .handleScreenshot(
+                eq(ScreenshotData.fromRequest(request)),
+                /* onSavedListener = */ any(),
+                /* requestCallback = */ any()
+            )
 
         assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
         val logEvent = eventLogger.get(0)
 
-        assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
-            logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id)
-        assertEquals("Expected supplied package name",
-            topComponent.packageName, eventLogger.get(0).packageName)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED UiEvent",
+            logEvent.eventId,
+            SCREENSHOT_REQUESTED_KEY_OTHER.id
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            eventLogger.get(0).packageName
+        )
     }
 
     @Test
     fun takeScreenshotProvidedImage() {
         val bounds = Rect(50, 50, 150, 150)
         val bitmap = makeHardwareBitmap(100, 100)
-        val bitmapBundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW,
-            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, topComponent)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW)
+                .setTopComponent(topComponent)
+                .setTaskId(TASK_ID)
+                .setUserId(USER_ID)
+                .setBitmap(bitmap)
+                .setBoundsOnScreen(bounds)
+                .setInsets(Insets.NONE)
+                .build()
 
-        service.handleRequest(request, { /* onSaved */ }, callback)
+        service.handleRequest(request, { /* onSaved */}, callback)
 
-        verify(controller, times(1)).handleImageAsScreenshot(
-            argThat { b -> b.equalsHardwareBitmap(bitmap) },
-            eq(bounds),
-            eq(Insets.NONE), eq(TASK_ID), eq(USER_ID), eq(topComponent),
-            /* onSavedListener = */ any(), /* requestCallback = */ any())
+        verify(controller, times(1))
+            .handleImageAsScreenshot(
+                argThat { b -> b.equalsHardwareBitmap(bitmap) },
+                eq(bounds),
+                eq(Insets.NONE),
+                eq(TASK_ID),
+                eq(USER_ID),
+                eq(topComponent),
+                /* onSavedListener = */ any(),
+                /* requestCallback = */ any()
+            )
 
-        assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
+        assertEquals("Expected one UiEvent", 1, eventLogger.numLogs())
         val logEvent = eventLogger.get(0)
 
-        assertEquals("Expected SCREENSHOT_REQUESTED_* UiEvent",
-            logEvent.eventId, SCREENSHOT_REQUESTED_OVERVIEW.id)
-        assertEquals("Expected supplied package name",
-            topComponent.packageName, eventLogger.get(0).packageName)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            logEvent.eventId,
+            SCREENSHOT_REQUESTED_OVERVIEW.id
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            eventLogger.get(0).packageName
+        )
     }
 
     @Test
     fun takeScreenshotFullscreen_userLocked() {
+        flags.set(SCREENSHOT_METADATA_REFACTOR, true)
+
         whenever(userManager.isUserUnlocked).thenReturn(false)
 
-        val request = ScreenshotRequest(
-            TAKE_SCREENSHOT_FULLSCREEN,
-            SCREENSHOT_KEY_CHORD,
-            topComponent)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
 
-        service.handleRequest(request, { /* onSaved */ }, callback)
+        service.handleRequest(request, { /* onSaved */}, callback)
 
         verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
         verify(callback, times(1)).reportError()
         verifyZeroInteractions(controller)
+
+        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        val requestEvent = eventLogger.get(0)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            SCREENSHOT_REQUESTED_KEY_OTHER.id,
+            requestEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            requestEvent.packageName
+        )
+        val failureEvent = eventLogger.get(1)
+        assertEquals(
+            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+            SCREENSHOT_CAPTURE_FAILED.id,
+            failureEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            failureEvent.packageName
+        )
     }
 
     @Test
     fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
-        whenever(devicePolicyManager.getScreenCaptureDisabled(
-            isNull(), eq(UserHandle.USER_ALL))
-        ).thenReturn(true)
+        flags.set(SCREENSHOT_METADATA_REFACTOR, true)
 
-        whenever(devicePolicyResourcesManager.getString(
-            eq(SCREENSHOT_BLOCKED_BY_ADMIN),
-            /* Supplier<String> */ any(),
-        )).thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+        whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
+            .thenReturn(true)
 
-        val request = ScreenshotRequest(
-            TAKE_SCREENSHOT_FULLSCREEN,
-            SCREENSHOT_KEY_CHORD,
-            topComponent)
+        whenever(
+                devicePolicyResourcesManager.getString(
+                    eq(SCREENSHOT_BLOCKED_BY_ADMIN),
+                    /* Supplier<String> */
+                    any(),
+                )
+            )
+            .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
 
-        service.handleRequest(request, { /* onSaved */ }, callback)
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
+
+        service.handleRequest(request, { /* onSaved */}, callback)
 
         // error shown: Toast.makeText(...).show(), untestable
         verify(callback, times(1)).reportError()
         verifyZeroInteractions(controller)
+        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        val requestEvent = eventLogger.get(0)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            SCREENSHOT_REQUESTED_KEY_OTHER.id,
+            requestEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            requestEvent.packageName
+        )
+        val failureEvent = eventLogger.get(1)
+        assertEquals(
+            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+            SCREENSHOT_CAPTURE_FAILED.id,
+            failureEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            failureEvent.packageName
+        )
+    }
+
+    @Test
+    fun takeScreenshotFullscreen_userLocked_metadataDisabled() {
+        flags.set(SCREENSHOT_METADATA_REFACTOR, false)
+        whenever(userManager.isUserUnlocked).thenReturn(false)
+
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
+
+        service.handleRequest(request, { /* onSaved */}, callback)
+
+        verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+        verify(callback, times(1)).reportError()
+        verifyZeroInteractions(controller)
+
+        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        val requestEvent = eventLogger.get(0)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            SCREENSHOT_REQUESTED_KEY_OTHER.id,
+            requestEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            requestEvent.packageName
+        )
+        val failureEvent = eventLogger.get(1)
+        assertEquals(
+            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+            SCREENSHOT_CAPTURE_FAILED.id,
+            failureEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            failureEvent.packageName
+        )
+    }
+
+    @Test
+    fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers_metadataDisabled() {
+        flags.set(SCREENSHOT_METADATA_REFACTOR, false)
+
+        whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
+            .thenReturn(true)
+
+        whenever(
+                devicePolicyResourcesManager.getString(
+                    eq(SCREENSHOT_BLOCKED_BY_ADMIN),
+                    /* Supplier<String> */
+                    any(),
+                )
+            )
+            .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
+
+        service.handleRequest(request, { /* onSaved */}, callback)
+
+        // error shown: Toast.makeText(...).show(), untestable
+        verify(callback, times(1)).reportError()
+        verifyZeroInteractions(controller)
+        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        val requestEvent = eventLogger.get(0)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            SCREENSHOT_REQUESTED_KEY_OTHER.id,
+            requestEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            requestEvent.packageName
+        )
+        val failureEvent = eventLogger.get(1)
+        assertEquals(
+            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+            SCREENSHOT_CAPTURE_FAILED.id,
+            failureEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            failureEvent.packageName
+        )
+    }
+
+    @Test
+    fun takeScreenshot_workProfile_nullBitmap_metadataDisabled() {
+        flags.set(SCREENSHOT_METADATA_REFACTOR, false)
+
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
+
+        doThrow(IllegalStateException::class.java)
+            .whenever(requestProcessor)
+            .processAsync(any(ScreenshotRequest::class.java), any())
+
+        service.handleRequest(request, { /* onSaved */}, callback)
+
+        verify(callback, times(1)).reportError()
+        verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+        verifyZeroInteractions(controller)
+        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        val requestEvent = eventLogger.get(0)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            SCREENSHOT_REQUESTED_KEY_OTHER.id,
+            requestEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            requestEvent.packageName
+        )
+        val failureEvent = eventLogger.get(1)
+        assertEquals(
+            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+            SCREENSHOT_CAPTURE_FAILED.id,
+            failureEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            failureEvent.packageName
+        )
+    }
+    @Test
+    fun takeScreenshot_workProfile_nullBitmap() {
+        flags.set(SCREENSHOT_METADATA_REFACTOR, true)
+
+        val request =
+            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+                .setTopComponent(topComponent)
+                .build()
+
+        doThrow(IllegalStateException::class.java)
+            .whenever(requestProcessor)
+            .processAsync(any(ScreenshotData::class.java), any())
+
+        service.handleRequest(request, { /* onSaved */}, callback)
+
+        verify(callback, times(1)).reportError()
+        verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+        verifyZeroInteractions(controller)
+        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        val requestEvent = eventLogger.get(0)
+        assertEquals(
+            "Expected SCREENSHOT_REQUESTED_* UiEvent",
+            SCREENSHOT_REQUESTED_KEY_OTHER.id,
+            requestEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            requestEvent.packageName
+        )
+        val failureEvent = eventLogger.get(1)
+        assertEquals(
+            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+            SCREENSHOT_CAPTURE_FAILED.id,
+            failureEvent.eventId
+        )
+        assertEquals(
+            "Expected supplied package name",
+            topComponent.packageName,
+            failureEvent.packageName
+        )
     }
 }
 
 private fun Bitmap.equalsHardwareBitmap(other: Bitmap): Boolean {
     return config == HARDWARE &&
-            other.config == HARDWARE &&
-            hardwareBuffer == other.hardwareBuffer &&
-            colorSpace == other.colorSpace
+        other.config == HARDWARE &&
+        hardwareBuffer == other.hardwareBuffer &&
+        colorSpace == other.colorSpace
 }
 
 /** A hardware Bitmap is mandated by use of ScreenshotHelper.HardwareBitmapBundler */
 private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
-    val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
-        HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
+    val buffer =
+        HardwareBuffer.create(
+            width,
+            height,
+            HardwareBuffer.RGBA_8888,
+            1,
+            HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+        )
     return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
new file mode 100644
index 0000000..3440f91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.testing.AndroidTestingRunner;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.FakeSharedPreferences;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import kotlin.Unit;
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class WorkProfileMessageControllerTest extends SysuiTestCase {
+    private static final String DEFAULT_LABEL = "default label";
+    private static final String APP_LABEL = "app label";
+    private static final UserHandle NON_WORK_USER = UserHandle.of(0);
+    private static final UserHandle WORK_USER = UserHandle.of(10);
+
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private Drawable mActivityIcon;
+    @Mock
+    private Drawable mBadgedActivityIcon;
+    @Mock
+    private ActivityInfo mActivityInfo;
+
+    private FakeSharedPreferences mSharedPreferences = new FakeSharedPreferences();
+
+    private WorkProfileMessageController mMessageController;
+
+    @Before
+    public void setup() throws PackageManager.NameNotFoundException {
+        MockitoAnnotations.initMocks(this);
+
+        when(mUserManager.isManagedProfile(eq(WORK_USER.getIdentifier()))).thenReturn(true);
+        when(mMockContext.getSharedPreferences(
+                eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
+                eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
+        when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
+        when(mPackageManager.getActivityIcon(any(ComponentName.class)))
+                .thenReturn(mActivityIcon);
+        when(mPackageManager.getUserBadgedIcon(
+                any(), any())).thenReturn(mBadgedActivityIcon);
+        when(mPackageManager.getActivityInfo(any(),
+                any(PackageManager.ComponentInfoFlags.class))).thenReturn(mActivityInfo);
+        when(mActivityInfo.loadLabel(eq(mPackageManager))).thenReturn(APP_LABEL);
+
+        mSharedPreferences.edit().putBoolean(
+                WorkProfileMessageController.PREFERENCE_KEY, false).apply();
+
+        mMessageController = new WorkProfileMessageController(mMockContext, mUserManager,
+                mPackageManager);
+    }
+
+    @Test
+    public void testOnScreenshotTaken_notManaged() {
+        assertNull(mMessageController.onScreenshotTaken(NON_WORK_USER));
+    }
+
+    @Test
+    public void testOnScreenshotTaken_alreadyDismissed() {
+        mSharedPreferences.edit().putBoolean(
+                WorkProfileMessageController.PREFERENCE_KEY, true).apply();
+
+        assertNull(mMessageController.onScreenshotTaken(WORK_USER));
+    }
+
+    @Test
+    public void testOnScreenshotTaken_packageNotFound()
+            throws PackageManager.NameNotFoundException {
+        when(mPackageManager.getActivityInfo(any(),
+                any(PackageManager.ComponentInfoFlags.class))).thenThrow(
+                new PackageManager.NameNotFoundException());
+
+        WorkProfileMessageController.WorkProfileFirstRunData data =
+                mMessageController.onScreenshotTaken(WORK_USER);
+
+        assertEquals(DEFAULT_LABEL, data.getAppName());
+        assertNull(data.getIcon());
+    }
+
+    @Test
+    public void testOnScreenshotTaken() {
+        WorkProfileMessageController.WorkProfileFirstRunData data =
+                mMessageController.onScreenshotTaken(WORK_USER);
+
+        assertEquals(APP_LABEL, data.getAppName());
+        assertEquals(mBadgedActivityIcon, data.getIcon());
+    }
+
+    @Test
+    public void testPopulateView() throws InterruptedException {
+        ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate(
+                R.layout.screenshot_work_profile_first_run, null);
+        WorkProfileMessageController.WorkProfileFirstRunData data =
+                new WorkProfileMessageController.WorkProfileFirstRunData(APP_LABEL,
+                        mBadgedActivityIcon);
+        final CountDownLatch countdown = new CountDownLatch(1);
+        mMessageController.populateView(layout, data, () -> {
+            countdown.countDown();
+            return Unit.INSTANCE;
+        });
+
+        ImageView image = layout.findViewById(R.id.screenshot_message_icon);
+        assertEquals(mBadgedActivityIcon, image.getDrawable());
+        TextView text = layout.findViewById(R.id.screenshot_message_content);
+        // The app name is used in a template, but at least validate that it was inserted.
+        assertTrue(text.getText().toString().contains(APP_LABEL));
+
+        // Validate that clicking the dismiss button calls back properly.
+        assertEquals(1, countdown.getCount());
+        layout.findViewById(R.id.message_dismiss_button).callOnClick();
+        countdown.await(1000, TimeUnit.MILLISECONDS);
+    }
+}
+
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/DisplayTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/DisplayTrackerImplTest.kt
new file mode 100644
index 0000000..ae976a0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/DisplayTrackerImplTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS
+import android.hardware.display.DisplayManagerGlobal
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DisplayTrackerImplTest : SysuiTestCase() {
+    @Mock private lateinit var displayManager: DisplayManager
+    @Mock private lateinit var handler: Handler
+
+    private val executor = Executor(Runnable::run)
+    private lateinit var mDefaultDisplay: Display
+    private lateinit var mSecondaryDisplay: Display
+    private lateinit var tracker: DisplayTrackerImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        mDefaultDisplay =
+            Display(
+                DisplayManagerGlobal.getInstance(),
+                Display.DEFAULT_DISPLAY,
+                DisplayInfo(),
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+            )
+        mSecondaryDisplay =
+            Display(
+                DisplayManagerGlobal.getInstance(),
+                Display.DEFAULT_DISPLAY + 1,
+                DisplayInfo(),
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+            )
+
+        `when`(displayManager.displays).thenReturn(arrayOf(mDefaultDisplay, mSecondaryDisplay))
+
+        tracker = DisplayTrackerImpl(displayManager, handler)
+    }
+
+    @Test
+    fun testGetDefaultDisplay() {
+        assertThat(tracker.defaultDisplayId).isEqualTo(Display.DEFAULT_DISPLAY)
+    }
+
+    @Test
+    fun testGetAllDisplays() {
+        assertThat(tracker.allDisplays).isEqualTo(arrayOf(mDefaultDisplay, mSecondaryDisplay))
+    }
+
+    @Test
+    fun registerCallback_registersDisplayListener() {
+        tracker.addDisplayChangeCallback(TestCallback(), executor)
+        verify(displayManager).registerDisplayListener(any(), any())
+    }
+
+    @Test
+    fun registerBrightnessCallback_registersDisplayListener() {
+        tracker.addBrightnessChangeCallback(TestCallback(), executor)
+        verify(displayManager)
+            .registerDisplayListener(any(), any(), eq(EVENT_FLAG_DISPLAY_BRIGHTNESS))
+    }
+
+    @Test
+    fun unregisterCallback_displayListenerStillRegistered() {
+        val callback1 = TestCallback()
+        tracker.addDisplayChangeCallback(callback1, executor)
+        tracker.addDisplayChangeCallback(TestCallback(), executor)
+        tracker.removeCallback(callback1)
+
+        verify(displayManager, never()).unregisterDisplayListener(any())
+    }
+
+    @Test
+    fun unregisterLastCallback_unregistersDisplayListener() {
+        val callback = TestCallback()
+        tracker.addDisplayChangeCallback(callback, executor)
+        tracker.removeCallback(callback)
+
+        verify(displayManager).unregisterDisplayListener(any())
+    }
+
+    @Test
+    fun callbackCalledOnDisplayAdd() {
+        val testDisplay = 2
+        val callback = TestCallback()
+        tracker.addDisplayChangeCallback(callback, executor)
+        tracker.displayChangedListener.onDisplayAdded(testDisplay)
+
+        assertThat(callback.lastDisplayAdded).isEqualTo(testDisplay)
+    }
+
+    @Test
+    fun callbackCalledOnDisplayRemoved() {
+        val testDisplay = 2
+        val callback = TestCallback()
+        tracker.addDisplayChangeCallback(callback, executor)
+        tracker.displayChangedListener.onDisplayRemoved(testDisplay)
+
+        assertThat(callback.lastDisplayRemoved).isEqualTo(testDisplay)
+    }
+
+    @Test
+    fun callbackCalledOnDisplayChanged() {
+        val testDisplay = 2
+        val callback = TestCallback()
+        tracker.addDisplayChangeCallback(callback, executor)
+        tracker.displayChangedListener.onDisplayChanged(testDisplay)
+
+        assertThat(callback.lastDisplayChanged).isEqualTo(testDisplay)
+    }
+
+    @Test
+    fun callbackCalledOnBrightnessChanged() {
+        val testDisplay = 2
+        val callback = TestCallback()
+        tracker.addBrightnessChangeCallback(callback, executor)
+        tracker.displayBrightnessChangedListener.onDisplayChanged(testDisplay)
+
+        assertThat(callback.lastDisplayChanged).isEqualTo(testDisplay)
+    }
+
+    private class TestCallback : DisplayTracker.Callback {
+        var lastDisplayAdded = -1
+        var lastDisplayRemoved = -1
+        var lastDisplayChanged = -1
+
+        override fun onDisplayAdded(displayId: Int) {
+            lastDisplayAdded = displayId
+        }
+
+        override fun onDisplayRemoved(displayId: Int) {
+            lastDisplayRemoved = displayId
+        }
+
+        override fun onDisplayChanged(displayId: Int) {
+            lastDisplayChanged = displayId
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
index 020a866..3fd19ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
@@ -30,12 +30,14 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.nio.file.Files
 import java.util.concurrent.Executor
-import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.isNull
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
@@ -61,36 +63,83 @@
             UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor)
     }
 
-    @After
-    fun end() {
-        val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID)
-        dir.deleteRecursively()
-    }
-
     @Test
     fun testGetFile() {
         assertThat(userFileManager.getFile(TEST_FILE_NAME, 0).path)
             .isEqualTo("${context.filesDir}/$TEST_FILE_NAME")
         assertThat(userFileManager.getFile(TEST_FILE_NAME, 11).path)
-            .isEqualTo("${context.filesDir}/${UserFileManagerImpl.ID}/11/files/$TEST_FILE_NAME")
+            .isEqualTo("${context.filesDir}/__USER_11_$TEST_FILE_NAME")
     }
 
     @Test
     fun testGetSharedPreferences() {
+        val primarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 0)
         val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11)
-        val secondaryUserDir =
-            Environment.buildPath(
-                context.filesDir,
-                UserFileManagerImpl.ID,
-                "11",
-                UserFileManagerImpl.SHARED_PREFS,
-                TEST_FILE_NAME
-            )
 
-        assertThat(secondarySharedPref).isNotNull()
-        assertThat(secondaryUserDir.exists())
-        assertThat(userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 0))
-            .isNotEqualTo(secondarySharedPref)
+        assertThat(primarySharedPref).isNotEqualTo(secondarySharedPref)
+
+        // Make sure these are different files
+        primarySharedPref.edit().putString("TEST", "ABC").commit()
+        assertThat(secondarySharedPref.getString("TEST", null)).isNull()
+
+        context.deleteSharedPreferences("TEST")
+        context.deleteSharedPreferences("__USER_11_TEST")
+    }
+
+    @Test
+    fun testMigrateFile() {
+        val userId = 12
+        val fileName = "myFile.txt"
+        val fileContents = "TestingFile"
+        val legacyFile =
+            UserFileManagerImpl.createLegacyFile(
+                context,
+                UserFileManagerImpl.FILES,
+                fileName,
+                userId
+            )!!
+
+        // Write file to legacy area
+        Files.createDirectories(legacyFile.getParentFile().toPath())
+        Files.write(legacyFile.toPath(), fileContents.toByteArray())
+        assertThat(legacyFile.exists()).isTrue()
+
+        // getFile() should migrate the legacy file to the new location
+        val file = userFileManager.getFile(fileName, userId)
+        val newContents = String(Files.readAllBytes(file.toPath()))
+
+        assertThat(newContents).isEqualTo(fileContents)
+        assertThat(legacyFile.exists()).isFalse()
+        assertThat(File(context.filesDir, UserFileManagerImpl.ROOT_DIR).exists()).isFalse()
+    }
+
+    @Test
+    fun testMigrateSharedPrefs() {
+        val userId = 13
+        val fileName = "myFile"
+        val contents = "TestingSharedPrefs"
+        val legacyFile =
+            UserFileManagerImpl.createLegacyFile(
+                context,
+                UserFileManagerImpl.SHARED_PREFS,
+                "$fileName.xml",
+                userId
+            )!!
+
+        // Write a valid shared prefs xml file to legacy area
+        val tmpPrefs = context.getSharedPreferences("tmp", Context.MODE_PRIVATE)
+        tmpPrefs.edit().putString(contents, contents).commit()
+        Files.createDirectories(legacyFile.getParentFile().toPath())
+        val tmpFile =
+            Environment.buildPath(context.dataDir, UserFileManagerImpl.SHARED_PREFS, "tmp.xml")
+        tmpFile.renameTo(legacyFile)
+        assertThat(legacyFile.exists()).isTrue()
+
+        // getSharedpreferences() should migrate the legacy file to the new location
+        val prefs = userFileManager.getSharedPreferences(fileName, Context.MODE_PRIVATE, userId)
+        assertThat(prefs.getString(contents, "")).isEqualTo(contents)
+        assertThat(legacyFile.exists()).isFalse()
+        assertThat(File(context.filesDir, UserFileManagerImpl.ROOT_DIR).exists()).isFalse()
     }
 
     @Test
@@ -111,44 +160,14 @@
 
     @Test
     fun testClearDeletedUserData() {
-        val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID, "11", "files")
-        dir.mkdirs()
-        val file =
-            Environment.buildPath(
-                context.filesDir,
-                UserFileManagerImpl.ID,
-                "11",
-                "files",
-                TEST_FILE_NAME
-            )
-        val secondaryUserDir =
-            Environment.buildPath(
-                context.filesDir,
-                UserFileManagerImpl.ID,
-                "11",
-            )
+        val file = userFileManager.getFile(TEST_FILE_NAME, 11)
         file.createNewFile()
-        assertThat(secondaryUserDir.exists()).isTrue()
+
         assertThat(file.exists()).isTrue()
         userFileManager.clearDeletedUserData()
         assertThat(backgroundExecutor.runAllReady()).isGreaterThan(0)
-        verify(userManager).aliveUsers
-        assertThat(secondaryUserDir.exists()).isFalse()
-        assertThat(file.exists()).isFalse()
-    }
+        verify(userManager, atLeastOnce()).aliveUsers
 
-    @Test
-    fun testEnsureParentDirExists() {
-        val file =
-            Environment.buildPath(
-                context.filesDir,
-                UserFileManagerImpl.ID,
-                "11",
-                "files",
-                TEST_FILE_NAME
-            )
-        assertThat(file.parentFile.exists()).isFalse()
-        UserFileManagerImpl.ensureParentDirExists(file)
-        assertThat(file.parentFile.exists()).isTrue()
+        assertThat(file.exists()).isFalse()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
index 3710281..57b6b2b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.settings
 
+import android.app.IActivityManager
 import android.content.Context
 import android.content.Intent
 import android.content.pm.UserInfo
@@ -51,6 +52,7 @@
 
     @Mock private lateinit var context: Context
     @Mock private lateinit var userManager: UserManager
+    @Mock private lateinit var iActivityManager: IActivityManager
     @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager
     @Mock(stubOnly = true) private lateinit var handler: Handler
 
@@ -67,7 +69,7 @@
         `when`(context.user).thenReturn(UserHandle.SYSTEM)
         `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context)
 
-        tracker = UserTrackerImpl(context, userManager, dumpManager, handler)
+        tracker = UserTrackerImpl(context, userManager, iActivityManager, dumpManager, handler)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index e65bbb1..71ba215 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.settings
 
+import android.app.IActivityManager
+import android.app.IUserSwitchObserver
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.content.pm.UserInfo
 import android.os.Handler
+import android.os.IRemoteCallback
 import android.os.UserHandle
 import android.os.UserManager
 import android.testing.AndroidTestingRunner
@@ -29,19 +32,20 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.util.mockito.capture
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -51,6 +55,10 @@
     private lateinit var context: Context
     @Mock
     private lateinit var userManager: UserManager
+    @Mock
+    private lateinit var iActivityManager: IActivityManager
+    @Mock
+    private lateinit var userSwitchingReply: IRemoteCallback
     @Mock(stubOnly = true)
     private lateinit var dumpManager: DumpManager
     @Mock(stubOnly = true)
@@ -76,7 +84,7 @@
             listOf(info)
         }
 
-        tracker = UserTrackerImpl(context, userManager, dumpManager, handler)
+        tracker = UserTrackerImpl(context, userManager, iActivityManager, dumpManager, handler)
     }
 
     @Test
@@ -125,8 +133,7 @@
         verify(context).registerReceiverForAllUsers(
                 eq(tracker), capture(captor), isNull(), eq(handler))
         with(captor.value) {
-            assertThat(countActions()).isEqualTo(7)
-            assertThat(hasAction(Intent.ACTION_USER_SWITCHED)).isTrue()
+            assertThat(countActions()).isEqualTo(6)
             assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue()
             assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
             assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
@@ -158,8 +165,10 @@
         tracker.initialize(0)
         val newID = 5
 
-        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, newID)
-        tracker.onReceive(context, intent)
+        val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+        verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+        captor.value.onUserSwitching(newID, userSwitchingReply)
+        verify(userSwitchingReply).sendResult(any())
 
         verify(userManager).getProfiles(newID)
 
@@ -272,6 +281,24 @@
     }
 
     @Test
+    fun testCallbackCalledOnUserChanging() {
+        tracker.initialize(0)
+        val callback = TestCallback()
+        tracker.addCallback(callback, executor)
+
+        val newID = 5
+
+        val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+        verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+        captor.value.onUserSwitching(newID, userSwitchingReply)
+        verify(userSwitchingReply).sendResult(any())
+
+        assertThat(callback.calledOnUserChanging).isEqualTo(1)
+        assertThat(callback.lastUser).isEqualTo(newID)
+        assertThat(callback.lastUserContext?.userId).isEqualTo(newID)
+    }
+
+    @Test
     fun testCallbackCalledOnUserChanged() {
         tracker.initialize(0)
         val callback = TestCallback()
@@ -279,8 +306,9 @@
 
         val newID = 5
 
-        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, newID)
-        tracker.onReceive(context, intent)
+        val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+        verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+        captor.value.onUserSwitchComplete(newID)
 
         assertThat(callback.calledOnUserChanged).isEqualTo(1)
         assertThat(callback.lastUser).isEqualTo(newID)
@@ -330,25 +358,36 @@
         tracker.addCallback(callback, executor)
         tracker.removeCallback(callback)
 
-        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, 5)
-        tracker.onReceive(context, intent)
+        val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+        verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+        captor.value.onUserSwitching(newID, userSwitchingReply)
+        verify(userSwitchingReply).sendResult(any())
+        captor.value.onUserSwitchComplete(newID)
 
         val intentProfiles = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
                 .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
 
         tracker.onReceive(context, intentProfiles)
 
+        assertThat(callback.calledOnUserChanging).isEqualTo(0)
         assertThat(callback.calledOnUserChanged).isEqualTo(0)
         assertThat(callback.calledOnProfilesChanged).isEqualTo(0)
     }
 
     private class TestCallback : UserTracker.Callback {
+        var calledOnUserChanging = 0
         var calledOnUserChanged = 0
         var calledOnProfilesChanged = 0
         var lastUser: Int? = null
         var lastUserContext: Context? = null
         var lastUserProfiles = emptyList<UserInfo>()
 
+        override fun onUserChanging(newUser: Int, userContext: Context) {
+            calledOnUserChanging++
+            lastUser = newUser
+            lastUserContext = userContext
+        }
+
         override fun onUserChanged(newUser: Int, userContext: Context) {
             calledOnUserChanged++
             lastUser = newUser
@@ -360,4 +399,4 @@
             lastUserProfiles = profiles
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index 9d1802a..58ade49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -28,6 +28,7 @@
 import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
@@ -51,6 +52,7 @@
     @Mock private lateinit var mainExecutor: Executor
     @Mock private lateinit var backgroundHandler: Handler
     @Mock private lateinit var brightnessSliderController: BrightnessSliderController
+    private val displayTracker = FakeDisplayTracker(mContext)
 
     @Rule
     @JvmField
@@ -60,6 +62,7 @@
                 override fun create(intent: Intent?): TestDialog {
                     return TestDialog(
                         userTracker,
+                        displayTracker,
                         brightnessSliderControllerFactory,
                         mainExecutor,
                         backgroundHandler
@@ -105,12 +108,14 @@
 
     class TestDialog(
         userTracker: UserTracker,
+        displayTracker: FakeDisplayTracker,
         brightnessSliderControllerFactory: BrightnessSliderController.Factory,
         mainExecutor: Executor,
         backgroundHandler: Handler
     ) :
         BrightnessDialog(
             userTracker,
+            displayTracker,
             brightnessSliderControllerFactory,
             mainExecutor,
             backgroundHandler
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index f802a5e..3706859 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -90,7 +90,7 @@
     fun testEdgeElementsAlignedWithEdgeOrGuide_qs() {
         with(qsConstraint) {
             assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
-            assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
+            assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0.5f)
 
             assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID)
             assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0.5f)
@@ -109,11 +109,12 @@
     @Test
     fun testEdgeElementsAlignedWithEdge_largeScreen() {
         with(largeScreenConstraint) {
-            assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
-            assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
+            assertThat(getConstraint(R.id.clock).layout.startToEnd).isEqualTo(R.id.begin_guide)
+            assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0.5f)
 
-            assertThat(getConstraint(R.id.privacy_container).layout.endToEnd).isEqualTo(PARENT_ID)
-            assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f)
+            assertThat(getConstraint(R.id.privacy_container).layout.endToStart)
+                .isEqualTo(R.id.end_guide)
+            assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(0.5f)
         }
     }
 
@@ -219,7 +220,12 @@
                 .isEqualTo(cutoutEnd - padding)
         }
 
-        assertThat(changes.largeScreenConstraintsChanges).isNull()
+        with(largeScreenConstraint) {
+            assertThat(getConstraint(R.id.begin_guide).layout.guideBegin)
+                .isEqualTo(cutoutStart - padding)
+            assertThat(getConstraint(R.id.end_guide).layout.guideEnd)
+                .isEqualTo(cutoutEnd - padding)
+        }
     }
 
     @Test
@@ -246,7 +252,10 @@
             assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
         }
 
-        assertThat(changes.largeScreenConstraintsChanges).isNull()
+        with(largeScreenConstraint) {
+            assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0)
+            assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
+        }
     }
 
     @Test
@@ -333,7 +342,6 @@
                 R.id.clock to "clock",
                 R.id.date to "date",
                 R.id.privacy_container to "privacy",
-                R.id.carrier_group to "carriers",
         )
         views.forEach { (id, name) ->
             assertWithMessage("$name has 0 height in qqs")
@@ -352,7 +360,6 @@
         val views = mapOf(
                 R.id.clock to "clock",
                 R.id.privacy_container to "privacy",
-                R.id.carrier_group to "carriers",
         )
         views.forEach { (id, name) ->
             expect.withMessage("$name changes height")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
deleted file mode 100644
index f580f5e..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ /dev/null
@@ -1,781 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade
-
-import android.content.Context
-import android.content.res.Resources
-import android.content.res.XmlResourceParser
-import android.graphics.Rect
-import android.testing.AndroidTestingRunner
-import android.view.DisplayCutout
-import android.view.View
-import android.view.ViewPropertyAnimator
-import android.view.WindowInsets
-import android.widget.TextView
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.animation.ShadeInterpolation
-import com.android.systemui.battery.BatteryMeterView
-import com.android.systemui.battery.BatteryMeterViewController
-import com.android.systemui.demomode.DemoMode
-import com.android.systemui.demomode.DemoModeController
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.qs.ChipVisibilityListener
-import com.android.systemui.qs.HeaderPrivacyIconsController
-import com.android.systemui.qs.carrier.QSCarrierGroup
-import com.android.systemui.qs.carrier.QSCarrierGroupController
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.phone.StatusIconContainer
-import com.android.systemui.statusbar.policy.Clock
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.VariableDateView
-import com.android.systemui.statusbar.policy.VariableDateViewController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Answers
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.anyFloat
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.inOrder
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-private val EMPTY_CHANGES = ConstraintsChanges()
-
-/**
- * Tests for [LargeScreenShadeHeaderController] when [Flags.COMBINED_QS_HEADERS] is `true`.
- *
- * Once that flag is removed, this class will be combined with
- * [LargeScreenShadeHeaderControllerTest].
- */
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
-
-    @Mock
-    private lateinit var statusIcons: StatusIconContainer
-    @Mock
-    private lateinit var statusBarIconController: StatusBarIconController
-    @Mock
-    private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
-    @Mock
-    private lateinit var iconManager: StatusBarIconController.TintedIconManager
-    @Mock
-    private lateinit var qsCarrierGroupController: QSCarrierGroupController
-    @Mock
-    private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
-    @Mock
-    private lateinit var featureFlags: FeatureFlags
-    @Mock
-    private lateinit var clock: Clock
-    @Mock
-    private lateinit var date: VariableDateView
-    @Mock
-    private lateinit var carrierGroup: QSCarrierGroup
-    @Mock
-    private lateinit var batteryMeterView: BatteryMeterView
-    @Mock
-    private lateinit var batteryMeterViewController: BatteryMeterViewController
-    @Mock
-    private lateinit var privacyIconsController: HeaderPrivacyIconsController
-    @Mock
-    private lateinit var insetsProvider: StatusBarContentInsetsProvider
-    @Mock
-    private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
-    @Mock
-    private lateinit var variableDateViewController: VariableDateViewController
-    @Mock
-    private lateinit var dumpManager: DumpManager
-    @Mock
-    private lateinit var combinedShadeHeadersConstraintManager:
-        CombinedShadeHeadersConstraintManager
-
-    @Mock
-    private lateinit var mockedContext: Context
-    @Mock(answer = Answers.RETURNS_MOCKS)
-    private lateinit var view: MotionLayout
-
-    @Mock
-    private lateinit var qqsConstraints: ConstraintSet
-    @Mock
-    private lateinit var qsConstraints: ConstraintSet
-    @Mock
-    private lateinit var largeScreenConstraints: ConstraintSet
-    @Mock private lateinit var demoModeController: DemoModeController
-
-    @JvmField @Rule
-    val mockitoRule = MockitoJUnit.rule()
-    var viewVisibility = View.GONE
-
-    private lateinit var controller: LargeScreenShadeHeaderController
-    private lateinit var carrierIconSlots: List<String>
-    private val configurationController = FakeConfigurationController()
-    private lateinit var demoModeControllerCapture: ArgumentCaptor<DemoMode>
-
-    @Before
-    fun setUp() {
-        demoModeControllerCapture = argumentCaptor<DemoMode>()
-        whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
-        whenever(clock.context).thenReturn(mockedContext)
-
-        whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
-        whenever(date.context).thenReturn(mockedContext)
-        whenever(variableDateViewControllerFactory.create(any()))
-            .thenReturn(variableDateViewController)
-
-        whenever<QSCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup)
-        whenever<BatteryMeterView>(view.findViewById(R.id.batteryRemainingIcon))
-            .thenReturn(batteryMeterView)
-
-        whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
-        whenever(statusIcons.context).thenReturn(context)
-
-        whenever(qsCarrierGroupControllerBuilder.setQSCarrierGroup(any()))
-            .thenReturn(qsCarrierGroupControllerBuilder)
-        whenever(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
-
-        whenever(view.context).thenReturn(context)
-        whenever(view.resources).thenReturn(context.resources)
-        whenever(view.setVisibility(ArgumentMatchers.anyInt())).then {
-            viewVisibility = it.arguments[0] as Int
-            null
-        }
-        whenever(view.visibility).thenAnswer { _ -> viewVisibility }
-        whenever(view.alpha).thenReturn(1f)
-
-        whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
-
-        whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true)
-
-        setUpDefaultInsets()
-        setUpMotionLayout(view)
-
-        controller = LargeScreenShadeHeaderController(
-            view,
-            statusBarIconController,
-            iconManagerFactory,
-            privacyIconsController,
-            insetsProvider,
-            configurationController,
-            variableDateViewControllerFactory,
-            batteryMeterViewController,
-            dumpManager,
-            featureFlags,
-            qsCarrierGroupControllerBuilder,
-            combinedShadeHeadersConstraintManager,
-            demoModeController
-        )
-        whenever(view.isAttachedToWindow).thenReturn(true)
-        controller.init()
-        carrierIconSlots = listOf(
-            context.getString(com.android.internal.R.string.status_bar_mobile))
-    }
-
-    @Test
-    fun testControllersCreatedAndInitialized() {
-        verify(variableDateViewController).init()
-
-        verify(batteryMeterViewController).init()
-        verify(batteryMeterViewController).ignoreTunerUpdates()
-        verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
-
-        val inOrder = inOrder(qsCarrierGroupControllerBuilder)
-        inOrder.verify(qsCarrierGroupControllerBuilder).setQSCarrierGroup(carrierGroup)
-        inOrder.verify(qsCarrierGroupControllerBuilder).build()
-    }
-
-    @Test
-    fun testClockPivotLtr() {
-        val width = 200
-        whenever(clock.width).thenReturn(width)
-        whenever(clock.isLayoutRtl).thenReturn(false)
-
-        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
-        verify(clock).addOnLayoutChangeListener(capture(captor))
-
-        captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
-        verify(clock).pivotX = 0f
-    }
-
-    @Test
-    fun testClockPivotRtl() {
-        val width = 200
-        whenever(clock.width).thenReturn(width)
-        whenever(clock.isLayoutRtl).thenReturn(true)
-
-        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
-        verify(clock).addOnLayoutChangeListener(capture(captor))
-
-        captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
-        verify(clock).pivotX = width.toFloat()
-    }
-
-    @Test
-    fun testShadeExpanded_true() {
-        // When shade is expanded, view should be visible regardless of largeScreenActive
-        controller.largeScreenActive = false
-        controller.qsVisible = true
-        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
-
-        controller.largeScreenActive = true
-        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun testShadeExpanded_false() {
-        // When shade is not expanded, view should be invisible regardless of largeScreenActive
-        controller.largeScreenActive = false
-        controller.qsVisible = false
-        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
-
-        controller.largeScreenActive = true
-        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
-    }
-
-    @Test
-    fun testLargeScreenActive_false() {
-        controller.largeScreenActive = true // Make sure there's a change
-        clearInvocations(view)
-
-        controller.largeScreenActive = false
-
-        verify(view).setTransition(HEADER_TRANSITION_ID)
-    }
-
-    @Test
-    fun testShadeExpandedFraction() {
-        // View needs to be visible for this to actually take effect
-        controller.qsVisible = true
-
-        clearInvocations(view)
-        controller.shadeExpandedFraction = 0.3f
-        verify(view).alpha = ShadeInterpolation.getContentAlpha(0.3f)
-
-        clearInvocations(view)
-        controller.shadeExpandedFraction = 1f
-        verify(view).alpha = ShadeInterpolation.getContentAlpha(1f)
-
-        clearInvocations(view)
-        controller.shadeExpandedFraction = 0f
-        verify(view).alpha = ShadeInterpolation.getContentAlpha(0f)
-    }
-
-    @Test
-    fun testQsExpandedFraction_headerTransition() {
-        controller.qsVisible = true
-        controller.largeScreenActive = false
-
-        clearInvocations(view)
-        controller.qsExpandedFraction = 0.3f
-        verify(view).progress = 0.3f
-    }
-
-    @Test
-    fun testQsExpandedFraction_largeScreen() {
-        controller.qsVisible = true
-        controller.largeScreenActive = true
-
-        clearInvocations(view)
-        controller.qsExpandedFraction = 0.3f
-        verify(view, never()).progress = anyFloat()
-    }
-
-    @Test
-    fun testScrollY_headerTransition() {
-        controller.largeScreenActive = false
-
-        clearInvocations(view)
-        controller.qsScrollY = 20
-        verify(view).scrollY = 20
-    }
-
-    @Test
-    fun testScrollY_largeScreen() {
-        controller.largeScreenActive = true
-
-        clearInvocations(view)
-        controller.qsScrollY = 20
-        verify(view, never()).scrollY = anyInt()
-    }
-
-    @Test
-    fun testPrivacyChipVisibilityChanged_visible_changesCorrectConstraints() {
-        val chipVisibleChanges = createMockConstraintChanges()
-        val chipNotVisibleChanges = createMockConstraintChanges()
-
-        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
-            .thenReturn(chipVisibleChanges)
-        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
-            .thenReturn(chipNotVisibleChanges)
-
-        val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
-        verify(privacyIconsController).chipVisibilityListener = capture(captor)
-
-        captor.value.onChipVisibilityRefreshed(true)
-
-        verify(chipVisibleChanges.qqsConstraintsChanges)!!.invoke(qqsConstraints)
-        verify(chipVisibleChanges.qsConstraintsChanges)!!.invoke(qsConstraints)
-        verify(chipVisibleChanges.largeScreenConstraintsChanges)!!.invoke(largeScreenConstraints)
-
-        verify(chipNotVisibleChanges.qqsConstraintsChanges, never())!!.invoke(any())
-        verify(chipNotVisibleChanges.qsConstraintsChanges, never())!!.invoke(any())
-        verify(chipNotVisibleChanges.largeScreenConstraintsChanges, never())!!.invoke(any())
-    }
-
-    @Test
-    fun testPrivacyChipVisibilityChanged_notVisible_changesCorrectConstraints() {
-        val chipVisibleChanges = createMockConstraintChanges()
-        val chipNotVisibleChanges = createMockConstraintChanges()
-
-        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
-            .thenReturn(chipVisibleChanges)
-        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
-            .thenReturn(chipNotVisibleChanges)
-
-        val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
-        verify(privacyIconsController).chipVisibilityListener = capture(captor)
-
-        captor.value.onChipVisibilityRefreshed(false)
-
-        verify(chipVisibleChanges.qqsConstraintsChanges, never())!!.invoke(qqsConstraints)
-        verify(chipVisibleChanges.qsConstraintsChanges, never())!!.invoke(qsConstraints)
-        verify(chipVisibleChanges.largeScreenConstraintsChanges, never())!!
-            .invoke(largeScreenConstraints)
-
-        verify(chipNotVisibleChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(chipNotVisibleChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(chipNotVisibleChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testInsetsGuides_ltr() {
-        whenever(view.isLayoutRtl).thenReturn(false)
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        val (insetLeft, insetRight) = 30 to 40
-        val (paddingStart, paddingEnd) = 10 to 20
-        whenever(view.paddingStart).thenReturn(paddingStart)
-        whenever(view.paddingEnd).thenReturn(paddingEnd)
-
-        mockInsetsProvider(insetLeft to insetRight, false)
-
-        whenever(combinedShadeHeadersConstraintManager
-            .edgesGuidelinesConstraints(anyInt(), anyInt(), anyInt(), anyInt())
-        ).thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets())
-
-        verify(combinedShadeHeadersConstraintManager)
-            .edgesGuidelinesConstraints(insetLeft, paddingStart, insetRight, paddingEnd)
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testInsetsGuides_rtl() {
-        whenever(view.isLayoutRtl).thenReturn(true)
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        val (insetLeft, insetRight) = 30 to 40
-        val (paddingStart, paddingEnd) = 10 to 20
-        whenever(view.paddingStart).thenReturn(paddingStart)
-        whenever(view.paddingEnd).thenReturn(paddingEnd)
-
-        mockInsetsProvider(insetLeft to insetRight, false)
-
-        whenever(combinedShadeHeadersConstraintManager
-            .edgesGuidelinesConstraints(anyInt(), anyInt(), anyInt(), anyInt())
-        ).thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets())
-
-        verify(combinedShadeHeadersConstraintManager)
-            .edgesGuidelinesConstraints(insetRight, paddingStart, insetLeft, paddingEnd)
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testNullCutout() {
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
-            .thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets(null))
-
-        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
-        verify(combinedShadeHeadersConstraintManager, never())
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testEmptyCutout() {
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
-            .thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets())
-
-        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
-        verify(combinedShadeHeadersConstraintManager, never())
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testCornerCutout_emptyRect() {
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        mockInsetsProvider(0 to 0, true)
-
-        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
-            .thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets())
-
-        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
-        verify(combinedShadeHeadersConstraintManager, never())
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testCornerCutout_nonEmptyRect() {
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        mockInsetsProvider(0 to 0, true)
-
-        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
-            .thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(1, 2, 3, 4)))
-
-        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
-        verify(combinedShadeHeadersConstraintManager, never())
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testTopCutout_ltr() {
-        val width = 100
-        val paddingLeft = 10
-        val paddingRight = 20
-        val cutoutWidth = 30
-
-        whenever(view.isLayoutRtl).thenReturn(false)
-        whenever(view.width).thenReturn(width)
-        whenever(view.paddingLeft).thenReturn(paddingLeft)
-        whenever(view.paddingRight).thenReturn(paddingRight)
-
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        mockInsetsProvider(0 to 0, false)
-
-        whenever(combinedShadeHeadersConstraintManager
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-        ).thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
-
-        verify(combinedShadeHeadersConstraintManager, never()).emptyCutoutConstraints()
-        val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
-        verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(false, offset)
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun testTopCutout_rtl() {
-        val width = 100
-        val paddingLeft = 10
-        val paddingRight = 20
-        val cutoutWidth = 30
-
-        whenever(view.isLayoutRtl).thenReturn(true)
-        whenever(view.width).thenReturn(width)
-        whenever(view.paddingLeft).thenReturn(paddingLeft)
-        whenever(view.paddingRight).thenReturn(paddingRight)
-
-        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
-        verify(view).setOnApplyWindowInsetsListener(capture(captor))
-        val mockConstraintsChanges = createMockConstraintChanges()
-
-        mockInsetsProvider(0 to 0, false)
-
-        whenever(combinedShadeHeadersConstraintManager
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-        ).thenReturn(mockConstraintsChanges)
-
-        captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
-
-        verify(combinedShadeHeadersConstraintManager, never()).emptyCutoutConstraints()
-        val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
-        verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(true, offset)
-
-        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
-        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
-    }
-
-    @Test
-    fun alarmIconNotIgnored() {
-        verify(statusIcons, never()).addIgnoredSlot(
-                context.getString(com.android.internal.R.string.status_bar_alarm_clock)
-        )
-    }
-
-    @Test
-    fun demoMode_attachDemoMode() {
-        verify(demoModeController).addCallback(capture(demoModeControllerCapture))
-        demoModeControllerCapture.value.onDemoModeStarted()
-        verify(clock).onDemoModeStarted()
-    }
-
-    @Test
-    fun demoMode_detachDemoMode() {
-        controller.simulateViewDetached()
-        verify(demoModeController).removeCallback(capture(demoModeControllerCapture))
-        demoModeControllerCapture.value.onDemoModeFinished()
-        verify(clock).onDemoModeFinished()
-    }
-
-    @Test
-    fun animateOutOnStartCustomizing() {
-        val animator = Mockito.mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
-        val duration = 1000L
-        whenever(view.animate()).thenReturn(animator)
-
-        controller.startCustomizingAnimation(show = true, duration)
-
-        verify(animator).setDuration(duration)
-        verify(animator).alpha(0f)
-        verify(animator).setInterpolator(Interpolators.ALPHA_OUT)
-        verify(animator).start()
-    }
-
-    @Test
-    fun animateInOnEndCustomizing() {
-        val animator = Mockito.mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
-        val duration = 1000L
-        whenever(view.animate()).thenReturn(animator)
-
-        controller.startCustomizingAnimation(show = false, duration)
-
-        verify(animator).setDuration(duration)
-        verify(animator).alpha(1f)
-        verify(animator).setInterpolator(Interpolators.ALPHA_IN)
-        verify(animator).start()
-    }
-
-    @Test
-    fun privacyChipParentVisibleFromStart() {
-        verify(privacyIconsController).onParentVisible()
-    }
-
-    @Test
-    fun privacyChipParentVisibleAlways() {
-        controller.largeScreenActive = true
-        controller.largeScreenActive = false
-        controller.largeScreenActive = true
-
-        verify(privacyIconsController, never()).onParentInvisible()
-    }
-
-    @Test
-    fun clockPivotYInCenter() {
-        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
-        verify(clock).addOnLayoutChangeListener(capture(captor))
-        var height = 100
-        val width = 50
-
-        clock.executeLayoutChange(0, 0, width, height, captor.value)
-        verify(clock).pivotY = height.toFloat() / 2
-
-        height = 150
-        clock.executeLayoutChange(0, 0, width, height, captor.value)
-        verify(clock).pivotY = height.toFloat() / 2
-    }
-
-    @Test
-    fun onDensityOrFontScaleChanged_reloadConstraints() {
-        // After density or font scale change, constraints need to be reloaded to reflect new
-        // dimensions.
-        reset(qqsConstraints)
-        reset(qsConstraints)
-        reset(largeScreenConstraints)
-
-        configurationController.notifyDensityOrFontScaleChanged()
-
-        val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
-        verify(qqsConstraints).load(eq(context), capture(captor))
-        assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
-        verify(qsConstraints).load(eq(context), capture(captor))
-        assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header)
-        verify(largeScreenConstraints).load(eq(context), capture(captor))
-        assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
-    }
-
-    private fun View.executeLayoutChange(
-            left: Int,
-            top: Int,
-            right: Int,
-            bottom: Int,
-            listener: View.OnLayoutChangeListener
-    ) {
-        val oldLeft = this.left
-        val oldTop = this.top
-        val oldRight = this.right
-        val oldBottom = this.bottom
-        whenever(this.left).thenReturn(left)
-        whenever(this.top).thenReturn(top)
-        whenever(this.right).thenReturn(right)
-        whenever(this.bottom).thenReturn(bottom)
-        whenever(this.height).thenReturn(bottom - top)
-        whenever(this.width).thenReturn(right - left)
-        listener.onLayoutChange(
-                this,
-                oldLeft,
-                oldTop,
-                oldRight,
-                oldBottom,
-                left,
-                top,
-                right,
-                bottom
-        )
-    }
-
-    private fun createWindowInsets(
-        topCutout: Rect? = Rect()
-    ): WindowInsets {
-        val windowInsets: WindowInsets = mock()
-        val displayCutout: DisplayCutout = mock()
-        whenever(windowInsets.displayCutout)
-            .thenReturn(if (topCutout != null) displayCutout else null)
-        whenever(displayCutout.boundingRectTop).thenReturn(topCutout)
-
-        return windowInsets
-    }
-
-    private fun mockInsetsProvider(
-        insets: Pair<Int, Int> = 0 to 0,
-        cornerCutout: Boolean = false,
-    ) {
-        whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
-            .thenReturn(insets.toAndroidPair())
-        whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(cornerCutout)
-    }
-
-    private fun createMockConstraintChanges(): ConstraintsChanges {
-        return ConstraintsChanges(mock(), mock(), mock())
-    }
-
-    private fun XmlResourceParser.getResId(): Int {
-        return Resources.getAttributeSetSourceResId(this)
-    }
-
-    private fun setUpMotionLayout(motionLayout: MotionLayout) {
-        whenever(motionLayout.getConstraintSet(QQS_HEADER_CONSTRAINT)).thenReturn(qqsConstraints)
-        whenever(motionLayout.getConstraintSet(QS_HEADER_CONSTRAINT)).thenReturn(qsConstraints)
-        whenever(motionLayout.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT))
-            .thenReturn(largeScreenConstraints)
-    }
-
-    private fun setUpDefaultInsets() {
-        whenever(combinedShadeHeadersConstraintManager
-            .edgesGuidelinesConstraints(anyInt(), anyInt(), anyInt(), anyInt())
-        ).thenReturn(EMPTY_CHANGES)
-        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
-            .thenReturn(EMPTY_CHANGES)
-        whenever(combinedShadeHeadersConstraintManager
-            .centerCutoutConstraints(anyBoolean(), anyInt())
-        ).thenReturn(EMPTY_CHANGES)
-        whenever(combinedShadeHeadersConstraintManager
-            .privacyChipVisibilityConstraints(anyBoolean())
-        ).thenReturn(EMPTY_CHANGES)
-        whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
-            .thenReturn(Pair(0, 0).toAndroidPair())
-        whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(false)
-    }
-
-    private fun<T, U> Pair<T, U>.toAndroidPair(): android.util.Pair<T, U> {
-        return android.util.Pair(first, second)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
deleted file mode 100644
index b568122..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ /dev/null
@@ -1,311 +0,0 @@
-package com.android.systemui.shade
-
-import android.animation.ValueAnimator
-import android.app.StatusBarManager
-import android.content.Context
-import android.testing.AndroidTestingRunner
-import android.view.View
-import android.view.ViewPropertyAnimator
-import android.widget.TextView
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.animation.ShadeInterpolation
-import com.android.systemui.battery.BatteryMeterView
-import com.android.systemui.battery.BatteryMeterViewController
-import com.android.systemui.demomode.DemoMode
-import com.android.systemui.demomode.DemoModeController
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.qs.HeaderPrivacyIconsController
-import com.android.systemui.qs.carrier.QSCarrierGroup
-import com.android.systemui.qs.carrier.QSCarrierGroupController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.phone.StatusIconContainer
-import com.android.systemui.statusbar.policy.Clock
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.VariableDateViewController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Answers
-import org.mockito.ArgumentMatchers.anyFloat
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
-
-    @Mock private lateinit var view: View
-    @Mock private lateinit var statusIcons: StatusIconContainer
-    @Mock private lateinit var statusBarIconController: StatusBarIconController
-    @Mock private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
-    @Mock private lateinit var iconManager: StatusBarIconController.TintedIconManager
-    @Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController
-    @Mock private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
-    @Mock private lateinit var featureFlags: FeatureFlags
-    @Mock private lateinit var clock: Clock
-    @Mock private lateinit var date: TextView
-    @Mock private lateinit var carrierGroup: QSCarrierGroup
-    @Mock private lateinit var batteryMeterView: BatteryMeterView
-    @Mock private lateinit var batteryMeterViewController: BatteryMeterViewController
-    @Mock private lateinit var privacyIconsController: HeaderPrivacyIconsController
-    @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
-    @Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
-    @Mock private lateinit var variableDateViewController: VariableDateViewController
-    @Mock private lateinit var dumpManager: DumpManager
-    @Mock private lateinit var combinedShadeHeadersConstraintManager:
-        CombinedShadeHeadersConstraintManager
-
-    @Mock private lateinit var mockedContext: Context
-    @Mock private lateinit var demoModeController: DemoModeController
-
-    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
-    var viewVisibility = View.GONE
-    var viewAlpha = 1f
-
-    private lateinit var mLargeScreenShadeHeaderController: LargeScreenShadeHeaderController
-    private lateinit var carrierIconSlots: List<String>
-    private val configurationController = FakeConfigurationController()
-
-    @Before
-    fun setup() {
-        whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
-        whenever(clock.context).thenReturn(mockedContext)
-        whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
-        whenever(date.context).thenReturn(mockedContext)
-        whenever<QSCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup)
-        whenever<BatteryMeterView>(view.findViewById(R.id.batteryRemainingIcon))
-                .thenReturn(batteryMeterView)
-        whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
-        whenever(view.context).thenReturn(context)
-        whenever(view.resources).thenReturn(context.resources)
-        whenever(statusIcons.context).thenReturn(context)
-        whenever(qsCarrierGroupControllerBuilder.setQSCarrierGroup(any()))
-                .thenReturn(qsCarrierGroupControllerBuilder)
-        whenever(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
-        whenever(view.setVisibility(anyInt())).then {
-            viewVisibility = it.arguments[0] as Int
-            null
-        }
-        whenever(view.visibility).thenAnswer { _ -> viewVisibility }
-
-        whenever(view.setAlpha(anyFloat())).then {
-            viewAlpha = it.arguments[0] as Float
-            null
-        }
-        whenever(view.alpha).thenAnswer { _ -> viewAlpha }
-
-        whenever(variableDateViewControllerFactory.create(any()))
-            .thenReturn(variableDateViewController)
-        whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
-        whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false)
-        mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController(
-                view,
-                statusBarIconController,
-                iconManagerFactory,
-                privacyIconsController,
-                insetsProvider,
-                configurationController,
-                variableDateViewControllerFactory,
-                batteryMeterViewController,
-                dumpManager,
-                featureFlags,
-                qsCarrierGroupControllerBuilder,
-                combinedShadeHeadersConstraintManager,
-                demoModeController
-                )
-        whenever(view.isAttachedToWindow).thenReturn(true)
-        mLargeScreenShadeHeaderController.init()
-        carrierIconSlots = listOf(
-                context.getString(com.android.internal.R.string.status_bar_mobile))
-    }
-
-    @After
-    fun verifyEveryTest() {
-        verifyZeroInteractions(combinedShadeHeadersConstraintManager)
-    }
-
-    @Test
-    fun setVisible_onlyWhenActive() {
-        makeShadeVisible()
-        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
-
-        mLargeScreenShadeHeaderController.largeScreenActive = false
-        assertThat(viewVisibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun updateListeners_registersWhenVisible() {
-        makeShadeVisible()
-        verify(qsCarrierGroupController).setListening(true)
-        verify(statusBarIconController).addIconGroup(any())
-    }
-
-    @Test
-    fun shadeExpandedFraction_updatesAlpha() {
-        makeShadeVisible()
-        mLargeScreenShadeHeaderController.shadeExpandedFraction = 0.5f
-        verify(view).setAlpha(ShadeInterpolation.getContentAlpha(0.5f))
-    }
-
-    @Test
-    fun alphaChangesUpdateVisibility() {
-        makeShadeVisible()
-        mLargeScreenShadeHeaderController.shadeExpandedFraction = 0f
-        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
-
-        mLargeScreenShadeHeaderController.shadeExpandedFraction = 1f
-        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun singleCarrier_enablesCarrierIconsInStatusIcons() {
-        whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true)
-
-        makeShadeVisible()
-
-        verify(statusIcons).removeIgnoredSlots(carrierIconSlots)
-    }
-
-    @Test
-    fun dualCarrier_disablesCarrierIconsInStatusIcons() {
-        whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(false)
-
-        makeShadeVisible()
-
-        verify(statusIcons).addIgnoredSlots(carrierIconSlots)
-    }
-
-    @Test
-    fun disableQS_notDisabled_visible() {
-        makeShadeVisible()
-        mLargeScreenShadeHeaderController.disable(0, 0, false)
-
-        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun disableQS_disabled_gone() {
-        makeShadeVisible()
-        mLargeScreenShadeHeaderController.disable(0, StatusBarManager.DISABLE2_QUICK_SETTINGS,
-            false)
-
-        assertThat(viewVisibility).isEqualTo(View.GONE)
-    }
-
-    private fun makeShadeVisible() {
-        mLargeScreenShadeHeaderController.largeScreenActive = true
-        mLargeScreenShadeHeaderController.qsVisible = true
-    }
-
-    @Test
-    fun updateConfig_changesFontStyle() {
-        configurationController.notifyDensityOrFontScaleChanged()
-
-        verify(clock).setTextAppearance(R.style.TextAppearance_QS_Status)
-        verify(date).setTextAppearance(R.style.TextAppearance_QS_Status)
-        verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers)
-    }
-
-    @Test
-    fun alarmIconIgnored() {
-        verify(statusIcons).addIgnoredSlot(
-                context.getString(com.android.internal.R.string.status_bar_alarm_clock)
-        )
-    }
-
-    @Test
-    fun animateOutOnStartCustomizing() {
-        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
-        val duration = 1000L
-        whenever(view.animate()).thenReturn(animator)
-
-        mLargeScreenShadeHeaderController.startCustomizingAnimation(show = true, duration)
-
-        verify(animator).setDuration(duration)
-        verify(animator).alpha(0f)
-        verify(animator).setInterpolator(Interpolators.ALPHA_OUT)
-        verify(animator).start()
-    }
-
-    @Test
-    fun animateInOnEndCustomizing() {
-        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
-        val duration = 1000L
-        whenever(view.animate()).thenReturn(animator)
-
-        mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, duration)
-
-        verify(animator).setDuration(duration)
-        verify(animator).alpha(1f)
-        verify(animator).setInterpolator(Interpolators.ALPHA_IN)
-        verify(animator).start()
-    }
-
-    @Test
-    fun testShadeExpanded_true_alpha_zero_invisible() {
-        view.alpha = 0f
-        mLargeScreenShadeHeaderController.largeScreenActive = true
-        mLargeScreenShadeHeaderController.qsVisible = true
-
-        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
-    }
-
-    @Test
-    fun animatorCallsUpdateVisibilityOnUpdate() {
-        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
-        whenever(view.animate()).thenReturn(animator)
-
-        mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, 0L)
-
-        val updateCaptor = argumentCaptor<ValueAnimator.AnimatorUpdateListener>()
-        verify(animator).setUpdateListener(capture(updateCaptor))
-
-        mLargeScreenShadeHeaderController.largeScreenActive = true
-        mLargeScreenShadeHeaderController.qsVisible = true
-
-        view.alpha = 1f
-        updateCaptor.value.onAnimationUpdate(mock())
-
-        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
-
-        view.alpha = 0f
-        updateCaptor.value.onAnimationUpdate(mock())
-
-        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
-    }
-
-    @Test
-    fun demoMode_attachDemoMode() {
-        val cb = argumentCaptor<DemoMode>()
-        verify(demoModeController).addCallback(capture(cb))
-        cb.value.onDemoModeStarted()
-        verify(clock).onDemoModeStarted()
-    }
-
-    @Test
-    fun demoMode_detachDemoMode() {
-        mLargeScreenShadeHeaderController.simulateViewDetached()
-        val cb = argumentCaptor<DemoMode>()
-        verify(demoModeController).removeCallback(capture(cb))
-        cb.value.onDemoModeFinished()
-        verify(clock).onDemoModeFinished()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
new file mode 100644
index 0000000..52b0b6a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -0,0 +1,753 @@
+/*
+ * 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.shade;
+
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.android.keyguard.KeyguardClockSwitch.LARGE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.IdRes;
+import android.content.ContentResolver;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.UserManager;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewStub;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.constraintlayout.widget.ConstraintSet;
+
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.testing.UiEventLoggerFake;
+import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.KeyguardClockSwitch;
+import com.android.keyguard.KeyguardClockSwitchController;
+import com.android.keyguard.KeyguardStatusView;
+import com.android.keyguard.KeyguardStatusViewController;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.LockIconViewController;
+import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent;
+import com.android.keyguard.dagger.KeyguardStatusBarViewComponent;
+import com.android.keyguard.dagger.KeyguardStatusViewComponent;
+import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.classifier.FalsingCollectorFake;
+import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.common.ui.view.LongPressHandlingView;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.QSFragment;
+import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.shade.transition.ShadeTransitionController;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.NotificationShelfController;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.QsFrameTranslateController;
+import com.android.systemui.statusbar.StatusBarStateControllerImpl;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.notification.ConversationNotificationManager;
+import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
+import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.stack.AmbientState;
+import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
+import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
+import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
+import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
+import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
+import com.android.systemui.statusbar.phone.TapAgainViewController;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
+import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.util.time.SystemClock;
+import com.android.wm.shell.animation.FlingAnimationUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+import java.util.Optional;
+
+import dagger.Lazy;
+import kotlinx.coroutines.CoroutineDispatcher;
+
+public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
+
+    protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
+    protected static final int NOTIFICATION_SCRIM_TOP_PADDING_IN_SPLIT_SHADE = 50;
+    protected static final int PANEL_WIDTH = 500; // Random value just for the test.
+
+    @Mock protected CentralSurfaces mCentralSurfaces;
+    @Mock protected NotificationStackScrollLayout mNotificationStackScrollLayout;
+    @Mock protected KeyguardBottomAreaView mKeyguardBottomArea;
+    @Mock protected KeyguardBottomAreaViewController mKeyguardBottomAreaViewController;
+    @Mock protected KeyguardBottomAreaView mQsFrame;
+    @Mock protected HeadsUpManagerPhone mHeadsUpManager;
+    @Mock protected NotificationShelfController mNotificationShelfController;
+    @Mock protected NotificationGutsManager mGutsManager;
+    @Mock protected KeyguardStatusBarView mKeyguardStatusBar;
+    @Mock protected KeyguardUserSwitcherView mUserSwitcherView;
+    @Mock protected ViewStub mUserSwitcherStubView;
+    @Mock protected HeadsUpTouchHelper.Callback mHeadsUpCallback;
+    @Mock protected KeyguardUpdateMonitor mUpdateMonitor;
+    @Mock protected KeyguardBypassController mKeyguardBypassController;
+    @Mock protected DozeParameters mDozeParameters;
+    @Mock protected ScreenOffAnimationController mScreenOffAnimationController;
+    @Mock protected NotificationPanelView mView;
+    @Mock protected LayoutInflater mLayoutInflater;
+    @Mock protected FeatureFlags mFeatureFlags;
+    @Mock protected DynamicPrivacyController mDynamicPrivacyController;
+    @Mock protected StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    @Mock protected KeyguardStateController mKeyguardStateController;
+    @Mock protected DozeLog mDozeLog;
+    @Mock protected ShadeLogger mShadeLog;
+    @Mock protected ShadeHeightLogger mShadeHeightLogger;
+    @Mock protected CommandQueue mCommandQueue;
+    @Mock protected VibratorHelper mVibratorHelper;
+    @Mock protected LatencyTracker mLatencyTracker;
+    @Mock protected PowerManager mPowerManager;
+    @Mock protected AccessibilityManager mAccessibilityManager;
+    @Mock protected MetricsLogger mMetricsLogger;
+    @Mock protected Resources mResources;
+    @Mock protected Configuration mConfiguration;
+    @Mock protected KeyguardClockSwitch mKeyguardClockSwitch;
+    @Mock protected MediaHierarchyManager mMediaHierarchyManager;
+    @Mock protected ConversationNotificationManager mConversationNotificationManager;
+    @Mock protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    @Mock protected KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
+    @Mock protected KeyguardQsUserSwitchComponent.Factory mKeyguardQsUserSwitchComponentFactory;
+    @Mock protected KeyguardQsUserSwitchComponent mKeyguardQsUserSwitchComponent;
+    @Mock protected KeyguardQsUserSwitchController mKeyguardQsUserSwitchController;
+    @Mock protected KeyguardUserSwitcherComponent.Factory mKeyguardUserSwitcherComponentFactory;
+    @Mock protected KeyguardUserSwitcherComponent mKeyguardUserSwitcherComponent;
+    @Mock protected KeyguardUserSwitcherController mKeyguardUserSwitcherController;
+    @Mock protected KeyguardStatusViewComponent mKeyguardStatusViewComponent;
+    @Mock protected KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
+    @Mock protected KeyguardStatusBarViewComponent mKeyguardStatusBarViewComponent;
+    @Mock protected KeyguardClockSwitchController mKeyguardClockSwitchController;
+    @Mock protected KeyguardStatusViewController mKeyguardStatusViewController;
+    @Mock protected KeyguardStatusBarViewController mKeyguardStatusBarViewController;
+    @Mock protected NotificationStackScrollLayoutController
+            mNotificationStackScrollLayoutController;
+    @Mock protected NotificationShadeDepthController mNotificationShadeDepthController;
+    @Mock protected LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    @Mock protected AuthController mAuthController;
+    @Mock protected ScrimController mScrimController;
+    @Mock protected MediaDataManager mMediaDataManager;
+    @Mock protected AmbientState mAmbientState;
+    @Mock protected UserManager mUserManager;
+    @Mock protected UiEventLogger mUiEventLogger;
+    @Mock protected LockIconViewController mLockIconViewController;
+    @Mock protected KeyguardMediaController mKeyguardMediaController;
+    @Mock protected NavigationModeController mNavigationModeController;
+    @Mock protected NavigationBarController mNavigationBarController;
+    @Mock protected QuickSettingsController mQsController;
+    @Mock protected ShadeHeaderController mShadeHeaderController;
+    @Mock protected ContentResolver mContentResolver;
+    @Mock protected TapAgainViewController mTapAgainViewController;
+    @Mock protected KeyguardIndicationController mKeyguardIndicationController;
+    @Mock protected FragmentService mFragmentService;
+    @Mock protected FragmentHostManager mFragmentHostManager;
+    @Mock protected NotificationRemoteInputManager mNotificationRemoteInputManager;
+    @Mock protected RecordingController mRecordingController;
+    @Mock protected LockscreenGestureLogger mLockscreenGestureLogger;
+    @Mock protected DumpManager mDumpManager;
+    @Mock protected InteractionJankMonitor mInteractionJankMonitor;
+    @Mock protected NotificationsQSContainerController mNotificationsQSContainerController;
+    @Mock protected QsFrameTranslateController mQsFrameTranslateController;
+    @Mock protected StatusBarWindowStateController mStatusBarWindowStateController;
+    @Mock protected KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    @Mock protected NotificationShadeWindowController mNotificationShadeWindowController;
+    @Mock protected SysUiState mSysUiState;
+    @Mock protected NotificationListContainer mNotificationListContainer;
+    @Mock protected NotificationStackSizeCalculator mNotificationStackSizeCalculator;
+    @Mock protected UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    @Mock protected ShadeTransitionController mShadeTransitionController;
+    @Mock protected QS mQs;
+    @Mock protected QSFragment mQSFragment;
+    @Mock protected ViewGroup mQsHeader;
+    @Mock protected ViewParent mViewParent;
+    @Mock protected ViewTreeObserver mViewTreeObserver;
+    @Mock protected KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
+    @Mock protected DreamingToLockscreenTransitionViewModel
+            mDreamingToLockscreenTransitionViewModel;
+    @Mock protected OccludedToLockscreenTransitionViewModel
+            mOccludedToLockscreenTransitionViewModel;
+    @Mock protected LockscreenToDreamingTransitionViewModel
+            mLockscreenToDreamingTransitionViewModel;
+    @Mock protected LockscreenToOccludedTransitionViewModel
+            mLockscreenToOccludedTransitionViewModel;
+    @Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
+
+    @Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    @Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
+    @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @Mock protected MotionEvent mDownMotionEvent;
+    @Mock protected CoroutineDispatcher mMainDispatcher;
+    @Captor
+    protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
+            mEmptySpaceClickListenerCaptor;
+
+    protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    protected KeyguardInteractor mKeyguardInteractor;
+    protected NotificationPanelViewController.TouchHandler mTouchHandler;
+    protected ConfigurationController mConfigurationController;
+    protected SysuiStatusBarStateController mStatusBarStateController;
+    protected NotificationPanelViewController mNotificationPanelViewController;
+    protected View.AccessibilityDelegate mAccessibilityDelegate;
+    protected NotificationsQuickSettingsContainer mNotificationContainerParent;
+    protected List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners;
+    protected Handler mMainHandler;
+    protected View.OnLayoutChangeListener mLayoutChangeListener;
+
+    protected final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
+    protected final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
+    protected final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+    protected final ShadeExpansionStateManager mShadeExpansionStateManager =
+            new ShadeExpansionStateManager();
+
+    protected QuickSettingsController mQuickSettingsController;
+    @Mock protected Lazy<NotificationPanelViewController> mNotificationPanelViewControllerLazy;
+
+    protected FragmentHostManager.FragmentListener mFragmentListener;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mMainDispatcher = getMainDispatcher();
+        mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(
+                new FakeKeyguardRepository());
+        mKeyguardInteractor = new KeyguardInteractor(new FakeKeyguardRepository(), mCommandQueue,
+                mFeatureFlags, new FakeKeyguardBouncerRepository());
+        SystemClock systemClock = new FakeSystemClock();
+        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
+                mInteractionJankMonitor, mShadeExpansionStateManager);
+
+        KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
+        keyguardStatusView.setId(R.id.keyguard_status_view);
+
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+        when(mHeadsUpCallback.getContext()).thenReturn(mContext);
+        when(mView.getResources()).thenReturn(mResources);
+        when(mView.getWidth()).thenReturn(PANEL_WIDTH);
+        when(mResources.getConfiguration()).thenReturn(mConfiguration);
+        mConfiguration.orientation = ORIENTATION_PORTRAIT;
+        when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
+        mDisplayMetrics.density = 100;
+        when(mResources.getBoolean(R.bool.config_enableNotificationShadeDrag)).thenReturn(true);
+        when(mResources.getDimensionPixelSize(R.dimen.notifications_top_padding_split_shade))
+                .thenReturn(NOTIFICATION_SCRIM_TOP_PADDING_IN_SPLIT_SHADE);
+        when(mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal))
+                .thenReturn(10);
+        when(mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance))
+                .thenReturn(SPLIT_SHADE_FULL_TRANSITION_DISTANCE);
+        when(mView.getContext()).thenReturn(getContext());
+        when(mView.findViewById(R.id.keyguard_header)).thenReturn(mKeyguardStatusBar);
+        when(mView.findViewById(R.id.keyguard_user_switcher_view)).thenReturn(mUserSwitcherView);
+        when(mView.findViewById(R.id.keyguard_user_switcher_stub)).thenReturn(
+                mUserSwitcherStubView);
+        when(mView.findViewById(R.id.keyguard_clock_container)).thenReturn(mKeyguardClockSwitch);
+        when(mView.findViewById(R.id.notification_stack_scroller))
+                .thenReturn(mNotificationStackScrollLayout);
+        when(mNotificationStackScrollLayoutController.getHeight()).thenReturn(1000);
+        when(mNotificationStackScrollLayoutController.getHeadsUpCallback())
+                .thenReturn(mHeadsUpCallback);
+        when(mKeyguardBottomAreaViewController.getView()).thenReturn(mKeyguardBottomArea);
+        when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea);
+        when(mKeyguardBottomArea.animate()).thenReturn(mock(ViewPropertyAnimator.class));
+        when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame);
+        when(mView.findViewById(R.id.keyguard_status_view))
+                .thenReturn(mock(KeyguardStatusView.class));
+        mNotificationContainerParent = new NotificationsQuickSettingsContainer(getContext(), null);
+        mNotificationContainerParent.addView(keyguardStatusView);
+        mNotificationContainerParent.onFinishInflate();
+        when(mView.findViewById(R.id.notification_container_parent))
+                .thenReturn(mNotificationContainerParent);
+        when(mFragmentService.getFragmentHostManager(mView)).thenReturn(mFragmentHostManager);
+        FlingAnimationUtils.Builder flingAnimationUtilsBuilder = new FlingAnimationUtils.Builder(
+                mDisplayMetrics);
+        when(mKeyguardQsUserSwitchComponentFactory.build(any()))
+                .thenReturn(mKeyguardQsUserSwitchComponent);
+        when(mKeyguardQsUserSwitchComponent.getKeyguardQsUserSwitchController())
+                .thenReturn(mKeyguardQsUserSwitchController);
+        when(mKeyguardUserSwitcherComponentFactory.build(any()))
+                .thenReturn(mKeyguardUserSwitcherComponent);
+        when(mKeyguardUserSwitcherComponent.getKeyguardUserSwitcherController())
+                .thenReturn(mKeyguardUserSwitcherController);
+        when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(true);
+        when(mQs.getView()).thenReturn(mView);
+        when(mQSFragment.getView()).thenReturn(mView);
+        doAnswer(invocation -> {
+            mFragmentListener = invocation.getArgument(1);
+            return null;
+        }).when(mFragmentHostManager).addTagListener(eq(QS.TAG), any());
+        doAnswer((Answer<Void>) invocation -> {
+            mTouchHandler = invocation.getArgument(0);
+            return null;
+        }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class));
+
+        // Dreaming->Lockscreen
+        when(mKeyguardTransitionInteractor.getDreamingToLockscreenTransition())
+                .thenReturn(emptyFlow());
+        when(mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha())
+                .thenReturn(emptyFlow());
+        when(mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(anyInt()))
+                .thenReturn(emptyFlow());
+
+        // Occluded->Lockscreen
+        when(mKeyguardTransitionInteractor.getOccludedToLockscreenTransition())
+                .thenReturn(emptyFlow());
+        when(mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha())
+                .thenReturn(emptyFlow());
+        when(mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(anyInt()))
+                .thenReturn(emptyFlow());
+
+        // Lockscreen->Dreaming
+        when(mKeyguardTransitionInteractor.getLockscreenToDreamingTransition())
+                .thenReturn(emptyFlow());
+        when(mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha())
+                .thenReturn(emptyFlow());
+        when(mLockscreenToDreamingTransitionViewModel.lockscreenTranslationY(anyInt()))
+                .thenReturn(emptyFlow());
+
+        // Gone->Dreaming
+        when(mKeyguardTransitionInteractor.getGoneToDreamingTransition())
+                .thenReturn(emptyFlow());
+        when(mGoneToDreamingTransitionViewModel.getLockscreenAlpha())
+                .thenReturn(emptyFlow());
+        when(mGoneToDreamingTransitionViewModel.lockscreenTranslationY(anyInt()))
+                .thenReturn(emptyFlow());
+
+        // Lockscreen->Occluded
+        when(mKeyguardTransitionInteractor.getLockscreenToOccludedTransition())
+                .thenReturn(emptyFlow());
+        when(mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha())
+                .thenReturn(emptyFlow());
+        when(mLockscreenToOccludedTransitionViewModel.lockscreenTranslationY(anyInt()))
+                .thenReturn(emptyFlow());
+
+        NotificationWakeUpCoordinator coordinator =
+                new NotificationWakeUpCoordinator(
+                        mDumpManager,
+                        mock(HeadsUpManagerPhone.class),
+                        new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
+                                mInteractionJankMonitor, mShadeExpansionStateManager),
+                        mKeyguardBypassController,
+                        mDozeParameters,
+                        mScreenOffAnimationController,
+                        mock(NotificationWakeUpCoordinatorLogger.class));
+        mConfigurationController = new ConfigurationControllerImpl(mContext);
+        PulseExpansionHandler expansionHandler = new PulseExpansionHandler(
+                mContext,
+                coordinator,
+                mKeyguardBypassController, mHeadsUpManager,
+                mock(NotificationRoundnessManager.class),
+                mConfigurationController,
+                mStatusBarStateController,
+                mFalsingManager,
+                mShadeExpansionStateManager,
+                mLockscreenShadeTransitionController,
+                new FalsingCollectorFake(),
+                mDumpManager);
+        when(mKeyguardStatusViewComponentFactory.build(any()))
+                .thenReturn(mKeyguardStatusViewComponent);
+        when(mKeyguardStatusViewComponent.getKeyguardClockSwitchController())
+                .thenReturn(mKeyguardClockSwitchController);
+        when(mKeyguardStatusViewComponent.getKeyguardStatusViewController())
+                .thenReturn(mKeyguardStatusViewController);
+        when(mKeyguardStatusBarViewComponentFactory.build(any(), any()))
+                .thenReturn(mKeyguardStatusBarViewComponent);
+        when(mKeyguardStatusBarViewComponent.getKeyguardStatusBarViewController())
+                .thenReturn(mKeyguardStatusBarViewController);
+        when(mLayoutInflater.inflate(eq(R.layout.keyguard_status_view), any(), anyBoolean()))
+                .thenReturn(keyguardStatusView);
+        when(mLayoutInflater.inflate(eq(R.layout.keyguard_user_switcher), any(), anyBoolean()))
+                .thenReturn(mUserSwitcherView);
+        when(mLayoutInflater.inflate(eq(R.layout.keyguard_bottom_area), any(), anyBoolean()))
+                .thenReturn(mKeyguardBottomArea);
+        when(mNotificationRemoteInputManager.isRemoteInputActive())
+                .thenReturn(false);
+        when(mInteractionJankMonitor.begin(any(), anyInt()))
+                .thenReturn(true);
+        when(mInteractionJankMonitor.end(anyInt()))
+                .thenReturn(true);
+        doAnswer(invocation -> {
+            ((Runnable) invocation.getArgument(0)).run();
+            return null;
+        }).when(mNotificationShadeWindowController).batchApplyWindowLayoutParams(any());
+        doAnswer(invocation -> {
+            mLayoutChangeListener = invocation.getArgument(0);
+            return null;
+        }).when(mView).addOnLayoutChangeListener(any());
+
+        when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
+        when(mView.getParent()).thenReturn(mViewParent);
+        when(mQs.getHeader()).thenReturn(mQsHeader);
+        when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+
+        mMainHandler = new Handler(Looper.getMainLooper());
+
+        when(mView.requireViewById(R.id.keyguard_long_press))
+                .thenReturn(mock(LongPressHandlingView.class));
+
+        mNotificationPanelViewController = new NotificationPanelViewController(
+                mView,
+                mMainHandler,
+                mLayoutInflater,
+                mFeatureFlags,
+                coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
+                mFalsingManager, new FalsingCollectorFake(),
+                mKeyguardStateController,
+                mStatusBarStateController,
+                mStatusBarWindowStateController,
+                mNotificationShadeWindowController,
+                mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper,
+                mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
+                mMetricsLogger,
+                mShadeLog,
+                mShadeHeightLogger,
+                mConfigurationController,
+                () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
+                mConversationNotificationManager, mMediaHierarchyManager,
+                mStatusBarKeyguardViewManager,
+                mGutsManager,
+                mNotificationsQSContainerController,
+                mNotificationStackScrollLayoutController,
+                mKeyguardStatusViewComponentFactory,
+                mKeyguardQsUserSwitchComponentFactory,
+                mKeyguardUserSwitcherComponentFactory,
+                mKeyguardStatusBarViewComponentFactory,
+                mLockscreenShadeTransitionController,
+                mAuthController,
+                mScrimController,
+                mUserManager,
+                mMediaDataManager,
+                mNotificationShadeDepthController,
+                mAmbientState,
+                mLockIconViewController,
+                mKeyguardMediaController,
+                mTapAgainViewController,
+                mNavigationModeController,
+                mNavigationBarController,
+                mQsController,
+                mFragmentService,
+                mContentResolver,
+                mRecordingController,
+                mShadeHeaderController,
+                mScreenOffAnimationController,
+                mLockscreenGestureLogger,
+                mShadeExpansionStateManager,
+                mNotificationRemoteInputManager,
+                mSysUIUnfoldComponent,
+                mSysUiState,
+                () -> mKeyguardBottomAreaViewController,
+                mKeyguardUnlockAnimationController,
+                mKeyguardIndicationController,
+                mNotificationListContainer,
+                mNotificationStackSizeCalculator,
+                mUnlockedScreenOffAnimationController,
+                mShadeTransitionController,
+                systemClock,
+                mKeyguardBottomAreaViewModel,
+                mKeyguardBottomAreaInteractor,
+                mAlternateBouncerInteractor,
+                mDreamingToLockscreenTransitionViewModel,
+                mOccludedToLockscreenTransitionViewModel,
+                mLockscreenToDreamingTransitionViewModel,
+                mGoneToDreamingTransitionViewModel,
+                mLockscreenToOccludedTransitionViewModel,
+                mMainDispatcher,
+                mKeyguardTransitionInteractor,
+                mDumpManager,
+                mKeyuardLongPressViewModel,
+                mKeyguardInteractor);
+        mNotificationPanelViewController.initDependencies(
+                mCentralSurfaces,
+                null,
+                () -> {},
+                mNotificationShelfController);
+        mNotificationPanelViewController.setTrackingStartedListener(() -> {});
+        mNotificationPanelViewController.setOpenCloseListener(
+                new NotificationPanelViewController.OpenCloseListener() {
+                    @Override
+                    public void onClosingFinished() {}
+
+                    @Override
+                    public void onOpenStarted() {}
+                });
+        mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
+        ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor =
+                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+        verify(mView, atLeast(1)).addOnAttachStateChangeListener(
+                onAttachStateChangeListenerArgumentCaptor.capture());
+        mOnAttachStateChangeListeners = onAttachStateChangeListenerArgumentCaptor.getAllValues();
+
+        ArgumentCaptor<View.AccessibilityDelegate> accessibilityDelegateArgumentCaptor =
+                ArgumentCaptor.forClass(View.AccessibilityDelegate.class);
+        verify(mView).setAccessibilityDelegate(accessibilityDelegateArgumentCaptor.capture());
+        mAccessibilityDelegate = accessibilityDelegateArgumentCaptor.getValue();
+        mNotificationPanelViewController.getStatusBarStateController()
+                .addCallback(mNotificationPanelViewController.getStatusBarStateListener());
+        mNotificationPanelViewController
+                .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+        verify(mNotificationStackScrollLayoutController)
+                .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
+        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
+        reset(mKeyguardStatusViewController);
+
+        when(mNotificationPanelViewControllerLazy.get())
+                .thenReturn(mNotificationPanelViewController);
+        mQuickSettingsController = new QuickSettingsController(
+                mNotificationPanelViewControllerLazy,
+                mView,
+                mQsFrameTranslateController,
+                mShadeTransitionController,
+                expansionHandler,
+                mNotificationRemoteInputManager,
+                mShadeExpansionStateManager,
+                mStatusBarKeyguardViewManager,
+                mNotificationStackScrollLayoutController,
+                mLockscreenShadeTransitionController,
+                mNotificationShadeDepthController,
+                mShadeHeaderController,
+                mStatusBarTouchableRegionManager,
+                mKeyguardStateController,
+                mKeyguardBypassController,
+                mUpdateMonitor,
+                mScrimController,
+                mMediaDataManager,
+                mMediaHierarchyManager,
+                mAmbientState,
+                mRecordingController,
+                mFalsingManager,
+                new FalsingCollectorFake(),
+                mAccessibilityManager,
+                mLockscreenGestureLogger,
+                mMetricsLogger,
+                mFeatureFlags,
+                mInteractionJankMonitor,
+                mShadeLog
+        );
+    }
+
+    @After
+    public void tearDown() {
+        mNotificationPanelViewController.mBottomAreaShadeAlphaAnimator.cancel();
+        mNotificationPanelViewController.cancelHeightAnimator();
+        mMainHandler.removeCallbacksAndMessages(null);
+    }
+
+    protected void setBottomPadding(int stackBottom, int lockIconPadding, int indicationPadding,
+            int ambientPadding) {
+
+        when(mNotificationStackScrollLayoutController.getTop()).thenReturn(0);
+        when(mNotificationStackScrollLayoutController.getHeight()).thenReturn(stackBottom);
+        when(mNotificationStackScrollLayoutController.getBottom()).thenReturn(stackBottom);
+        when(mLockIconViewController.getTop()).thenReturn((float) (stackBottom - lockIconPadding));
+
+        when(mResources.getDimensionPixelSize(R.dimen.keyguard_indication_bottom_padding))
+                .thenReturn(indicationPadding);
+        mNotificationPanelViewController.loadDimens();
+
+        mNotificationPanelViewController.setAmbientIndicationTop(
+                /* ambientIndicationTop= */ stackBottom - ambientPadding,
+                /* ambientTextVisible= */ true);
+    }
+
+    protected void triggerPositionClockAndNotifications() {
+        mNotificationPanelViewController.onQsSetExpansionHeightCalled(false);
+    }
+
+    protected FalsingManager.FalsingTapListener getFalsingTapListener() {
+        for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
+            listener.onViewAttachedToWindow(mView);
+        }
+        assertThat(mFalsingManager.getTapListeners().size()).isEqualTo(1);
+        return mFalsingManager.getTapListeners().get(0);
+    }
+
+    protected void givenViewAttached() {
+        for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
+            listener.onViewAttachedToWindow(mView);
+        }
+    }
+
+    protected ConstraintSet.Layout getConstraintSetLayout(@IdRes int id) {
+        ConstraintSet constraintSet = new ConstraintSet();
+        constraintSet.clone(mNotificationContainerParent);
+        return constraintSet.getConstraint(id).layout;
+    }
+
+    protected void enableSplitShade(boolean enabled) {
+        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(enabled);
+        mNotificationPanelViewController.updateResources();
+    }
+
+    protected void updateMultiUserSetting(boolean enabled) {
+        when(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)).thenReturn(false);
+        when(mUserManager.isUserSwitcherEnabled(false)).thenReturn(enabled);
+        final ArgumentCaptor<ContentObserver> observerCaptor =
+                ArgumentCaptor.forClass(ContentObserver.class);
+        verify(mContentResolver)
+                .registerContentObserver(any(), anyBoolean(), observerCaptor.capture());
+        observerCaptor.getValue().onChange(/* selfChange */ false);
+    }
+
+    protected void updateSmallestScreenWidth(int smallestScreenWidthDp) {
+        Configuration configuration = new Configuration();
+        configuration.smallestScreenWidthDp = smallestScreenWidthDp;
+        mConfigurationController.onConfigurationChanged(configuration);
+    }
+
+    protected void onTouchEvent(MotionEvent ev) {
+        mTouchHandler.onTouch(mView, ev);
+    }
+
+    protected void setDozing(boolean dozing, boolean dozingAlwaysOn) {
+        when(mDozeParameters.getAlwaysOn()).thenReturn(dozingAlwaysOn);
+        mNotificationPanelViewController.setDozing(
+                /* dozing= */ dozing,
+                /* animate= */ false
+        );
+    }
+
+    protected void assertKeyguardStatusViewCentered() {
+        mNotificationPanelViewController.updateResources();
+        assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd).isAnyOf(
+                ConstraintSet.PARENT_ID, ConstraintSet.UNSET);
+    }
+
+    protected void assertKeyguardStatusViewNotCentered() {
+        mNotificationPanelViewController.updateResources();
+        assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd).isEqualTo(
+                R.id.qs_edge_guideline);
+    }
+
+    protected void setIsFullWidth(boolean fullWidth) {
+        float nsslWidth = fullWidth ? PANEL_WIDTH : PANEL_WIDTH / 2f;
+        when(mNotificationStackScrollLayoutController.getWidth()).thenReturn(nsslWidth);
+        triggerLayoutChange();
+    }
+
+    protected void triggerLayoutChange() {
+        mLayoutChangeListener.onLayoutChange(
+                mView,
+                /* left= */ 0,
+                /* top= */ 0,
+                /* right= */ 0,
+                /* bottom= */ 0,
+                /* oldLeft= */ 0,
+                /* oldTop= */ 0,
+                /* oldRight= */ 0,
+                /* oldBottom= */ 0
+        );
+    }
+
+    protected CoroutineDispatcher getMainDispatcher() {
+        return mMainDispatcher;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 65b2ac0..abcde3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.shade;
 
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-
 import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
@@ -31,530 +29,53 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.IdRes;
-import android.content.ContentResolver;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.UserManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.ViewPropertyAnimator;
-import android.view.ViewStub;
-import android.view.ViewTreeObserver;
-import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.constraintlayout.widget.ConstraintSet;
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.internal.util.CollectionUtils;
-import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.FaceAuthApiRequestReason;
-import com.android.keyguard.KeyguardClockSwitch;
-import com.android.keyguard.KeyguardClockSwitchController;
-import com.android.keyguard.KeyguardStatusView;
-import com.android.keyguard.KeyguardStatusViewController;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.LockIconViewController;
-import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent;
-import com.android.keyguard.dagger.KeyguardStatusBarViewComponent;
-import com.android.keyguard.dagger.KeyguardStatusViewComponent;
-import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.classifier.FalsingCollectorFake;
-import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.doze.DozeLog;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.fragments.FragmentHostManager;
-import com.android.systemui.fragments.FragmentService;
-import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
-import com.android.systemui.media.controls.pipeline.MediaDataManager;
-import com.android.systemui.media.controls.ui.KeyguardMediaController;
-import com.android.systemui.media.controls.ui.MediaHierarchyManager;
-import com.android.systemui.model.SysUiState;
-import com.android.systemui.navigationbar.NavigationBarController;
-import com.android.systemui.navigationbar.NavigationModeController;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSFragment;
-import com.android.systemui.screenrecord.RecordingController;
-import com.android.systemui.shade.transition.ShadeTransitionController;
-import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.KeyguardIndicationController;
-import com.android.systemui.statusbar.LockscreenShadeTransitionController;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.NotificationShelfController;
-import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.QsFrameTranslateController;
-import com.android.systemui.statusbar.StatusBarStateControllerImpl;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.notification.ConversationNotificationManager;
-import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
-import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
-import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
-import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.ScrimController;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
-import com.android.systemui.statusbar.phone.TapAgainViewController;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
-import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
-import com.android.systemui.statusbar.window.StatusBarWindowStateController;
-import com.android.systemui.unfold.SysUIUnfoldComponent;
-import com.android.systemui.util.time.FakeSystemClock;
-import com.android.systemui.util.time.SystemClock;
-import com.android.wm.shell.animation.FlingAnimationUtils;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.stubbing.Answer;
 
 import java.util.List;
-import java.util.Optional;
-
-import kotlinx.coroutines.CoroutineDispatcher;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class NotificationPanelViewControllerTest extends SysuiTestCase {
-
-    private static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
-    private static final int NOTIFICATION_SCRIM_TOP_PADDING_IN_SPLIT_SHADE = 50;
-    private static final int PANEL_WIDTH = 500; // Random value just for the test.
-
-    @Mock private CentralSurfaces mCentralSurfaces;
-    @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout;
-    @Mock private KeyguardBottomAreaView mKeyguardBottomArea;
-    @Mock private KeyguardBottomAreaViewController mKeyguardBottomAreaViewController;
-    @Mock private KeyguardBottomAreaView mQsFrame;
-    @Mock private HeadsUpManagerPhone mHeadsUpManager;
-    @Mock private NotificationShelfController mNotificationShelfController;
-    @Mock private NotificationGutsManager mGutsManager;
-    @Mock private KeyguardStatusBarView mKeyguardStatusBar;
-    @Mock private KeyguardUserSwitcherView mUserSwitcherView;
-    @Mock private ViewStub mUserSwitcherStubView;
-    @Mock private HeadsUpTouchHelper.Callback mHeadsUpCallback;
-    @Mock private KeyguardUpdateMonitor mUpdateMonitor;
-    @Mock private KeyguardBypassController mKeyguardBypassController;
-    @Mock private DozeParameters mDozeParameters;
-    @Mock private ScreenOffAnimationController mScreenOffAnimationController;
-    @Mock private NotificationPanelView mView;
-    @Mock private LayoutInflater mLayoutInflater;
-    @Mock private FeatureFlags mFeatureFlags;
-    @Mock private DynamicPrivacyController mDynamicPrivacyController;
-    @Mock private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
-    @Mock private KeyguardStateController mKeyguardStateController;
-    @Mock private DozeLog mDozeLog;
-    @Mock private ShadeLogger mShadeLog;
-    @Mock private ShadeHeightLogger mShadeHeightLogger;
-    @Mock private CommandQueue mCommandQueue;
-    @Mock private VibratorHelper mVibratorHelper;
-    @Mock private LatencyTracker mLatencyTracker;
-    @Mock private PowerManager mPowerManager;
-    @Mock private AccessibilityManager mAccessibilityManager;
-    @Mock private MetricsLogger mMetricsLogger;
-    @Mock private Resources mResources;
-    @Mock private Configuration mConfiguration;
-    @Mock private KeyguardClockSwitch mKeyguardClockSwitch;
-    @Mock private MediaHierarchyManager mMediaHierarchyManager;
-    @Mock private ConversationNotificationManager mConversationNotificationManager;
-    @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    @Mock private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
-    @Mock private KeyguardQsUserSwitchComponent.Factory mKeyguardQsUserSwitchComponentFactory;
-    @Mock private KeyguardQsUserSwitchComponent mKeyguardQsUserSwitchComponent;
-    @Mock private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController;
-    @Mock private KeyguardUserSwitcherComponent.Factory mKeyguardUserSwitcherComponentFactory;
-    @Mock private KeyguardUserSwitcherComponent mKeyguardUserSwitcherComponent;
-    @Mock private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
-    @Mock private KeyguardStatusViewComponent mKeyguardStatusViewComponent;
-    @Mock private KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
-    @Mock private KeyguardStatusBarViewComponent mKeyguardStatusBarViewComponent;
-    @Mock private KeyguardClockSwitchController mKeyguardClockSwitchController;
-    @Mock private KeyguardStatusViewController mKeyguardStatusViewController;
-    @Mock private KeyguardStatusBarViewController mKeyguardStatusBarViewController;
-    @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
-    @Mock private NotificationShadeDepthController mNotificationShadeDepthController;
-    @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    @Mock private AuthController mAuthController;
-    @Mock private ScrimController mScrimController;
-    @Mock private MediaDataManager mMediaDataManager;
-    @Mock private AmbientState mAmbientState;
-    @Mock private UserManager mUserManager;
-    @Mock private UiEventLogger mUiEventLogger;
-    @Mock private LockIconViewController mLockIconViewController;
-    @Mock private KeyguardMediaController mKeyguardMediaController;
-    @Mock private NavigationModeController mNavigationModeController;
-    @Mock private NavigationBarController mNavigationBarController;
-    @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
-    @Mock private ContentResolver mContentResolver;
-    @Mock private TapAgainViewController mTapAgainViewController;
-    @Mock private KeyguardIndicationController mKeyguardIndicationController;
-    @Mock private FragmentService mFragmentService;
-    @Mock private FragmentHostManager mFragmentHostManager;
-    @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
-    @Mock private RecordingController mRecordingController;
-    @Mock private LockscreenGestureLogger mLockscreenGestureLogger;
-    @Mock private DumpManager mDumpManager;
-    @Mock private InteractionJankMonitor mInteractionJankMonitor;
-    @Mock private NotificationsQSContainerController mNotificationsQSContainerController;
-    @Mock private QsFrameTranslateController mQsFrameTranslateController;
-    @Mock private StatusBarWindowStateController mStatusBarWindowStateController;
-    @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
-    @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
-    @Mock private SysUiState mSysUiState;
-    @Mock private NotificationListContainer mNotificationListContainer;
-    @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
-    @Mock private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-    @Mock private ShadeTransitionController mShadeTransitionController;
-    @Mock private QS mQs;
-    @Mock private QSFragment mQSFragment;
-    @Mock private ViewGroup mQsHeader;
-    @Mock private ViewParent mViewParent;
-    @Mock private ViewTreeObserver mViewTreeObserver;
-    @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
-    @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
-    @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
-    @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
-    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
-    @Mock private CoroutineDispatcher mMainDispatcher;
-    @Mock private MotionEvent mDownMotionEvent;
-    @Captor
-    private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
-            mEmptySpaceClickListenerCaptor;
-
-    private NotificationPanelViewController.TouchHandler mTouchHandler;
-    private ConfigurationController mConfigurationController;
-    private SysuiStatusBarStateController mStatusBarStateController;
-    private NotificationPanelViewController mNotificationPanelViewController;
-    private View.AccessibilityDelegate mAccessibilityDelegate;
-    private NotificationsQuickSettingsContainer mNotificationContainerParent;
-    private List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners;
-    private Handler mMainHandler;
-    private View.OnLayoutChangeListener mLayoutChangeListener;
-
-    private final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
-    private final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
-    private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
-    private final ShadeExpansionStateManager mShadeExpansionStateManager =
-            new ShadeExpansionStateManager();
-    private FragmentHostManager.FragmentListener mFragmentListener;
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class NotificationPanelViewControllerTest extends NotificationPanelViewControllerBaseTest {
 
     @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        SystemClock systemClock = new FakeSystemClock();
-        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
-                mInteractionJankMonitor, mShadeExpansionStateManager);
-
-        KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
-        keyguardStatusView.setId(R.id.keyguard_status_view);
+    public void before() {
         DejankUtils.setImmediate(true);
-
-        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
-        when(mHeadsUpCallback.getContext()).thenReturn(mContext);
-        when(mView.getResources()).thenReturn(mResources);
-        when(mView.getWidth()).thenReturn(PANEL_WIDTH);
-        when(mResources.getConfiguration()).thenReturn(mConfiguration);
-        mConfiguration.orientation = ORIENTATION_PORTRAIT;
-        when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
-        mDisplayMetrics.density = 100;
-        when(mResources.getBoolean(R.bool.config_enableNotificationShadeDrag)).thenReturn(true);
-        when(mResources.getDimensionPixelSize(R.dimen.notifications_top_padding_split_shade))
-                .thenReturn(NOTIFICATION_SCRIM_TOP_PADDING_IN_SPLIT_SHADE);
-        when(mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal))
-                .thenReturn(10);
-        when(mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance))
-                .thenReturn(SPLIT_SHADE_FULL_TRANSITION_DISTANCE);
-        when(mView.getContext()).thenReturn(getContext());
-        when(mView.findViewById(R.id.keyguard_header)).thenReturn(mKeyguardStatusBar);
-        when(mView.findViewById(R.id.keyguard_user_switcher_view)).thenReturn(mUserSwitcherView);
-        when(mView.findViewById(R.id.keyguard_user_switcher_stub)).thenReturn(
-                mUserSwitcherStubView);
-        when(mView.findViewById(R.id.keyguard_clock_container)).thenReturn(mKeyguardClockSwitch);
-        when(mView.findViewById(R.id.notification_stack_scroller))
-                .thenReturn(mNotificationStackScrollLayout);
-        when(mNotificationStackScrollLayoutController.getHeight()).thenReturn(1000);
-        when(mNotificationStackScrollLayoutController.getHeadsUpCallback())
-                .thenReturn(mHeadsUpCallback);
-        when(mKeyguardBottomAreaViewController.getView()).thenReturn(mKeyguardBottomArea);
-        when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea);
-        when(mKeyguardBottomArea.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame);
-        when(mView.findViewById(R.id.keyguard_status_view))
-                .thenReturn(mock(KeyguardStatusView.class));
-        mNotificationContainerParent = new NotificationsQuickSettingsContainer(getContext(), null);
-        mNotificationContainerParent.addView(keyguardStatusView);
-        mNotificationContainerParent.onFinishInflate();
-        when(mView.findViewById(R.id.notification_container_parent))
-                .thenReturn(mNotificationContainerParent);
-        when(mFragmentService.getFragmentHostManager(mView)).thenReturn(mFragmentHostManager);
-        FlingAnimationUtils.Builder flingAnimationUtilsBuilder = new FlingAnimationUtils.Builder(
-                mDisplayMetrics);
-        when(mKeyguardQsUserSwitchComponentFactory.build(any()))
-                .thenReturn(mKeyguardQsUserSwitchComponent);
-        when(mKeyguardQsUserSwitchComponent.getKeyguardQsUserSwitchController())
-                .thenReturn(mKeyguardQsUserSwitchController);
-        when(mKeyguardUserSwitcherComponentFactory.build(any()))
-                .thenReturn(mKeyguardUserSwitcherComponent);
-        when(mKeyguardUserSwitcherComponent.getKeyguardUserSwitcherController())
-                .thenReturn(mKeyguardUserSwitcherController);
-        when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(true);
-        when(mQs.getView()).thenReturn(mView);
-        when(mQSFragment.getView()).thenReturn(mView);
-        doAnswer(invocation -> {
-            mFragmentListener = invocation.getArgument(1);
-            return null;
-        }).when(mFragmentHostManager).addTagListener(eq(QS.TAG), any());
-        doAnswer((Answer<Void>) invocation -> {
-            mTouchHandler = invocation.getArgument(0);
-            return null;
-        }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class));
-
-        NotificationWakeUpCoordinator coordinator =
-                new NotificationWakeUpCoordinator(
-                        mDumpManager,
-                        mock(HeadsUpManagerPhone.class),
-                        new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
-                                mInteractionJankMonitor, mShadeExpansionStateManager),
-                        mKeyguardBypassController,
-                        mDozeParameters,
-                        mScreenOffAnimationController,
-                        mock(NotificationWakeUpCoordinatorLogger.class));
-        mConfigurationController = new ConfigurationControllerImpl(mContext);
-        PulseExpansionHandler expansionHandler = new PulseExpansionHandler(
-                mContext,
-                coordinator,
-                mKeyguardBypassController, mHeadsUpManager,
-                mock(NotificationRoundnessManager.class),
-                mConfigurationController,
-                mStatusBarStateController,
-                mFalsingManager,
-                mShadeExpansionStateManager,
-                mLockscreenShadeTransitionController,
-                new FalsingCollectorFake(),
-                mDumpManager);
-        when(mKeyguardStatusViewComponentFactory.build(any()))
-                .thenReturn(mKeyguardStatusViewComponent);
-        when(mKeyguardStatusViewComponent.getKeyguardClockSwitchController())
-                .thenReturn(mKeyguardClockSwitchController);
-        when(mKeyguardStatusViewComponent.getKeyguardStatusViewController())
-                .thenReturn(mKeyguardStatusViewController);
-        when(mKeyguardStatusBarViewComponentFactory.build(any(), any()))
-                .thenReturn(mKeyguardStatusBarViewComponent);
-        when(mKeyguardStatusBarViewComponent.getKeyguardStatusBarViewController())
-                .thenReturn(mKeyguardStatusBarViewController);
-        when(mLayoutInflater.inflate(eq(R.layout.keyguard_status_view), any(), anyBoolean()))
-                .thenReturn(keyguardStatusView);
-        when(mLayoutInflater.inflate(eq(R.layout.keyguard_user_switcher), any(), anyBoolean()))
-                .thenReturn(mUserSwitcherView);
-        when(mLayoutInflater.inflate(eq(R.layout.keyguard_bottom_area), any(), anyBoolean()))
-                .thenReturn(mKeyguardBottomArea);
-        when(mNotificationRemoteInputManager.isRemoteInputActive())
-                .thenReturn(false);
-        when(mInteractionJankMonitor.begin(any(), anyInt()))
-                .thenReturn(true);
-        when(mInteractionJankMonitor.end(anyInt()))
-                .thenReturn(true);
-        doAnswer(invocation -> {
-            ((Runnable) invocation.getArgument(0)).run();
-            return null;
-        }).when(mNotificationShadeWindowController).batchApplyWindowLayoutParams(any());
-        doAnswer(invocation -> {
-            mLayoutChangeListener = invocation.getArgument(0);
-            return null;
-        }).when(mView).addOnLayoutChangeListener(any());
-
-        when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
-        when(mView.getParent()).thenReturn(mViewParent);
-        when(mQs.getHeader()).thenReturn(mQsHeader);
-        when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
-
-        mMainHandler = new Handler(Looper.getMainLooper());
-
-        mNotificationPanelViewController = new NotificationPanelViewController(
-                mView,
-                mMainHandler,
-                mLayoutInflater,
-                mFeatureFlags,
-                coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
-                mFalsingManager, new FalsingCollectorFake(),
-                mKeyguardStateController,
-                mStatusBarStateController,
-                mStatusBarWindowStateController,
-                mNotificationShadeWindowController,
-                mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper,
-                mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
-                mMetricsLogger,
-                mShadeLog,
-                mShadeHeightLogger,
-                mConfigurationController,
-                () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
-                mConversationNotificationManager, mMediaHierarchyManager,
-                mStatusBarKeyguardViewManager,
-                mGutsManager,
-                mNotificationsQSContainerController,
-                mNotificationStackScrollLayoutController,
-                mKeyguardStatusViewComponentFactory,
-                mKeyguardQsUserSwitchComponentFactory,
-                mKeyguardUserSwitcherComponentFactory,
-                mKeyguardStatusBarViewComponentFactory,
-                mLockscreenShadeTransitionController,
-                mAuthController,
-                mScrimController,
-                mUserManager,
-                mMediaDataManager,
-                mNotificationShadeDepthController,
-                mAmbientState,
-                mLockIconViewController,
-                mKeyguardMediaController,
-                mTapAgainViewController,
-                mNavigationModeController,
-                mNavigationBarController,
-                mFragmentService,
-                mContentResolver,
-                mRecordingController,
-                mLargeScreenShadeHeaderController,
-                mScreenOffAnimationController,
-                mLockscreenGestureLogger,
-                mShadeExpansionStateManager,
-                mNotificationRemoteInputManager,
-                mSysUIUnfoldComponent,
-                mInteractionJankMonitor,
-                mQsFrameTranslateController,
-                mSysUiState,
-                () -> mKeyguardBottomAreaViewController,
-                mKeyguardUnlockAnimationController,
-                mKeyguardIndicationController,
-                mNotificationListContainer,
-                mNotificationStackSizeCalculator,
-                mUnlockedScreenOffAnimationController,
-                mShadeTransitionController,
-                systemClock,
-                mKeyguardBottomAreaViewModel,
-                mKeyguardBottomAreaInteractor,
-                mDreamingToLockscreenTransitionViewModel,
-                mOccludedToLockscreenTransitionViewModel,
-                mMainDispatcher,
-                mKeyguardTransitionInteractor,
-                mDumpManager);
-        mNotificationPanelViewController.initDependencies(
-                mCentralSurfaces,
-                null,
-                () -> {},
-                mNotificationShelfController);
-        mNotificationPanelViewController.setTrackingStartedListener(() -> {});
-        mNotificationPanelViewController.setOpenCloseListener(
-                new NotificationPanelViewController.OpenCloseListener() {
-                    @Override
-                    public void onClosingFinished() {}
-
-                    @Override
-                    public void onOpenStarted() {}
-                });
-        mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
-        ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor =
-                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-        verify(mView, atLeast(1)).addOnAttachStateChangeListener(
-                onAttachStateChangeListenerArgumentCaptor.capture());
-        mOnAttachStateChangeListeners = onAttachStateChangeListenerArgumentCaptor.getAllValues();
-
-        ArgumentCaptor<View.AccessibilityDelegate> accessibilityDelegateArgumentCaptor =
-                ArgumentCaptor.forClass(View.AccessibilityDelegate.class);
-        verify(mView).setAccessibilityDelegate(accessibilityDelegateArgumentCaptor.capture());
-        mAccessibilityDelegate = accessibilityDelegateArgumentCaptor.getValue();
-        mNotificationPanelViewController.getStatusBarStateController()
-                .addCallback(mNotificationPanelViewController.getStatusBarStateListener());
-        mNotificationPanelViewController
-                .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
-        verify(mNotificationStackScrollLayoutController)
-                .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
-        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
-        reset(mKeyguardStatusViewController);
-    }
-
-    @After
-    public void tearDown() {
-        mNotificationPanelViewController.cancelHeightAnimator();
-        mMainHandler.removeCallbacksAndMessages(null);
     }
 
     @Test
@@ -609,23 +130,6 @@
                 .isNotEqualTo(-1);
     }
 
-    private void setBottomPadding(int stackBottom, int lockIconPadding, int indicationPadding,
-            int ambientPadding) {
-
-        when(mNotificationStackScrollLayoutController.getTop()).thenReturn(0);
-        when(mNotificationStackScrollLayoutController.getHeight()).thenReturn(stackBottom);
-        when(mNotificationStackScrollLayoutController.getBottom()).thenReturn(stackBottom);
-        when(mLockIconViewController.getTop()).thenReturn((float) (stackBottom - lockIconPadding));
-
-        when(mResources.getDimensionPixelSize(R.dimen.keyguard_indication_bottom_padding))
-                .thenReturn(indicationPadding);
-        mNotificationPanelViewController.loadDimens();
-
-        mNotificationPanelViewController.setAmbientIndicationTop(
-                /* ambientIndicationTop= */ stackBottom - ambientPadding,
-                /* ambientTextVisible= */ true);
-    }
-
     @Test
     @Ignore("b/261472011 - Test appears inconsistent across environments")
     public void getVerticalSpaceForLockscreenNotifications_useLockIconBottomPadding_returnsSpaceAvailable() {
@@ -732,27 +236,14 @@
 
     @Test
     public void testOnTouchEvent_expansionCanBeBlocked() {
-        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
-                0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
-                0 /* metaState */));
-        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
-                0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 200f /* y */,
-                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0));
+        onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 200f, 0));
         assertThat((int) mNotificationPanelViewController.getExpandedHeight()).isEqualTo(200);
-        assertThat(mNotificationPanelViewController.isTrackingBlocked()).isFalse();
 
         mNotificationPanelViewController.blockExpansionForCurrentTouch();
-        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
-                0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */,
-                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 300f, 0));
         // Expansion should not have changed because it was blocked
         assertThat((int) mNotificationPanelViewController.getExpandedHeight()).isEqualTo(200);
-        assertThat(mNotificationPanelViewController.isTrackingBlocked()).isTrue();
-
-        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
-                0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
-                0 /* metaState */));
-        assertThat(mNotificationPanelViewController.isTrackingBlocked()).isFalse();
     }
 
     @Test
@@ -943,68 +434,6 @@
     }
 
     @Test
-    public void testDisableUserSwitcherAfterEnabling_returnsViewStubToTheViewHierarchy() {
-        givenViewAttached();
-        when(mResources.getBoolean(
-                com.android.internal.R.bool.config_keyguardUserSwitcher)).thenReturn(true);
-        updateMultiUserSetting(true);
-        clearInvocations(mView);
-
-        updateMultiUserSetting(false);
-
-        ArgumentCaptor<View> captor = ArgumentCaptor.forClass(View.class);
-        verify(mView, atLeastOnce()).addView(captor.capture(), anyInt());
-        final View userSwitcherStub = CollectionUtils.find(captor.getAllValues(),
-                view -> view.getId() == R.id.keyguard_user_switcher_stub);
-        assertThat(userSwitcherStub).isNotNull();
-        assertThat(userSwitcherStub).isInstanceOf(ViewStub.class);
-    }
-
-    @Test
-    public void testChangeSmallestScreenWidthAndUserSwitchEnabled_inflatesUserSwitchView() {
-        givenViewAttached();
-        when(mView.findViewById(R.id.keyguard_user_switcher_view)).thenReturn(null);
-        updateSmallestScreenWidth(300);
-        when(mResources.getBoolean(
-                com.android.internal.R.bool.config_keyguardUserSwitcher)).thenReturn(true);
-        when(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)).thenReturn(false);
-        when(mUserManager.isUserSwitcherEnabled(false)).thenReturn(true);
-
-        updateSmallestScreenWidth(800);
-
-        verify(mUserSwitcherStubView).inflate();
-    }
-
-    @Test
-    public void testFinishInflate_userSwitcherDisabled_doNotInflateUserSwitchView_initClock() {
-        givenViewAttached();
-        when(mResources.getBoolean(
-                com.android.internal.R.bool.config_keyguardUserSwitcher)).thenReturn(true);
-        when(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)).thenReturn(false);
-        when(mUserManager.isUserSwitcherEnabled(false /* showEvenIfNotActionable */))
-                .thenReturn(false);
-
-        mNotificationPanelViewController.onFinishInflate();
-
-        verify(mUserSwitcherStubView, never()).inflate();
-        verify(mKeyguardStatusViewController, times(3)).displayClock(LARGE, /* animate */ true);
-    }
-
-    @Test
-    public void testReInflateViews_userSwitcherDisabled_doNotInflateUserSwitchView() {
-        givenViewAttached();
-        when(mResources.getBoolean(
-                com.android.internal.R.bool.config_keyguardUserSwitcher)).thenReturn(true);
-        when(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)).thenReturn(false);
-        when(mUserManager.isUserSwitcherEnabled(false /* showEvenIfNotActionable */))
-                .thenReturn(false);
-
-        mNotificationPanelViewController.reInflateViews();
-
-        verify(mUserSwitcherStubView, never()).inflate();
-    }
-
-    @Test
     public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -1022,7 +451,7 @@
     @Test
     public void testCanCollapsePanelOnTouch_trueWhenInSettings() {
         mStatusBarStateController.setState(SHADE);
-        mNotificationPanelViewController.setQsExpanded(true);
+        when(mQsController.getExpanded()).thenReturn(true);
 
         assertThat(mNotificationPanelViewController.canCollapsePanelOnTouch()).isTrue();
     }
@@ -1031,7 +460,7 @@
     public void testCanCollapsePanelOnTouch_falseInDualPaneShade() {
         mStatusBarStateController.setState(SHADE);
         enableSplitShade(/* enabled= */ true);
-        mNotificationPanelViewController.setQsExpanded(true);
+        when(mQsController.getExpanded()).thenReturn(true);
 
         assertThat(mNotificationPanelViewController.canCollapsePanelOnTouch()).isFalse();
     }
@@ -1080,29 +509,9 @@
     }
 
     @Test
-    public void testDoubleTapRequired_Keyguard() {
-        FalsingManager.FalsingTapListener listener = getFalsingTapListener();
-        mStatusBarStateController.setState(KEYGUARD);
-
-        listener.onAdditionalTapRequired();
-
-        verify(mKeyguardIndicationController).showTransientIndication(anyInt());
-    }
-
-    @Test
-    public void testDoubleTapRequired_ShadeLocked() {
-        FalsingManager.FalsingTapListener listener = getFalsingTapListener();
-        mStatusBarStateController.setState(SHADE_LOCKED);
-
-        listener.onAdditionalTapRequired();
-
-        verify(mTapAgainViewController).show();
-    }
-
-    @Test
     public void testRotatingToSplitShadeWithQsExpanded_transitionsToShadeLocked() {
         mStatusBarStateController.setState(KEYGUARD);
-        mNotificationPanelViewController.setQsExpanded(true);
+        when(mQsController.getExpanded()).thenReturn(true);
 
         enableSplitShade(true);
 
@@ -1113,24 +522,18 @@
     public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() {
         enableSplitShade(true);
         mStatusBarStateController.setState(SHADE);
-        mNotificationPanelViewController.setQsExpanded(true);
-
         mStatusBarStateController.setState(KEYGUARD);
 
-        assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false);
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false);
+        verify(mQsController).closeQs();
     }
 
     @Test
     public void testLockedSplitShadeTransitioningToKeyguard_closesQS() {
         enableSplitShade(true);
         mStatusBarStateController.setState(SHADE_LOCKED);
-        mNotificationPanelViewController.setQsExpanded(true);
-
         mStatusBarStateController.setState(KEYGUARD);
 
-        assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false);
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false);
+        verify(mQsController).closeQs();
     }
 
     @Test
@@ -1142,7 +545,7 @@
         verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
 
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
-        mNotificationPanelViewController.closeQs();
+        triggerPositionClockAndNotifications();
         verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true);
     }
 
@@ -1270,18 +673,6 @@
     }
 
     @Test
-    public void testLargeScreenHeaderMadeActiveForLargeScreen() {
-        mStatusBarStateController.setState(SHADE);
-        when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(true);
-        mNotificationPanelViewController.updateResources();
-        verify(mLargeScreenShadeHeaderController).setLargeScreenActive(true);
-
-        when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(false);
-        mNotificationPanelViewController.updateResources();
-        verify(mLargeScreenShadeHeaderController).setLargeScreenActive(false);
-    }
-
-    @Test
     public void testExpandWithQsMethodIsUsingLockscreenTransitionController() {
         enableSplitShade(/* enabled= */ true);
         mStatusBarStateController.setState(KEYGUARD);
@@ -1335,12 +726,14 @@
     @Test
     public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
         enableSplitShade(/* enabled= */ true);
+        mShadeExpansionStateManager.updateState(STATE_OPEN);
+        verify(mQsController).setExpandImmediate(false);
+
         mShadeExpansionStateManager.updateState(STATE_CLOSED);
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
+        verify(mQsController, times(2)).setExpandImmediate(false);
 
         mShadeExpansionStateManager.updateState(STATE_OPENING);
-
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isTrue();
+        verify(mQsController).setExpandImmediate(true);
     }
 
     @Test
@@ -1352,33 +745,28 @@
         // going to lockscreen would trigger STATE_OPENING
         mShadeExpansionStateManager.updateState(STATE_OPENING);
 
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
+        verify(mQsController, never()).setExpandImmediate(true);
     }
 
     @Test
     public void testQsImmediateResetsWhenPanelOpensOrCloses() {
-        mNotificationPanelViewController.setQsExpandImmediate(true);
         mShadeExpansionStateManager.updateState(STATE_OPEN);
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
-
-        mNotificationPanelViewController.setQsExpandImmediate(true);
         mShadeExpansionStateManager.updateState(STATE_CLOSED);
-        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
+        verify(mQsController, times(2)).setExpandImmediate(false);
     }
 
     @Test
     public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() {
         // to make sure shade is in expanded state
         mNotificationPanelViewController.startWaitingForOpenPanelGesture();
-        assertThat(mNotificationPanelViewController.isQsExpanded()).isFalse();
 
         // switch to split shade from portrait (default state)
         enableSplitShade(/* enabled= */ true);
-        assertThat(mNotificationPanelViewController.isQsExpanded()).isTrue();
+        verify(mQsController).setExpanded(true);
 
         // switch to portrait from split shade
         enableSplitShade(/* enabled= */ false);
-        assertThat(mNotificationPanelViewController.isQsExpanded()).isFalse();
+        verify(mQsController).setExpanded(false);
     }
 
     @Test
@@ -1390,73 +778,11 @@
 
         assertThat(mNotificationPanelViewController.isClosing()).isFalse();
         mNotificationPanelViewController.animateCloseQs(false);
+
         assertThat(mNotificationPanelViewController.isClosing()).isTrue();
     }
 
     @Test
-    public void testPanelStaysOpenWhenClosingQs() {
-        mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
-                /* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0);
-        mNotificationPanelViewController.setExpandedFraction(1f);
-
-        assertThat(mNotificationPanelViewController.isClosing()).isFalse();
-        mNotificationPanelViewController.animateCloseQs(false);
-        assertThat(mNotificationPanelViewController.isClosing()).isFalse();
-    }
-
-    @Test
-    public void interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking() {
-        mNotificationPanelViewController.setQs(mQs);
-        when(mQsFrame.getX()).thenReturn(0f);
-        when(mQsFrame.getWidth()).thenReturn(1000);
-        when(mQsHeader.getTop()).thenReturn(0);
-        when(mQsHeader.getBottom()).thenReturn(1000);
-        NotificationPanelViewController.TouchHandler touchHandler =
-                mNotificationPanelViewController.createTouchHandler();
-
-        mNotificationPanelViewController.setExpandedFraction(1f);
-        touchHandler.onInterceptTouchEvent(
-                createMotionEvent(/* x= */ 0, /* y= */ 0, MotionEvent.ACTION_DOWN));
-        touchHandler.onInterceptTouchEvent(
-                createMotionEvent(/* x= */ 0, /* y= */ 500, MotionEvent.ACTION_MOVE));
-
-        assertThat(mNotificationPanelViewController.isQsTracking()).isTrue();
-    }
-
-    @Test
-    public void interceptTouchEvent_withinQs_shadeExpanded_inSplitShade_doesNotStartQsTracking() {
-        enableSplitShade(true);
-        mNotificationPanelViewController.setQs(mQs);
-        when(mQsFrame.getX()).thenReturn(0f);
-        when(mQsFrame.getWidth()).thenReturn(1000);
-        when(mQsHeader.getTop()).thenReturn(0);
-        when(mQsHeader.getBottom()).thenReturn(1000);
-        NotificationPanelViewController.TouchHandler touchHandler =
-                mNotificationPanelViewController.createTouchHandler();
-
-        mNotificationPanelViewController.setExpandedFraction(1f);
-        touchHandler.onInterceptTouchEvent(
-                createMotionEvent(/* x= */ 0, /* y= */ 0, MotionEvent.ACTION_DOWN));
-        touchHandler.onInterceptTouchEvent(
-                createMotionEvent(/* x= */ 0, /* y= */ 500, MotionEvent.ACTION_MOVE));
-
-        assertThat(mNotificationPanelViewController.isQsTracking()).isFalse();
-    }
-
-    @Test
-    public void testOnAttachRefreshStatusBarState() {
-        mStatusBarStateController.setState(KEYGUARD);
-        when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(false);
-        for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
-            listener.onViewAttachedToWindow(mView);
-        }
-        verify(mKeyguardStatusViewController).setKeyguardStatusViewVisibility(
-                KEYGUARD/*statusBarState*/,
-                false/*keyguardFadingAway*/,
-                false/*goingToFullShade*/, SHADE/*oldStatusBarState*/);
-    }
-
-    @Test
     public void getMaxPanelTransitionDistance_expanding_inSplitShade_returnsSplitShadeFullTransitionDistance() {
         enableSplitShade(true);
         mNotificationPanelViewController.expandWithQs();
@@ -1471,11 +797,14 @@
         enableSplitShade(true);
         mNotificationPanelViewController.expandWithQs();
         when(mHeadsUpManager.isTrackingHeadsUp()).thenReturn(true);
+        when(mQsController.calculatePanelHeightExpanded(anyInt())).thenReturn(10000);
         mNotificationPanelViewController.setHeadsUpDraggingStartingHeight(
                 SPLIT_SHADE_FULL_TRANSITION_DISTANCE);
 
         int maxDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance();
 
+        // make sure we're ignoring the placeholder value for Qs max height
+        assertThat(maxDistance).isLessThan(10000);
         assertThat(maxDistance).isGreaterThan(SPLIT_SHADE_FULL_TRANSITION_DISTANCE);
     }
 
@@ -1502,95 +831,26 @@
 
     @Test
     public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() {
-        mNotificationPanelViewController.setQs(mQs);
-
         setIsFullWidth(true);
 
-        verify(mQs).setIsNotificationPanelFullWidth(true);
+        verify(mQsController).setNotificationPanelFullWidth(true);
     }
 
     @Test
     public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() {
-        mNotificationPanelViewController.setQs(mQs);
-
         setIsFullWidth(false);
 
-        verify(mQs).setIsNotificationPanelFullWidth(false);
+        verify(mQsController).setNotificationPanelFullWidth(false);
     }
 
     @Test
     public void onLayoutChange_qsNotSet_doesNotCrash() {
-        mNotificationPanelViewController.setQs(null);
+        mQuickSettingsController.setQs(null);
 
         triggerLayoutChange();
     }
 
     @Test
-    public void onQsFragmentAttached_fullWidth_setsFullWidthTrueOnQS() {
-        setIsFullWidth(true);
-        givenViewAttached();
-        mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
-
-        verify(mQSFragment).setIsNotificationPanelFullWidth(true);
-    }
-
-    @Test
-    public void onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS() {
-        setIsFullWidth(false);
-        givenViewAttached();
-        mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
-
-        verify(mQSFragment).setIsNotificationPanelFullWidth(false);
-    }
-
-    @Test
-    public void setQsExpansion_lockscreenShadeTransitionInProgress_usesLockscreenSquishiness() {
-        float squishinessFraction = 0.456f;
-        mNotificationPanelViewController.setQs(mQs);
-        when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
-                .thenReturn(squishinessFraction);
-        when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
-                .thenReturn(0.987f);
-        // Call setTransitionToFullShadeAmount to get into the full shade transition in progress
-        // state.
-        mNotificationPanelViewController.setTransitionToFullShadeAmount(
-                /* pxAmount= */ 234,
-                /* animate= */ false,
-                /* delay= */ 0
-        );
-
-        mNotificationPanelViewController.setQsExpansionHeight(/* height= */ 123);
-
-        // First for setTransitionToFullShadeAmount and then setQsExpansion
-        verify(mQs, times(2)).setQsExpansion(
-                /* expansion= */ anyFloat(),
-                /* panelExpansionFraction= */ anyFloat(),
-                /* proposedTranslation= */ anyFloat(),
-                eq(squishinessFraction)
-        );
-    }
-
-    @Test
-    public void setQsExpansion_lockscreenShadeTransitionNotInProgress_usesStandardSquishiness() {
-        float lsSquishinessFraction = 0.456f;
-        float nsslSquishinessFraction = 0.987f;
-        mNotificationPanelViewController.setQs(mQs);
-        when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
-                .thenReturn(lsSquishinessFraction);
-        when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
-                .thenReturn(nsslSquishinessFraction);
-
-        mNotificationPanelViewController.setQsExpansionHeight(/* height= */ 123);
-
-        verify(mQs).setQsExpansion(
-                /* expansion= */ anyFloat(),
-                /* panelExpansionFraction= */ anyFloat(),
-                /* proposedTranslation= */ anyFloat(),
-                eq(nsslSquishinessFraction)
-        );
-    }
-
-    @Test
     public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
         StatusBarStateController.StateListener statusBarStateListener =
                 mNotificationPanelViewController.getStatusBarStateListener();
@@ -1715,15 +975,6 @@
         int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance();
         mNotificationPanelViewController.setExpandedHeight(transitionDistance);
         assertThat(mNotificationPanelViewController.isShadeFullyOpen()).isFalse();
-
-        // set maxQsExpansion in NPVC
-        int maxQsExpansion = 123;
-        mNotificationPanelViewController.setQs(mQs);
-        when(mQs.getDesiredHeight()).thenReturn(maxQsExpansion);
-        triggerLayoutChange();
-
-        mNotificationPanelViewController.setQsExpansionHeight(maxQsExpansion);
-        assertThat(mNotificationPanelViewController.isShadeFullyOpen()).isTrue();
     }
 
     @Test
@@ -1731,98 +982,4 @@
         mStatusBarStateController.setState(SHADE_LOCKED);
         assertThat(mNotificationPanelViewController.isShadeFullyOpen()).isTrue();
     }
-
-    private static MotionEvent createMotionEvent(int x, int y, int action) {
-        return MotionEvent.obtain(
-                /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0);
-    }
-
-    private void triggerPositionClockAndNotifications() {
-        mNotificationPanelViewController.closeQs();
-    }
-
-    private FalsingManager.FalsingTapListener getFalsingTapListener() {
-        for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
-            listener.onViewAttachedToWindow(mView);
-        }
-        assertThat(mFalsingManager.getTapListeners().size()).isEqualTo(1);
-        return mFalsingManager.getTapListeners().get(0);
-    }
-
-    private void givenViewAttached() {
-        for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
-            listener.onViewAttachedToWindow(mView);
-        }
-    }
-
-    private ConstraintSet.Layout getConstraintSetLayout(@IdRes int id) {
-        ConstraintSet constraintSet = new ConstraintSet();
-        constraintSet.clone(mNotificationContainerParent);
-        return constraintSet.getConstraint(id).layout;
-    }
-
-    private void enableSplitShade(boolean enabled) {
-        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(enabled);
-        mNotificationPanelViewController.updateResources();
-    }
-
-    private void updateMultiUserSetting(boolean enabled) {
-        when(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)).thenReturn(false);
-        when(mUserManager.isUserSwitcherEnabled(false)).thenReturn(enabled);
-        final ArgumentCaptor<ContentObserver> observerCaptor =
-                ArgumentCaptor.forClass(ContentObserver.class);
-        verify(mContentResolver)
-                .registerContentObserver(any(), anyBoolean(), observerCaptor.capture());
-        observerCaptor.getValue().onChange(/* selfChange */ false);
-    }
-
-    private void updateSmallestScreenWidth(int smallestScreenWidthDp) {
-        Configuration configuration = new Configuration();
-        configuration.smallestScreenWidthDp = smallestScreenWidthDp;
-        mConfigurationController.onConfigurationChanged(configuration);
-    }
-
-    private void onTouchEvent(MotionEvent ev) {
-        mTouchHandler.onTouch(mView, ev);
-    }
-
-    private void setDozing(boolean dozing, boolean dozingAlwaysOn) {
-        when(mDozeParameters.getAlwaysOn()).thenReturn(dozingAlwaysOn);
-        mNotificationPanelViewController.setDozing(
-                /* dozing= */ dozing,
-                /* animate= */ false
-        );
-    }
-
-    private void assertKeyguardStatusViewCentered() {
-        mNotificationPanelViewController.updateResources();
-        assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd).isAnyOf(
-                ConstraintSet.PARENT_ID, ConstraintSet.UNSET);
-    }
-
-    private void assertKeyguardStatusViewNotCentered() {
-        mNotificationPanelViewController.updateResources();
-        assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd).isEqualTo(
-                R.id.qs_edge_guideline);
-    }
-
-    private void setIsFullWidth(boolean fullWidth) {
-        float nsslWidth = fullWidth ? PANEL_WIDTH : PANEL_WIDTH / 2f;
-        when(mNotificationStackScrollLayoutController.getWidth()).thenReturn(nsslWidth);
-        triggerLayoutChange();
-    }
-
-    private void triggerLayoutChange() {
-        mLayoutChangeListener.onLayoutChange(
-                mView,
-                /* left= */ 0,
-                /* top= */ 0,
-                /* right= */ 0,
-                /* bottom= */ 0,
-                /* oldLeft= */ 0,
-                /* oldTop= */ 0,
-                /* oldRight= */ 0,
-                /* oldBottom= */ 0
-        );
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..0c046e9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.shade
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewStub
+import androidx.test.filters.SmallTest
+import com.android.internal.util.CollectionUtils
+import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.systemui.R
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class NotificationPanelViewControllerWithCoroutinesTest :
+    NotificationPanelViewControllerBaseTest() {
+
+    @Captor private lateinit var viewCaptor: ArgumentCaptor<View>
+
+    override fun getMainDispatcher() = Dispatchers.Main.immediate
+
+    @Test
+    fun testDisableUserSwitcherAfterEnabling_returnsViewStubToTheViewHierarchy() = runTest {
+        launch(Dispatchers.Main.immediate) { givenViewAttached() }
+        advanceUntilIdle()
+
+        whenever(mResources.getBoolean(com.android.internal.R.bool.config_keyguardUserSwitcher))
+            .thenReturn(true)
+        updateMultiUserSetting(true)
+        clearInvocations(mView)
+
+        updateMultiUserSetting(false)
+
+        verify(mView, atLeastOnce()).addView(viewCaptor.capture(), anyInt())
+        val userSwitcherStub =
+            CollectionUtils.find(
+                viewCaptor.getAllValues(),
+                { view -> view.getId() == R.id.keyguard_user_switcher_stub }
+            )
+        assertThat(userSwitcherStub).isNotNull()
+        assertThat(userSwitcherStub).isInstanceOf(ViewStub::class.java)
+    }
+
+    @Test
+    fun testChangeSmallestScreenWidthAndUserSwitchEnabled_inflatesUserSwitchView() = runTest {
+        launch(Dispatchers.Main.immediate) { givenViewAttached() }
+        advanceUntilIdle()
+
+        whenever(mView.findViewById<View>(R.id.keyguard_user_switcher_view)).thenReturn(null)
+        updateSmallestScreenWidth(300)
+        whenever(mResources.getBoolean(com.android.internal.R.bool.config_keyguardUserSwitcher))
+            .thenReturn(true)
+        whenever(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user))
+            .thenReturn(false)
+        whenever(mUserManager.isUserSwitcherEnabled(false)).thenReturn(true)
+
+        updateSmallestScreenWidth(800)
+
+        verify(mUserSwitcherStubView).inflate()
+    }
+
+    @Test
+    fun testFinishInflate_userSwitcherDisabled_doNotInflateUserSwitchView_initClock() = runTest {
+        launch(Dispatchers.Main.immediate) { givenViewAttached() }
+        advanceUntilIdle()
+
+        whenever(mResources.getBoolean(com.android.internal.R.bool.config_keyguardUserSwitcher))
+            .thenReturn(true)
+        whenever(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user))
+            .thenReturn(false)
+        whenever(mUserManager.isUserSwitcherEnabled(false /* showEvenIfNotActionable */))
+            .thenReturn(false)
+
+        mNotificationPanelViewController.onFinishInflate()
+
+        verify(mUserSwitcherStubView, never()).inflate()
+        verify(mKeyguardStatusViewController, times(3)).displayClock(LARGE, /* animate */ true)
+
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun testReInflateViews_userSwitcherDisabled_doNotInflateUserSwitchView() = runTest {
+        launch(Dispatchers.Main.immediate) { givenViewAttached() }
+        advanceUntilIdle()
+
+        whenever(mResources.getBoolean(com.android.internal.R.bool.config_keyguardUserSwitcher))
+            .thenReturn(true)
+        whenever(mResources.getBoolean(R.bool.qs_show_user_switcher_for_single_user))
+            .thenReturn(false)
+        whenever(mUserManager.isUserSwitcherEnabled(false /* showEvenIfNotActionable */))
+            .thenReturn(false)
+
+        mNotificationPanelViewController.reInflateViews()
+
+        verify(mUserSwitcherStubView, never()).inflate()
+
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun testDoubleTapRequired_Keyguard() = runTest {
+        launch(Dispatchers.Main.immediate) {
+            val listener = getFalsingTapListener()
+            mStatusBarStateController.setState(KEYGUARD)
+
+            listener.onAdditionalTapRequired()
+
+            verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+        }
+        advanceUntilIdle()
+    }
+
+    @Test
+    fun testDoubleTapRequired_ShadeLocked() = runTest {
+        launch(Dispatchers.Main.immediate) {
+            val listener = getFalsingTapListener()
+            mStatusBarStateController.setState(SHADE_LOCKED)
+
+            listener.onAdditionalTapRequired()
+
+            verify(mTapAgainViewController).show()
+        }
+        advanceUntilIdle()
+    }
+
+    @Test
+    fun testOnAttachRefreshStatusBarState() = runTest {
+        launch(Dispatchers.Main.immediate) {
+            mStatusBarStateController.setState(KEYGUARD)
+            whenever(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(false)
+            mOnAttachStateChangeListeners.forEach { it.onViewAttachedToWindow(mView) }
+            verify(mKeyguardStatusViewController)
+                .setKeyguardStatusViewVisibility(
+                    KEYGUARD /*statusBarState*/,
+                    false /*keyguardFadingAway*/,
+                    false /*goingToFullShade*/,
+                    SHADE /*oldStatusBarState*/
+                )
+        }
+        advanceUntilIdle()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
index bdafc7d..dfb1bce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
@@ -12,9 +12,11 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.fragments.FragmentHostManager
+import com.android.systemui.fragments.FragmentService
 import com.android.systemui.navigationbar.NavigationModeController
 import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
+import com.android.systemui.plugins.qs.QS
 import com.android.systemui.recents.OverviewProxyService
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -29,6 +31,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.eq
@@ -36,8 +39,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -64,11 +67,13 @@
     @Mock
     private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
     @Mock
-    private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController
+    private lateinit var mShadeHeaderController: ShadeHeaderController
     @Mock
     private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
     @Mock
-    private lateinit var featureFlags: FeatureFlags
+    private lateinit var fragmentService: FragmentService
+    @Mock
+    private lateinit var fragmentHostManager: FragmentHostManager
     @Captor
     lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
     @Captor
@@ -77,6 +82,8 @@
     lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
     @Captor
     lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
+    @Captor
+    lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
 
     private lateinit var controller: NotificationsQSContainerController
     private lateinit var navigationModeCallback: ModeChangedListener
@@ -91,15 +98,17 @@
         mContext.ensureTestableResources()
         whenever(notificationsQSContainer.context).thenReturn(mContext)
         whenever(notificationsQSContainer.resources).thenReturn(mContext.resources)
+        whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
         fakeSystemClock = FakeSystemClock()
         delayableExecutor = FakeExecutor(fakeSystemClock)
+
         controller = NotificationsQSContainerController(
                 notificationsQSContainer,
                 navigationModeController,
                 overviewProxyService,
-                largeScreenShadeHeaderController,
+                mShadeHeaderController,
                 shadeExpansionStateManager,
-                featureFlags,
+                fragmentService,
                 delayableExecutor
         )
 
@@ -114,9 +123,10 @@
         doNothing().`when`(notificationsQSContainer)
                 .setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
         doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture())
-
+        doNothing().`when`(notificationsQSContainer)
+            .addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
         controller.init()
-        controller.onViewAttached()
+        attachStateListenerCaptor.value.onViewAttachedToWindow(notificationsQSContainer)
 
         navigationModeCallback = navigationModeCaptor.value
         taskbarVisibilityCallback = taskbarVisibilityCaptor.value
@@ -382,9 +392,9 @@
                 container,
                 navigationModeController,
                 overviewProxyService,
-                largeScreenShadeHeaderController,
+                mShadeHeaderController,
                 shadeExpansionStateManager,
-                featureFlags,
+                fragmentService,
                 delayableExecutor
         )
         controller.updateConstraints()
@@ -414,16 +424,27 @@
     @Test
     fun testStartCustomizingWithDuration() {
         controller.setCustomizerShowing(true, 100L)
-        verify(largeScreenShadeHeaderController).startCustomizingAnimation(true, 100L)
+        verify(mShadeHeaderController).startCustomizingAnimation(true, 100L)
     }
 
     @Test
     fun testEndCustomizingWithDuration() {
         controller.setCustomizerShowing(true, 0L) // Only tracks changes
-        reset(largeScreenShadeHeaderController)
+        reset(mShadeHeaderController)
 
         controller.setCustomizerShowing(false, 100L)
-        verify(largeScreenShadeHeaderController).startCustomizingAnimation(false, 100L)
+        verify(mShadeHeaderController).startCustomizingAnimation(false, 100L)
+    }
+
+    @Test
+    fun testTagListenerAdded() {
+        verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(notificationsQSContainer))
+    }
+
+    @Test
+    fun testTagListenerRemoved() {
+        attachStateListenerCaptor.value.onViewDetachedFromWindow(notificationsQSContainer)
+        verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(notificationsQSContainer))
     }
 
     private fun disableSplitShade() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 08a9c96..526dc8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -46,11 +46,14 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -68,6 +71,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
+import java.util.List;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
@@ -91,13 +96,21 @@
     @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
+    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
 
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
-
+    private float mPreferredRefreshRate = -1;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        // Preferred refresh rate is equal to the first displayMode's refresh rate
+        mPreferredRefreshRate = mContext.getDisplay().getSupportedModes()[0].getRefreshRate();
+        overrideResource(
+                R.integer.config_keyguardRefreshRate,
+                (int) mPreferredRefreshRate
+        );
+
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
@@ -117,6 +130,7 @@
 
         mNotificationShadeWindowController.attach();
         verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
+        verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt());
     }
 
     @Test
@@ -334,4 +348,59 @@
         assertThat(mLayoutParameters.getValue().screenOrientation)
                 .isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
     }
+
+    @Test
+    public void udfpsEnrolled_minAndMaxRefreshRateSetToPreferredRefreshRate() {
+        // GIVEN udfps is enrolled
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+        // WHEN keyguard is showing
+        setKeyguardShowing();
+
+        // THEN min and max refresh rate is set to the preferredRefreshRate
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+        final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+        assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+        assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+    }
+
+    @Test
+    public void udfpsNotEnrolled_refreshRateUnset() {
+        // GIVEN udfps is NOT enrolled
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+
+        // WHEN keyguard is showing
+        setKeyguardShowing();
+
+        // THEN min and max refresh rate aren't set (set to 0)
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+        final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+        assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+        assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+    }
+
+    @Test
+    public void keyguardNotShowing_refreshRateUnset() {
+        // GIVEN UDFPS is enrolled
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+        // WHEN keyguard is NOT showing
+        mNotificationShadeWindowController.setKeyguardShowing(false);
+
+        // THEN min and max refresh rate aren't set (set to 0)
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+        final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+        assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+        assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+    }
+
+    private void setKeyguardShowing() {
+        mNotificationShadeWindowController.setKeyguardShowing(true);
+        mNotificationShadeWindowController.setKeyguardGoingAway(false);
+        mNotificationShadeWindowController.setKeyguardFadingAway(false);
+        mStateListener.getValue().onStateChanged(StatusBarState.KEYGUARD);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index c3207c2..82a5743 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -21,16 +21,19 @@
 import android.view.MotionEvent
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardHostViewController
+import com.android.keyguard.KeyguardSecurityContainerController
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.dock.DockManager
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationInsetsController
@@ -43,13 +46,17 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
+import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.emptyFlow
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
+import org.mockito.Mockito
 import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -59,48 +66,32 @@
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
 class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
-    @Mock
-    private lateinit var view: NotificationShadeWindowView
-    @Mock
-    private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
-    @Mock
-    private lateinit var centralSurfaces: CentralSurfaces
-    @Mock
-    private lateinit var dockManager: DockManager
-    @Mock
-    private lateinit var notificationPanelViewController: NotificationPanelViewController
-    @Mock
-    private lateinit var notificationShadeDepthController: NotificationShadeDepthController
-    @Mock
-    private lateinit var notificationShadeWindowController: NotificationShadeWindowController
-    @Mock
-    private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
-    @Mock
-    private lateinit var featureFlags: FeatureFlags
-    @Mock
-    private lateinit var ambientState: AmbientState
-    @Mock
-    private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
-    @Mock
-    private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
-    @Mock
-    private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
-    @Mock
-    private lateinit var statusBarWindowStateController: StatusBarWindowStateController
+    @Mock private lateinit var view: NotificationShadeWindowView
+    @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var centralSurfaces: CentralSurfaces
+    @Mock private lateinit var dockManager: DockManager
+    @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController
+    @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController
+    @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
+    @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
+    @Mock private lateinit var ambientState: AmbientState
+    @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
+    @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
+    @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController
     @Mock
     private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController
-    @Mock
-    private lateinit var lockIconViewController: LockIconViewController
-    @Mock
-    private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
-    @Mock
-    private lateinit var pulsingGestureListener: PulsingGestureListener
-    @Mock
-    private lateinit var notificationInsetsController: NotificationInsetsController
+    @Mock private lateinit var lockIconViewController: LockIconViewController
+    @Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
+    @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock private lateinit var notificationInsetsController: NotificationInsetsController
+    @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
-    @Mock lateinit var keyguardBouncerContainer: ViewGroup
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
-    @Mock lateinit var keyguardHostViewController: KeyguardHostViewController
+    @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
+    @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    @Mock
+    lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
 
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
     private lateinit var interactionEventHandler: InteractionEventHandler
@@ -111,35 +102,45 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(view.bottom).thenReturn(VIEW_BOTTOM)
-        underTest = NotificationShadeWindowViewController(
-            lockscreenShadeTransitionController,
-            FalsingCollectorFake(),
-            sysuiStatusBarStateController,
-            dockManager,
-            notificationShadeDepthController,
-            view,
-            notificationPanelViewController,
-            ShadeExpansionStateManager(),
-            stackScrollLayoutController,
-            statusBarKeyguardViewManager,
-            statusBarWindowStateController,
-            lockIconViewController,
-            centralSurfaces,
-            notificationShadeWindowController,
-            keyguardUnlockAnimationController,
-            notificationInsetsController,
-            ambientState,
-            pulsingGestureListener,
-            featureFlags,
-            keyguardBouncerViewModel,
-            keyguardBouncerComponentFactory
-        )
+        whenever(view.findViewById<ViewGroup>(R.id.keyguard_bouncer_container))
+            .thenReturn(mock(ViewGroup::class.java))
+        whenever(keyguardBouncerComponentFactory.create(any(ViewGroup::class.java)))
+            .thenReturn(keyguardBouncerComponent)
+        whenever(keyguardBouncerComponent.securityContainerController)
+            .thenReturn(keyguardSecurityContainerController)
+        whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition)
+            .thenReturn(emptyFlow<TransitionStep>())
+        underTest =
+            NotificationShadeWindowViewController(
+                lockscreenShadeTransitionController,
+                FalsingCollectorFake(),
+                sysuiStatusBarStateController,
+                dockManager,
+                notificationShadeDepthController,
+                view,
+                notificationPanelViewController,
+                ShadeExpansionStateManager(),
+                stackScrollLayoutController,
+                statusBarKeyguardViewManager,
+                statusBarWindowStateController,
+                lockIconViewController,
+                centralSurfaces,
+                notificationShadeWindowController,
+                keyguardUnlockAnimationController,
+                notificationInsetsController,
+                ambientState,
+                pulsingGestureListener,
+                keyguardBouncerViewModel,
+                keyguardBouncerComponentFactory,
+                alternateBouncerInteractor,
+                keyguardTransitionInteractor,
+                primaryBouncerToGoneTransitionViewModel,
+            )
         underTest.setupExpandedStatusBar()
 
-        interactionEventHandlerCaptor =
-            ArgumentCaptor.forClass(InteractionEventHandler::class.java)
+        interactionEventHandlerCaptor = ArgumentCaptor.forClass(InteractionEventHandler::class.java)
         verify(view).setInteractionEventHandler(interactionEventHandlerCaptor.capture())
-            interactionEventHandler = interactionEventHandlerCaptor.value
+        interactionEventHandler = interactionEventHandlerCaptor.value
     }
 
     // Note: So far, these tests only cover interactions with the status bar view controller. More
@@ -169,14 +170,11 @@
     @Test
     fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() {
         underTest.setStatusBarViewController(phoneStatusBarViewController)
-        val downEvBelow = MotionEvent.obtain(
-            0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0
-        )
+        val downEvBelow =
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0)
         interactionEventHandler.handleDispatchTouchEvent(downEvBelow)
 
-        val nextEvent = MotionEvent.obtain(
-            0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0
-        )
+        val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0)
         whenever(phoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true)
 
         val returnVal = interactionEventHandler.handleDispatchTouchEvent(nextEvent)
@@ -268,6 +266,7 @@
 
     @Test
     fun testGetBouncerContainer() {
+        Mockito.clearInvocations(view)
         underTest.bouncerContainer
         verify(view).findViewById<ViewGroup>(R.id.keyguard_bouncer_container)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 4bf00c4..faa6221 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -25,22 +25,28 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import android.os.SystemClock;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
+import android.view.ViewGroup;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.KeyguardSecurityContainerController;
 import com.android.keyguard.LockIconViewController;
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.dock.DockManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationInsetsController;
@@ -89,10 +95,14 @@
     @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     @Mock private AmbientState mAmbientState;
     @Mock private PulsingGestureListener mPulsingGestureListener;
-    @Mock private FeatureFlags mFeatureFlags;
     @Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel;
     @Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
+    @Mock private KeyguardBouncerComponent mKeyguardBouncerComponent;
+    @Mock private KeyguardSecurityContainerController mKeyguardSecurityContainerController;
     @Mock private NotificationInsetsController mNotificationInsetsController;
+    @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
 
     @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler>
             mInteractionEventHandlerCaptor;
@@ -106,11 +116,20 @@
         when(mView.findViewById(R.id.notification_stack_scroller))
                 .thenReturn(mNotificationStackScrollLayout);
 
+        when(mView.findViewById(R.id.keyguard_bouncer_container)).thenReturn(mock(ViewGroup.class));
+        when(mKeyguardBouncerComponentFactory.create(any(ViewGroup.class))).thenReturn(
+                mKeyguardBouncerComponent);
+        when(mKeyguardBouncerComponent.getSecurityContainerController()).thenReturn(
+                mKeyguardSecurityContainerController);
+
         when(mStatusBarStateController.isDozing()).thenReturn(false);
         mDependency.injectTestDependency(ShadeController.class, mShadeController);
 
         when(mDockManager.isDocked()).thenReturn(false);
 
+        when(mKeyguardTransitionInteractor.getLockscreenToDreamingTransition())
+                .thenReturn(emptyFlow());
+
         mController = new NotificationShadeWindowViewController(
                 mLockscreenShadeTransitionController,
                 new FalsingCollectorFake(),
@@ -130,9 +149,11 @@
                 mNotificationInsetsController,
                 mAmbientState,
                 mPulsingGestureListener,
-                mFeatureFlags,
                 mKeyguardBouncerViewModel,
-                mKeyguardBouncerComponentFactory
+                mKeyguardBouncerComponentFactory,
+                mAlternateBouncerInteractor,
+                mKeyguardTransitionInteractor,
+                mPrimaryBouncerToGoneTransitionViewModel
         );
         mController.setupExpandedStatusBar();
         mController.setDragDownHelper(mDragDownHelper);
@@ -155,7 +176,7 @@
 
         // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we should intercept touch
@@ -168,7 +189,7 @@
 
         // WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(false);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we shouldn't intercept touch
@@ -181,7 +202,7 @@
 
         // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we should handle the touch
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 3e769e9..76aa08a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.tuner.TunerService.Tunable
@@ -69,6 +70,8 @@
     private lateinit var statusBarStateController: StatusBarStateController
     @Mock
     private lateinit var shadeLogger: ShadeLogger
+    @Mock
+    private lateinit var userTracker: UserTracker
 
     private lateinit var tunableCaptor: ArgumentCaptor<Tunable>
     private lateinit var underTest: PulsingGestureListener
@@ -85,6 +88,7 @@
                 ambientDisplayConfiguration,
                 statusBarStateController,
                 shadeLogger,
+                userTracker,
                 tunerService,
                 dumpManager
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt
new file mode 100644
index 0000000..b547318
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.shade
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.DisplayCutout
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class QsBatteryModeControllerTest : SysuiTestCase() {
+
+    private companion object {
+        val CENTER_TOP_CUTOUT: DisplayCutout =
+            mock<DisplayCutout>().also {
+                whenever(it.boundingRectTop).thenReturn(Rect(10, 0, 20, 10))
+            }
+
+        const val MOTION_LAYOUT_MAX_FRAME = 100
+        const val QQS_START_FRAME = 14
+        const val QS_END_FRAME = 58
+    }
+
+    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()!!
+
+    @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
+    @Mock private lateinit var mockedContext: Context
+    @Mock private lateinit var mockedResources: Resources
+
+    private lateinit var controller: QsBatteryModeController // under test
+
+    @Before
+    fun setup() {
+        whenever(mockedContext.resources).thenReturn(mockedResources)
+        whenever(mockedResources.getInteger(R.integer.fade_in_start_frame)).thenReturn(QS_END_FRAME)
+        whenever(mockedResources.getInteger(R.integer.fade_out_complete_frame))
+            .thenReturn(QQS_START_FRAME)
+
+        controller = QsBatteryModeController(mockedContext, insetsProvider)
+    }
+
+    @Test
+    fun `returns MODE_ON for qqs with center cutout`() {
+        assertThat(
+                controller.getBatteryMode(CENTER_TOP_CUTOUT, QQS_START_FRAME.prevFrameToFraction())
+            )
+            .isEqualTo(BatteryMeterView.MODE_ON)
+    }
+
+    @Test
+    fun `returns MODE_ESTIMATE for qs with center cutout`() {
+        assertThat(controller.getBatteryMode(CENTER_TOP_CUTOUT, QS_END_FRAME.nextFrameToFraction()))
+            .isEqualTo(BatteryMeterView.MODE_ESTIMATE)
+    }
+
+    @Test
+    fun `returns MODE_ON for qqs with corner cutout`() {
+        whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(true)
+
+        assertThat(
+                controller.getBatteryMode(CENTER_TOP_CUTOUT, QQS_START_FRAME.prevFrameToFraction())
+            )
+            .isEqualTo(BatteryMeterView.MODE_ESTIMATE)
+    }
+
+    @Test
+    fun `returns MODE_ESTIMATE for qs with corner cutout`() {
+        whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(true)
+
+        assertThat(controller.getBatteryMode(CENTER_TOP_CUTOUT, QS_END_FRAME.nextFrameToFraction()))
+            .isEqualTo(BatteryMeterView.MODE_ESTIMATE)
+    }
+
+    @Test
+    fun `returns null in-between`() {
+        assertThat(
+                controller.getBatteryMode(CENTER_TOP_CUTOUT, QQS_START_FRAME.nextFrameToFraction())
+            )
+            .isNull()
+        assertThat(controller.getBatteryMode(CENTER_TOP_CUTOUT, QS_END_FRAME.prevFrameToFraction()))
+            .isNull()
+    }
+
+    private fun Int.prevFrameToFraction(): Float = (this - 1) / MOTION_LAYOUT_MAX_FRAME.toFloat()
+    private fun Int.nextFrameToFraction(): Float = (this + 1) / MOTION_LAYOUT_MAX_FRAME.toFloat()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
new file mode 100644
index 0000000..e3a3678
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
@@ -0,0 +1,565 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade;
+
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.MotionEvent.BUTTON_SECONDARY;
+import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY;
+
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
+import com.android.keyguard.KeyguardStatusView;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.QSFragment;
+import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.shade.transition.ShadeTransitionController;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.QsFrameTranslateController;
+import com.android.systemui.statusbar.StatusBarStateControllerImpl;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.notification.stack.AmbientState;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
+import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
+import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import dagger.Lazy;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class QuickSettingsControllerTest extends SysuiTestCase {
+
+    private static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
+    private static final float QS_FRAME_START_X = 0f;
+    private static final int QS_FRAME_WIDTH = 1000;
+    private static final int QS_FRAME_TOP = 0;
+    private static final int QS_FRAME_BOTTOM = 1000;
+
+
+    private QuickSettingsController mQsController;
+
+    @Mock private Resources mResources;
+    @Mock private KeyguardBottomAreaView mQsFrame;
+    @Mock private KeyguardStatusBarView mKeyguardStatusBar;
+    @Mock private QS mQs;
+    @Mock private QSFragment mQSFragment;
+
+    @Mock private Lazy<NotificationPanelViewController> mPanelViewControllerLazy;
+    @Mock private NotificationPanelViewController mNotificationPanelViewController;
+    @Mock private NotificationPanelView mPanelView;
+    @Mock private ViewGroup mQsHeader;
+    @Mock private ViewParent mPanelViewParent;
+    @Mock private QsFrameTranslateController mQsFrameTranslateController;
+    @Mock private ShadeTransitionController mShadeTransitionController;
+    @Mock private PulseExpansionHandler mPulseExpansionHandler;
+    @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
+    @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
+    @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    @Mock private NotificationShadeDepthController mNotificationShadeDepthController;
+    @Mock private ShadeHeaderController mShadeHeaderController;
+    @Mock private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    @Mock private KeyguardStateController mKeyguardStateController;
+    @Mock private KeyguardBypassController mKeyguardBypassController;
+    @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock private ScrimController mScrimController;
+    @Mock private MediaDataManager mMediaDataManager;
+    @Mock private MediaHierarchyManager mMediaHierarchyManager;
+    @Mock private AmbientState mAmbientState;
+    @Mock private RecordingController mRecordingController;
+    @Mock private FalsingManager mFalsingManager;
+    @Mock private FalsingCollector mFalsingCollector;
+    @Mock private AccessibilityManager mAccessibilityManager;
+    @Mock private LockscreenGestureLogger mLockscreenGestureLogger;
+    @Mock private MetricsLogger mMetricsLogger;
+    @Mock private FeatureFlags mFeatureFlags;
+    @Mock private InteractionJankMonitor mInteractionJankMonitor;
+    @Mock private ShadeLogger mShadeLogger;
+
+    @Mock private DumpManager mDumpManager;
+
+    @Mock private HeadsUpManagerPhone mHeadsUpManager;
+    @Mock private UiEventLogger mUiEventLogger;
+
+    private SysuiStatusBarStateController mStatusBarStateController;
+
+    private Handler mMainHandler;
+    private LockscreenShadeTransitionController.Callback mLockscreenShadeTransitionCallback;
+
+    private final ShadeExpansionStateManager mShadeExpansionStateManager =
+            new ShadeExpansionStateManager();
+
+    private FragmentHostManager.FragmentListener mFragmentListener;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController);
+        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
+                mInteractionJankMonitor, mShadeExpansionStateManager);
+
+        KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
+        keyguardStatusView.setId(R.id.keyguard_status_view);
+
+        when(mPanelView.getResources()).thenReturn(mResources);
+        when(mPanelView.getContext()).thenReturn(getContext());
+        when(mPanelView.findViewById(R.id.keyguard_header)).thenReturn(mKeyguardStatusBar);
+        when(mNotificationStackScrollLayoutController.getHeight()).thenReturn(1000);
+        when(mPanelView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame);
+        when(mQsFrame.getX()).thenReturn(QS_FRAME_START_X);
+        when(mQsFrame.getWidth()).thenReturn(QS_FRAME_WIDTH);
+        when(mQsHeader.getTop()).thenReturn(QS_FRAME_TOP);
+        when(mQsHeader.getBottom()).thenReturn(QS_FRAME_BOTTOM);
+        when(mPanelView.getY()).thenReturn((float) QS_FRAME_TOP);
+        when(mPanelView.getHeight()).thenReturn(QS_FRAME_BOTTOM);
+        when(mPanelView.findViewById(R.id.keyguard_status_view))
+                .thenReturn(mock(KeyguardStatusView.class));
+        when(mQs.getView()).thenReturn(mPanelView);
+        when(mQSFragment.getView()).thenReturn(mPanelView);
+
+        when(mNotificationRemoteInputManager.isRemoteInputActive())
+                .thenReturn(false);
+        when(mInteractionJankMonitor.begin(any(), anyInt()))
+                .thenReturn(true);
+        when(mInteractionJankMonitor.end(anyInt()))
+                .thenReturn(true);
+
+        when(mPanelView.getParent()).thenReturn(mPanelViewParent);
+        when(mQs.getHeader()).thenReturn(mQsHeader);
+
+        doAnswer(invocation -> {
+            mLockscreenShadeTransitionCallback = invocation.getArgument(0);
+            return null;
+        }).when(mLockscreenShadeTransitionController).addCallback(any());
+
+
+        mMainHandler = new Handler(Looper.getMainLooper());
+
+        mQsController = new QuickSettingsController(
+                mPanelViewControllerLazy,
+                mPanelView,
+                mQsFrameTranslateController,
+                mShadeTransitionController,
+                mPulseExpansionHandler,
+                mNotificationRemoteInputManager,
+                mShadeExpansionStateManager,
+                mStatusBarKeyguardViewManager,
+                mNotificationStackScrollLayoutController,
+                mLockscreenShadeTransitionController,
+                mNotificationShadeDepthController,
+                mShadeHeaderController,
+                mStatusBarTouchableRegionManager,
+                mKeyguardStateController,
+                mKeyguardBypassController,
+                mKeyguardUpdateMonitor,
+                mScrimController,
+                mMediaDataManager,
+                mMediaHierarchyManager,
+                mAmbientState,
+                mRecordingController,
+                mFalsingManager,
+                mFalsingCollector,
+                mAccessibilityManager,
+                mLockscreenGestureLogger,
+                mMetricsLogger,
+                mFeatureFlags,
+                mInteractionJankMonitor,
+                mShadeLogger
+        );
+
+        mFragmentListener = mQsController.getQsFragmentListener();
+    }
+
+    @After
+    public void tearDown() {
+        mMainHandler.removeCallbacksAndMessages(null);
+    }
+
+    @Test
+    public void testCloseQsSideEffects() {
+        enableSplitShade(true);
+        mQsController.setExpandImmediate(true);
+        mQsController.setExpanded(true);
+        mQsController.closeQs();
+
+        assertThat(mQsController.getExpanded()).isEqualTo(false);
+        assertThat(mQsController.isExpandImmediate()).isEqualTo(false);
+    }
+
+    @Test
+    public void testLargeScreenHeaderMadeActiveForLargeScreen() {
+        mStatusBarStateController.setState(SHADE);
+        when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(true);
+        mQsController.updateResources();
+        verify(mShadeHeaderController).setLargeScreenActive(true);
+
+        when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(false);
+        mQsController.updateResources();
+        verify(mShadeHeaderController).setLargeScreenActive(false);
+    }
+
+    @Test
+    public void testPanelStaysOpenWhenClosingQs() {
+        mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
+                /* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0);
+        mQsController.setShadeExpandedHeight(1);
+
+        float shadeExpandedHeight = mQsController.getShadeExpandedHeight();
+        mQsController.animateCloseQs(false);
+
+        assertThat(mQsController.getShadeExpandedHeight()).isEqualTo(shadeExpandedHeight);
+    }
+
+    @Test
+    public void interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking() {
+        mQsController.setQs(mQs);
+
+        mQsController.setShadeExpandedHeight(1f);
+        mQsController.onIntercept(
+                createMotionEvent(0, 0, ACTION_DOWN));
+        mQsController.onIntercept(
+                createMotionEvent(0, 500, ACTION_MOVE));
+
+        assertThat(mQsController.isTracking()).isTrue();
+    }
+
+    @Test
+    public void interceptTouchEvent_withinQs_shadeExpanded_inSplitShade_doesNotStartQsTracking() {
+        enableSplitShade(true);
+        mQsController.setQs(mQs);
+
+        mQsController.setShadeExpandedHeight(1f);
+        mQsController.onIntercept(
+                createMotionEvent(0, 0, ACTION_DOWN));
+        mQsController.onIntercept(
+                createMotionEvent(0, 500, ACTION_MOVE));
+
+        assertThat(mQsController.isTracking()).isFalse();
+    }
+
+    @Test
+    public void interceptTouch_downBetweenFullyCollapsedAndExpanded() {
+        mQsController.setQs(mQs);
+        when(mQs.getDesiredHeight()).thenReturn(QS_FRAME_BOTTOM);
+        mQsController.onHeightChanged();
+        mQsController.setExpansionHeight(QS_FRAME_BOTTOM / 2f);
+
+        assertThat(mQsController.onIntercept(
+                createMotionEvent(0, QS_FRAME_BOTTOM / 2, ACTION_DOWN))).isTrue();
+    }
+
+    @Test
+    public void onTouch_moveActionSetsCorrectExpansionHeight() {
+        mQsController.setQs(mQs);
+        when(mQs.getDesiredHeight()).thenReturn(QS_FRAME_BOTTOM);
+        mQsController.onHeightChanged();
+        mQsController.setExpansionHeight(QS_FRAME_BOTTOM / 2f);
+        mQsController.handleTouch(
+                createMotionEvent(0, QS_FRAME_BOTTOM / 4, ACTION_DOWN), false, false);
+        assertThat(mQsController.isTracking()).isTrue();
+        mQsController.handleTouch(
+                createMotionEvent(0, QS_FRAME_BOTTOM / 4 + 1, ACTION_MOVE), false, false);
+
+        assertThat(mQsController.getExpansionHeight()).isEqualTo(QS_FRAME_BOTTOM / 2 + 1);
+    }
+
+    @Test
+    public void handleTouch_downActionInQsArea() {
+        mQsController.setQs(mQs);
+        mQsController.setBarState(SHADE);
+        mQsController.onPanelExpansionChanged(
+                new ShadeExpansionChangeEvent(
+                        0.5f,
+                        true,
+                        true,
+                        0
+                ));
+        MotionEvent event =
+                createMotionEvent(QS_FRAME_WIDTH / 2, QS_FRAME_BOTTOM / 2, ACTION_DOWN);
+        mQsController.handleTouch(event, false, false);
+
+        assertThat(mQsController.isTracking()).isTrue();
+        assertThat(mQsController.getInitialTouchY()).isEqualTo(QS_FRAME_BOTTOM / 2);
+    }
+
+    @Test
+    public void handleTouch_qsTouchedWhileCollapsingDisablesTracking() {
+        mQsController.handleTouch(
+                createMotionEvent(0, QS_FRAME_BOTTOM, ACTION_DOWN), false, false);
+        mQsController.setLastShadeFlingWasExpanding(false);
+        mQsController.handleTouch(
+                createMotionEvent(0, QS_FRAME_BOTTOM / 2, ACTION_MOVE), false, true);
+        MotionEvent secondTouch = createMotionEvent(0, QS_FRAME_TOP, ACTION_DOWN);
+        mQsController.handleTouch(secondTouch, false, true);
+        assertThat(mQsController.isTracking()).isFalse();
+    }
+
+    @Test
+    public void handleTouch_qsTouchedWhileExpanding() {
+        mQsController.setQs(mQs);
+        mQsController.handleTouch(
+                createMotionEvent(100, 100, ACTION_DOWN), false, false);
+        mQsController.handleTouch(
+                createMotionEvent(0, QS_FRAME_BOTTOM / 2, ACTION_MOVE), false, false);
+        mQsController.setLastShadeFlingWasExpanding(true);
+        mQsController.handleTouch(
+                createMotionEvent(0, QS_FRAME_TOP, ACTION_DOWN), false, false);
+        assertThat(mQsController.isTracking()).isTrue();
+    }
+
+    @Test
+    public void handleTouch_isConflictingExpansionGestureSet() {
+        assertThat(mQsController.isConflictingExpansionGesture()).isFalse();
+        mShadeExpansionStateManager.onPanelExpansionChanged(1f, true, false, 0f);
+        mQsController.handleTouch(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, ACTION_DOWN, 0f /* x */, 0f /* y */,
+                0 /* metaState */), false, false);
+        assertThat(mQsController.isConflictingExpansionGesture()).isTrue();
+    }
+
+    @Test
+    public void handleTouch_isConflictingExpansionGestureSet_cancel() {
+        mShadeExpansionStateManager.onPanelExpansionChanged(1f, true, false, 0f);
+        mQsController.handleTouch(createMotionEvent(0, 0, ACTION_DOWN), false, false);
+        assertThat(mQsController.isConflictingExpansionGesture()).isTrue();
+        mQsController.handleTouch(createMotionEvent(0, 0, ACTION_UP), true, true);
+        assertThat(mQsController.isConflictingExpansionGesture()).isFalse();
+    }
+
+    @Test
+    public void handleTouch_twoFingerExpandPossibleConditions() {
+        assertThat(mQsController.isTwoFingerExpandPossible()).isFalse();
+        mQsController.handleTouch(createMotionEvent(0, 0, ACTION_DOWN), true, false);
+        assertThat(mQsController.isTwoFingerExpandPossible()).isTrue();
+    }
+
+    @Test
+    public void handleTouch_twoFingerDrag() {
+        mQsController.setQs(mQs);
+        mQsController.setStatusBarMinHeight(1);
+        mQsController.setTwoFingerExpandPossible(true);
+        mQsController.handleTouch(
+                createMultitouchMotionEvent(ACTION_POINTER_DOWN), false, false);
+        assertThat(mQsController.isExpandImmediate()).isTrue();
+        verify(mQs).setListening(true);
+    }
+
+    @Test
+    public void onQsFragmentAttached_fullWidth_setsFullWidthTrueOnQS() {
+        setIsFullWidth(true);
+        mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+        verify(mQSFragment).setIsNotificationPanelFullWidth(true);
+    }
+
+    @Test
+    public void onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS() {
+        setIsFullWidth(false);
+        mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+        verify(mQSFragment).setIsNotificationPanelFullWidth(false);
+    }
+
+    @Test
+    public void setQsExpansion_lockscreenShadeTransitionInProgress_usesLockscreenSquishiness() {
+        float squishinessFraction = 0.456f;
+        mQsController.setQs(mQs);
+        when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
+                .thenReturn(squishinessFraction);
+        when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
+                .thenReturn(0.987f);
+        // Call setTransitionToFullShadeAmount to get into the full shade transition in progress
+        // state.
+        mLockscreenShadeTransitionCallback.setTransitionToFullShadeAmount(234, false, 0);
+
+        mQsController.setExpansionHeight(123);
+
+        // First for setTransitionToFullShadeAmount and then setQsExpansion
+        verify(mQs, times(2)).setQsExpansion(anyFloat(), anyFloat(), anyFloat(),
+                eq(squishinessFraction)
+        );
+    }
+
+    @Test
+    public void setQsExpansion_lockscreenShadeTransitionNotInProgress_usesStandardSquishiness() {
+        float lsSquishinessFraction = 0.456f;
+        float nsslSquishinessFraction = 0.987f;
+        mQsController.setQs(mQs);
+        when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
+                .thenReturn(lsSquishinessFraction);
+        when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
+                .thenReturn(nsslSquishinessFraction);
+
+        mQsController.setExpansionHeight(123);
+
+        verify(mQs).setQsExpansion(anyFloat(), anyFloat(), anyFloat(), eq(nsslSquishinessFraction)
+        );
+    }
+
+    @Test
+    public void updateExpansion_expandImmediateOrAlreadyExpanded_usesFullSquishiness() {
+        mQsController.setQs(mQs);
+        when(mQs.getDesiredHeight()).thenReturn(100);
+        mQsController.onHeightChanged();
+
+        mQsController.setExpandImmediate(true);
+        mQsController.setExpanded(false);
+        mQsController.updateExpansion();
+        mQsController.setExpandImmediate(false);
+        mQsController.setExpanded(true);
+        mQsController.updateExpansion();
+        verify(mQs, times(2)).setQsExpansion(0, 0, 0, 1);
+    }
+
+    @Test
+    public void shadeExpanded_onKeyguard() {
+        mStatusBarStateController.setState(KEYGUARD);
+        // set maxQsExpansion in NPVC
+        int maxQsExpansion = 123;
+        mQsController.setQs(mQs);
+        when(mQs.getDesiredHeight()).thenReturn(maxQsExpansion);
+
+        int oldMaxHeight = mQsController.updateHeightsOnShadeLayoutChange();
+        mQsController.handleShadeLayoutChanged(oldMaxHeight);
+
+        mQsController.setExpansionHeight(maxQsExpansion);
+        assertThat(mQsController.computeExpansionFraction()).isEqualTo(1f);
+    }
+
+    @Test
+    public void handleTouch_splitShadeAndtouchXOutsideQs() {
+        enableSplitShade(true);
+
+        assertThat(mQsController.handleTouch(createMotionEvent(
+                        QS_FRAME_WIDTH + 1, QS_FRAME_BOTTOM - 1, ACTION_DOWN),
+                false, false)).isFalse();
+    }
+
+    @Test
+    public void isOpenQsEvent_twoFingerDrag() {
+        assertThat(mQsController.isOpenQsEvent(
+                createMultitouchMotionEvent(ACTION_POINTER_DOWN))).isTrue();
+    }
+
+    @Test
+    public void isOpenQsEvent_stylusButtonClickDrag() {
+        MotionEvent event = createMotionEvent(0, 0, ACTION_DOWN);
+        event.setButtonState(BUTTON_STYLUS_PRIMARY);
+
+        assertThat(mQsController.isOpenQsEvent(event)).isTrue();
+    }
+
+    @Test
+    public void isOpenQsEvent_mouseButtonClickDrag() {
+        MotionEvent event = createMotionEvent(0, 0, ACTION_DOWN);
+        event.setButtonState(BUTTON_SECONDARY);
+
+        assertThat(mQsController.isOpenQsEvent(event)).isTrue();
+    }
+
+    private static MotionEvent createMotionEvent(int x, int y, int action) {
+        return MotionEvent.obtain(0, 0, action, x, y, 0);
+    }
+
+    // Creates an empty multitouch event for now
+    private static MotionEvent createMultitouchMotionEvent(int action) {
+        return MotionEvent.obtain(0, 0, action, 2,
+                new MotionEvent.PointerProperties[] {
+                        new MotionEvent.PointerProperties(),
+                        new MotionEvent.PointerProperties()
+                },
+                new MotionEvent.PointerCoords[] {
+                        new MotionEvent.PointerCoords(),
+                        new MotionEvent.PointerCoords()
+                }, 0, 0, 0, 0, 0, 0, 0, 0);
+    }
+
+    private void enableSplitShade(boolean enabled) {
+        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(enabled);
+        mQsController.updateResources();
+    }
+
+    private void setIsFullWidth(boolean fullWidth) {
+        mQsController.setNotificationPanelFullWidth(fullWidth);
+        triggerLayoutChange();
+    }
+
+    private void triggerLayoutChange() {
+        int oldMaxHeight = mQsController.updateHeightsOnShadeLayoutChange();
+        mQsController.handleShadeLayoutChanged(oldMaxHeight);
+    }
+
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
new file mode 100644
index 0000000..d530829
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -0,0 +1,933 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.shade
+
+import android.animation.Animator
+import android.app.StatusBarManager
+import android.content.Context
+import android.content.res.Resources
+import android.content.res.XmlResourceParser
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.DisplayCutout
+import android.view.View
+import android.view.ViewPropertyAnimator
+import android.view.WindowInsets
+import android.widget.TextView
+import androidx.constraintlayout.motion.widget.MotionLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ShadeInterpolation
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.ChipVisibilityListener
+import com.android.systemui.qs.HeaderPrivacyIconsController
+import com.android.systemui.qs.carrier.QSCarrierGroup
+import com.android.systemui.qs.carrier.QSCarrierGroupController
+import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
+import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
+import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.policy.Clock
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.statusbar.policy.VariableDateView
+import com.android.systemui.statusbar.policy.VariableDateViewController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+private val EMPTY_CHANGES = ConstraintsChanges()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ShadeHeaderControllerTest : SysuiTestCase() {
+
+    @Mock(answer = Answers.RETURNS_MOCKS) private lateinit var view: MotionLayout
+    @Mock private lateinit var statusIcons: StatusIconContainer
+    @Mock private lateinit var statusBarIconController: StatusBarIconController
+    @Mock private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+    @Mock private lateinit var iconManager: StatusBarIconController.TintedIconManager
+    @Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController
+    @Mock private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
+    @Mock private lateinit var clock: Clock
+    @Mock private lateinit var date: VariableDateView
+    @Mock private lateinit var carrierGroup: QSCarrierGroup
+    @Mock private lateinit var batteryMeterView: BatteryMeterView
+    @Mock private lateinit var batteryMeterViewController: BatteryMeterViewController
+    @Mock private lateinit var privacyIconsController: HeaderPrivacyIconsController
+    @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
+    @Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
+    @Mock private lateinit var variableDateViewController: VariableDateViewController
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock
+    private lateinit var combinedShadeHeadersConstraintManager:
+        CombinedShadeHeadersConstraintManager
+
+    @Mock private lateinit var mockedContext: Context
+    private lateinit var viewContext: Context
+
+    @Mock private lateinit var qqsConstraints: ConstraintSet
+    @Mock private lateinit var qsConstraints: ConstraintSet
+    @Mock private lateinit var largeScreenConstraints: ConstraintSet
+
+    @Mock private lateinit var demoModeController: DemoModeController
+    @Mock private lateinit var qsBatteryModeController: QsBatteryModeController
+
+    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
+    var viewVisibility = View.GONE
+    var viewAlpha = 1f
+
+    private lateinit var shadeHeaderController: ShadeHeaderController
+    private lateinit var carrierIconSlots: List<String>
+    private val configurationController = FakeConfigurationController()
+    @Captor private lateinit var demoModeControllerCapture: ArgumentCaptor<DemoMode>
+
+    @Before
+    fun setup() {
+        whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
+        whenever(clock.context).thenReturn(mockedContext)
+
+        whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
+        whenever(date.context).thenReturn(mockedContext)
+
+        whenever<QSCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup)
+
+        whenever<BatteryMeterView>(view.findViewById(R.id.batteryRemainingIcon))
+            .thenReturn(batteryMeterView)
+
+        whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
+
+        viewContext = Mockito.spy(context)
+        whenever(view.context).thenReturn(viewContext)
+        whenever(view.resources).thenReturn(context.resources)
+        whenever(statusIcons.context).thenReturn(context)
+        whenever(qsCarrierGroupControllerBuilder.setQSCarrierGroup(any()))
+            .thenReturn(qsCarrierGroupControllerBuilder)
+        whenever(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
+        whenever(view.setVisibility(anyInt())).then {
+            viewVisibility = it.arguments[0] as Int
+            null
+        }
+        whenever(view.visibility).thenAnswer { _ -> viewVisibility }
+
+        whenever(view.setAlpha(anyFloat())).then {
+            viewAlpha = it.arguments[0] as Float
+            null
+        }
+        whenever(view.alpha).thenAnswer { _ -> viewAlpha }
+
+        whenever(variableDateViewControllerFactory.create(any()))
+            .thenReturn(variableDateViewController)
+        whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
+
+        setUpDefaultInsets()
+        setUpMotionLayout(view)
+
+        shadeHeaderController =
+            ShadeHeaderController(
+                view,
+                statusBarIconController,
+                iconManagerFactory,
+                privacyIconsController,
+                insetsProvider,
+                configurationController,
+                variableDateViewControllerFactory,
+                batteryMeterViewController,
+                dumpManager,
+                qsCarrierGroupControllerBuilder,
+                combinedShadeHeadersConstraintManager,
+                demoModeController,
+                qsBatteryModeController,
+            )
+        whenever(view.isAttachedToWindow).thenReturn(true)
+        shadeHeaderController.init()
+        carrierIconSlots =
+            listOf(context.getString(com.android.internal.R.string.status_bar_mobile))
+    }
+
+    @Test
+    fun updateListeners_registersWhenVisible() {
+        makeShadeVisible()
+        verify(qsCarrierGroupController).setListening(true)
+        verify(statusBarIconController).addIconGroup(any())
+    }
+
+    @Test
+    fun statusIconsAddedWhenAttached() {
+        verify(statusBarIconController).addIconGroup(any())
+    }
+
+    @Test
+    fun statusIconsRemovedWhenDettached() {
+        shadeHeaderController.simulateViewDetached()
+        verify(statusBarIconController).removeIconGroup(any())
+    }
+
+    @Test
+    fun shadeExpandedFraction_updatesAlpha() {
+        makeShadeVisible()
+        shadeHeaderController.shadeExpandedFraction = 0.5f
+        verify(view).setAlpha(ShadeInterpolation.getContentAlpha(0.5f))
+    }
+
+    @Test
+    fun singleCarrier_enablesCarrierIconsInStatusIcons() {
+        whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true)
+
+        makeShadeVisible()
+
+        verify(statusIcons).removeIgnoredSlots(carrierIconSlots)
+    }
+
+    @Test
+    fun dualCarrier_disablesCarrierIconsInStatusIcons() {
+        whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(false)
+
+        makeShadeVisible()
+
+        verify(statusIcons).addIgnoredSlots(carrierIconSlots)
+    }
+
+    @Test
+    fun disableQS_notDisabled_visible() {
+        makeShadeVisible()
+        shadeHeaderController.disable(0, 0, false)
+
+        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun disableQS_disabled_gone() {
+        makeShadeVisible()
+        shadeHeaderController.disable(0, StatusBarManager.DISABLE2_QUICK_SETTINGS, false)
+
+        assertThat(viewVisibility).isEqualTo(View.GONE)
+    }
+
+    private fun makeShadeVisible() {
+        shadeHeaderController.largeScreenActive = true
+        shadeHeaderController.qsVisible = true
+    }
+
+    @Test
+    fun updateConfig_changesFontStyle() {
+        configurationController.notifyDensityOrFontScaleChanged()
+
+        verify(clock).setTextAppearance(R.style.TextAppearance_QS_Status)
+        verify(date).setTextAppearance(R.style.TextAppearance_QS_Status)
+        verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers)
+    }
+
+    @Test
+    fun animateOutOnStartCustomizing() {
+        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+        val duration = 1000L
+        whenever(view.animate()).thenReturn(animator)
+
+        shadeHeaderController.startCustomizingAnimation(show = true, duration)
+
+        verify(animator).setDuration(duration)
+        verify(animator).alpha(0f)
+        verify(animator).setInterpolator(Interpolators.ALPHA_OUT)
+        verify(animator).start()
+    }
+
+    @Test
+    fun animateInOnEndCustomizing() {
+        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+        val duration = 1000L
+        whenever(view.animate()).thenReturn(animator)
+
+        shadeHeaderController.startCustomizingAnimation(show = false, duration)
+
+        verify(animator).setDuration(duration)
+        verify(animator).alpha(1f)
+        verify(animator).setInterpolator(Interpolators.ALPHA_IN)
+        verify(animator).start()
+    }
+
+    @Test
+    fun customizerAnimatorChangesViewVisibility() {
+        makeShadeVisible()
+
+        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+        val duration = 1000L
+        whenever(view.animate()).thenReturn(animator)
+        val listenerCaptor = argumentCaptor<Animator.AnimatorListener>()
+
+        shadeHeaderController.startCustomizingAnimation(show = true, duration)
+        verify(animator).setListener(capture(listenerCaptor))
+        // Start and end the animation
+        listenerCaptor.value.onAnimationStart(mock())
+        listenerCaptor.value.onAnimationEnd(mock())
+        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
+
+        reset(animator)
+        shadeHeaderController.startCustomizingAnimation(show = false, duration)
+        verify(animator).setListener(capture(listenerCaptor))
+        // Start and end the animation
+        listenerCaptor.value.onAnimationStart(mock())
+        listenerCaptor.value.onAnimationEnd(mock())
+        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun animatorListenersClearedAtEnd() {
+        val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+        whenever(view.animate()).thenReturn(animator)
+
+        shadeHeaderController.startCustomizingAnimation(show = true, 0L)
+        val listenerCaptor = argumentCaptor<Animator.AnimatorListener>()
+        verify(animator).setListener(capture(listenerCaptor))
+
+        listenerCaptor.value.onAnimationEnd(mock())
+        verify(animator).setListener(null)
+    }
+
+    @Test
+    fun demoMode_attachDemoMode() {
+        val cb = argumentCaptor<DemoMode>()
+        verify(demoModeController).addCallback(capture(cb))
+        cb.value.onDemoModeStarted()
+        verify(clock).onDemoModeStarted()
+    }
+
+    @Test
+    fun demoMode_detachDemoMode() {
+        shadeHeaderController.simulateViewDetached()
+        val cb = argumentCaptor<DemoMode>()
+        verify(demoModeController).removeCallback(capture(cb))
+        cb.value.onDemoModeFinished()
+        verify(clock).onDemoModeFinished()
+    }
+
+    @Test
+    fun testControllersCreatedAndInitialized() {
+        verify(variableDateViewController).init()
+
+        verify(batteryMeterViewController).init()
+        verify(batteryMeterViewController).ignoreTunerUpdates()
+
+        val inOrder = Mockito.inOrder(qsCarrierGroupControllerBuilder)
+        inOrder.verify(qsCarrierGroupControllerBuilder).setQSCarrierGroup(carrierGroup)
+        inOrder.verify(qsCarrierGroupControllerBuilder).build()
+    }
+
+    @Test
+    fun `battery mode controller called when qsExpandedFraction changes`() {
+        whenever(qsBatteryModeController.getBatteryMode(Mockito.same(null), eq(0f)))
+            .thenReturn(BatteryMeterView.MODE_ON)
+        whenever(qsBatteryModeController.getBatteryMode(Mockito.same(null), eq(1f)))
+            .thenReturn(BatteryMeterView.MODE_ESTIMATE)
+        shadeHeaderController.qsVisible = true
+
+        val times = 10
+        repeat(times) { shadeHeaderController.qsExpandedFraction = it / (times - 1).toFloat() }
+
+        verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ON)
+        verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+    }
+
+    @Test
+    fun testClockPivotLtr() {
+        val width = 200
+        whenever(clock.width).thenReturn(width)
+        whenever(clock.isLayoutRtl).thenReturn(false)
+
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+
+        captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
+        verify(clock).pivotX = 0f
+    }
+
+    @Test
+    fun testClockPivotRtl() {
+        val width = 200
+        whenever(clock.width).thenReturn(width)
+        whenever(clock.isLayoutRtl).thenReturn(true)
+
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+
+        captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
+        verify(clock).pivotX = width.toFloat()
+    }
+
+    @Test
+    fun testShadeExpanded_true() {
+        // When shade is expanded, view should be visible regardless of largeScreenActive
+        shadeHeaderController.largeScreenActive = false
+        shadeHeaderController.qsVisible = true
+        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
+
+        shadeHeaderController.largeScreenActive = true
+        assertThat(viewVisibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun testShadeExpanded_false() {
+        // When shade is not expanded, view should be invisible regardless of largeScreenActive
+        shadeHeaderController.largeScreenActive = false
+        shadeHeaderController.qsVisible = false
+        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
+
+        shadeHeaderController.largeScreenActive = true
+        assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
+    }
+
+    @Test
+    fun testLargeScreenActive_false() {
+        shadeHeaderController.largeScreenActive = true // Make sure there's a change
+        Mockito.clearInvocations(view)
+
+        shadeHeaderController.largeScreenActive = false
+
+        verify(view).setTransition(ShadeHeaderController.HEADER_TRANSITION_ID)
+    }
+
+    @Test
+    fun testShadeExpandedFraction() {
+        // View needs to be visible for this to actually take effect
+        shadeHeaderController.qsVisible = true
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.shadeExpandedFraction = 0.3f
+        verify(view).alpha = ShadeInterpolation.getContentAlpha(0.3f)
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.shadeExpandedFraction = 1f
+        verify(view).alpha = ShadeInterpolation.getContentAlpha(1f)
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.shadeExpandedFraction = 0f
+        verify(view).alpha = ShadeInterpolation.getContentAlpha(0f)
+    }
+
+    @Test
+    fun testQsExpandedFraction_headerTransition() {
+        shadeHeaderController.qsVisible = true
+        shadeHeaderController.largeScreenActive = false
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.qsExpandedFraction = 0.3f
+        verify(view).progress = 0.3f
+    }
+
+    @Test
+    fun testQsExpandedFraction_largeScreen() {
+        shadeHeaderController.qsVisible = true
+        shadeHeaderController.largeScreenActive = true
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.qsExpandedFraction = 0.3f
+        verify(view, Mockito.never()).progress = anyFloat()
+    }
+
+    @Test
+    fun testScrollY_headerTransition() {
+        shadeHeaderController.largeScreenActive = false
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.qsScrollY = 20
+        verify(view).scrollY = 20
+    }
+
+    @Test
+    fun testScrollY_largeScreen() {
+        shadeHeaderController.largeScreenActive = true
+
+        Mockito.clearInvocations(view)
+        shadeHeaderController.qsScrollY = 20
+        verify(view, Mockito.never()).scrollY = anyInt()
+    }
+
+    @Test
+    fun testPrivacyChipVisibilityChanged_visible_changesCorrectConstraints() {
+        val chipVisibleChanges = createMockConstraintChanges()
+        val chipNotVisibleChanges = createMockConstraintChanges()
+
+        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
+            .thenReturn(chipVisibleChanges)
+        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
+            .thenReturn(chipNotVisibleChanges)
+
+        val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
+        verify(privacyIconsController).chipVisibilityListener = capture(captor)
+
+        captor.value.onChipVisibilityRefreshed(true)
+
+        verify(chipVisibleChanges.qqsConstraintsChanges)!!.invoke(qqsConstraints)
+        verify(chipVisibleChanges.qsConstraintsChanges)!!.invoke(qsConstraints)
+        verify(chipVisibleChanges.largeScreenConstraintsChanges)!!.invoke(largeScreenConstraints)
+
+        verify(chipNotVisibleChanges.qqsConstraintsChanges, Mockito.never())!!.invoke(any())
+        verify(chipNotVisibleChanges.qsConstraintsChanges, Mockito.never())!!.invoke(any())
+        verify(chipNotVisibleChanges.largeScreenConstraintsChanges, Mockito.never())!!.invoke(any())
+    }
+
+    @Test
+    fun testPrivacyChipVisibilityChanged_notVisible_changesCorrectConstraints() {
+        val chipVisibleChanges = createMockConstraintChanges()
+        val chipNotVisibleChanges = createMockConstraintChanges()
+
+        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
+            .thenReturn(chipVisibleChanges)
+        whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
+            .thenReturn(chipNotVisibleChanges)
+
+        val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
+        verify(privacyIconsController).chipVisibilityListener = capture(captor)
+
+        captor.value.onChipVisibilityRefreshed(false)
+
+        verify(chipVisibleChanges.qqsConstraintsChanges, Mockito.never())!!.invoke(qqsConstraints)
+        verify(chipVisibleChanges.qsConstraintsChanges, Mockito.never())!!.invoke(qsConstraints)
+        verify(chipVisibleChanges.largeScreenConstraintsChanges, Mockito.never())!!.invoke(
+            largeScreenConstraints
+        )
+
+        verify(chipNotVisibleChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(chipNotVisibleChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(chipNotVisibleChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testInsetsGuides_ltr() {
+        whenever(view.isLayoutRtl).thenReturn(false)
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        val (insetLeft, insetRight) = 30 to 40
+        val (paddingStart, paddingEnd) = 10 to 20
+        whenever(view.paddingStart).thenReturn(paddingStart)
+        whenever(view.paddingEnd).thenReturn(paddingEnd)
+
+        mockInsetsProvider(insetLeft to insetRight, false)
+
+        whenever(
+                combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt()
+                )
+            )
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets())
+
+        verify(combinedShadeHeadersConstraintManager)
+            .edgesGuidelinesConstraints(insetLeft, paddingStart, insetRight, paddingEnd)
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testInsetsGuides_rtl() {
+        whenever(view.isLayoutRtl).thenReturn(true)
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        val (insetLeft, insetRight) = 30 to 40
+        val (paddingStart, paddingEnd) = 10 to 20
+        whenever(view.paddingStart).thenReturn(paddingStart)
+        whenever(view.paddingEnd).thenReturn(paddingEnd)
+
+        mockInsetsProvider(insetLeft to insetRight, false)
+
+        whenever(
+                combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt()
+                )
+            )
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets())
+
+        verify(combinedShadeHeadersConstraintManager)
+            .edgesGuidelinesConstraints(insetRight, paddingStart, insetLeft, paddingEnd)
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testNullCutout() {
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets(null))
+
+        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
+        verify(combinedShadeHeadersConstraintManager, Mockito.never())
+            .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testEmptyCutout() {
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets())
+
+        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
+        verify(combinedShadeHeadersConstraintManager, Mockito.never())
+            .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testCornerCutout_emptyRect() {
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        mockInsetsProvider(0 to 0, true)
+
+        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets())
+
+        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
+        verify(combinedShadeHeadersConstraintManager, Mockito.never())
+            .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testCornerCutout_nonEmptyRect() {
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        mockInsetsProvider(0 to 0, true)
+
+        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(1, 2, 3, 4)))
+
+        verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
+        verify(combinedShadeHeadersConstraintManager, Mockito.never())
+            .centerCutoutConstraints(Mockito.anyBoolean(), anyInt())
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testTopCutout_ltr() {
+        val width = 100
+        val paddingLeft = 10
+        val paddingRight = 20
+        val cutoutWidth = 30
+
+        whenever(view.isLayoutRtl).thenReturn(false)
+        whenever(view.width).thenReturn(width)
+        whenever(view.paddingLeft).thenReturn(paddingLeft)
+        whenever(view.paddingRight).thenReturn(paddingRight)
+
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        mockInsetsProvider(0 to 0, false)
+
+        whenever(
+                combinedShadeHeadersConstraintManager.centerCutoutConstraints(
+                    Mockito.anyBoolean(),
+                    anyInt()
+                )
+            )
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
+
+        verify(combinedShadeHeadersConstraintManager, Mockito.never()).emptyCutoutConstraints()
+        val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
+        verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(false, offset)
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun testTopCutout_rtl() {
+        val width = 100
+        val paddingLeft = 10
+        val paddingRight = 20
+        val cutoutWidth = 30
+
+        whenever(view.isLayoutRtl).thenReturn(true)
+        whenever(view.width).thenReturn(width)
+        whenever(view.paddingLeft).thenReturn(paddingLeft)
+        whenever(view.paddingRight).thenReturn(paddingRight)
+
+        val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+        verify(view).setOnApplyWindowInsetsListener(capture(captor))
+        val mockConstraintsChanges = createMockConstraintChanges()
+
+        mockInsetsProvider(0 to 0, false)
+
+        whenever(
+                combinedShadeHeadersConstraintManager.centerCutoutConstraints(
+                    Mockito.anyBoolean(),
+                    anyInt()
+                )
+            )
+            .thenReturn(mockConstraintsChanges)
+
+        captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
+
+        verify(combinedShadeHeadersConstraintManager, Mockito.never()).emptyCutoutConstraints()
+        val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
+        verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(true, offset)
+
+        verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
+        verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
+    }
+
+    @Test
+    fun alarmIconNotIgnored() {
+        verify(statusIcons, Mockito.never())
+            .addIgnoredSlot(context.getString(com.android.internal.R.string.status_bar_alarm_clock))
+    }
+
+    @Test
+    fun privacyChipParentVisibleFromStart() {
+        verify(privacyIconsController).onParentVisible()
+    }
+
+    @Test
+    fun privacyChipParentVisibleAlways() {
+        shadeHeaderController.largeScreenActive = true
+        shadeHeaderController.largeScreenActive = false
+        shadeHeaderController.largeScreenActive = true
+
+        verify(privacyIconsController, Mockito.never()).onParentInvisible()
+    }
+
+    @Test
+    fun clockPivotYInCenter() {
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+        var height = 100
+        val width = 50
+
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+
+        height = 150
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+    }
+
+    @Test
+    fun onDensityOrFontScaleChanged_reloadConstraints() {
+        // After density or font scale change, constraints need to be reloaded to reflect new
+        // dimensions.
+        Mockito.reset(qqsConstraints)
+        Mockito.reset(qsConstraints)
+        Mockito.reset(largeScreenConstraints)
+
+        configurationController.notifyDensityOrFontScaleChanged()
+
+        val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
+        verify(qqsConstraints).load(eq(viewContext), capture(captor))
+        assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
+        verify(qsConstraints).load(eq(viewContext), capture(captor))
+        assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header)
+        verify(largeScreenConstraints).load(eq(viewContext), capture(captor))
+        assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
+    }
+
+    @Test
+    fun `carrier left padding is set when clock layout changes`() {
+        val width = 200
+        whenever(clock.width).thenReturn(width)
+        whenever(clock.scaleX).thenReturn(2.57f) // 2.57 comes from qs_header.xml
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+        captor.value.onLayoutChange(clock, 0, 0, width, 0, 0, 0, 0, 0)
+
+        verify(carrierGroup).setPaddingRelative(514, 0, 0, 0)
+    }
+
+    private fun View.executeLayoutChange(
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int,
+        listener: View.OnLayoutChangeListener
+    ) {
+        val oldLeft = this.left
+        val oldTop = this.top
+        val oldRight = this.right
+        val oldBottom = this.bottom
+        whenever(this.left).thenReturn(left)
+        whenever(this.top).thenReturn(top)
+        whenever(this.right).thenReturn(right)
+        whenever(this.bottom).thenReturn(bottom)
+        whenever(this.height).thenReturn(bottom - top)
+        whenever(this.width).thenReturn(right - left)
+        listener.onLayoutChange(
+            this,
+            oldLeft,
+            oldTop,
+            oldRight,
+            oldBottom,
+            left,
+            top,
+            right,
+            bottom
+        )
+    }
+
+    private fun createWindowInsets(topCutout: Rect? = Rect()): WindowInsets {
+        val windowInsets: WindowInsets = mock()
+        val displayCutout: DisplayCutout = mock()
+        whenever(windowInsets.displayCutout)
+            .thenReturn(if (topCutout != null) displayCutout else null)
+        whenever(displayCutout.boundingRectTop).thenReturn(topCutout)
+
+        return windowInsets
+    }
+
+    private fun mockInsetsProvider(
+        insets: Pair<Int, Int> = 0 to 0,
+        cornerCutout: Boolean = false,
+    ) {
+        whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
+            .thenReturn(insets.toAndroidPair())
+        whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(cornerCutout)
+    }
+
+    private fun createMockConstraintChanges(): ConstraintsChanges {
+        return ConstraintsChanges(mock(), mock(), mock())
+    }
+
+    private fun XmlResourceParser.getResId(): Int {
+        return Resources.getAttributeSetSourceResId(this)
+    }
+
+    private fun setUpMotionLayout(motionLayout: MotionLayout) {
+        whenever(motionLayout.getConstraintSet(QQS_HEADER_CONSTRAINT)).thenReturn(qqsConstraints)
+        whenever(motionLayout.getConstraintSet(QS_HEADER_CONSTRAINT)).thenReturn(qsConstraints)
+        whenever(motionLayout.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT))
+            .thenReturn(largeScreenConstraints)
+    }
+
+    private fun setUpDefaultInsets() {
+        whenever(
+                combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints(
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt()
+                )
+            )
+            .thenReturn(EMPTY_CHANGES)
+        whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
+            .thenReturn(EMPTY_CHANGES)
+        whenever(
+                combinedShadeHeadersConstraintManager.centerCutoutConstraints(
+                    Mockito.anyBoolean(),
+                    anyInt()
+                )
+            )
+            .thenReturn(EMPTY_CHANGES)
+        whenever(
+                combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(
+                    Mockito.anyBoolean()
+                )
+            )
+            .thenReturn(EMPTY_CHANGES)
+        whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
+            .thenReturn(Pair(0, 0).toAndroidPair())
+        whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(false)
+        setupCurrentInsets(null)
+    }
+
+    private fun setupCurrentInsets(cutout: DisplayCutout?) {
+        val mockedDisplay =
+            mock<Display>().also { display -> whenever(display.cutout).thenReturn(cutout) }
+        whenever(viewContext.display).thenReturn(mockedDisplay)
+    }
+
+    private fun <T, U> Pair<T, U>.toAndroidPair(): android.util.Pair<T, U> {
+        return android.util.Pair(first, second)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
index 7cac854..d5a1f80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
@@ -2,37 +2,24 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.qs.QS
-import com.android.systemui.shade.NotificationPanelViewController
 import com.android.systemui.shade.STATE_OPENING
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionStateManager
-import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class ShadeTransitionControllerTest : SysuiTestCase() {
 
-    @Mock private lateinit var npvc: NotificationPanelViewController
-    @Mock private lateinit var nsslController: NotificationStackScrollLayoutController
-    @Mock private lateinit var qs: QS
-    @Mock private lateinit var noOpOverScroller: NoOpOverScroller
-    @Mock private lateinit var splitShadeOverScroller: SplitShadeOverScroller
     @Mock private lateinit var scrimShadeTransitionController: ScrimShadeTransitionController
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
@@ -52,119 +39,19 @@
                 shadeExpansionStateManager,
                 dumpManager,
                 context,
-                splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller },
-                noOpOverScroller,
                 scrimShadeTransitionController,
                 statusBarStateController,
             )
-
-        // Resetting as they are notified upon initialization.
-        reset(noOpOverScroller, splitShadeOverScroller)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_forwardsToSplitShadeOverScroller() {
-        initLateProperties()
-        enableSplitShade()
-
-        startPanelExpansion()
-
-        verify(splitShadeOverScroller).onPanelStateChanged(STATE_OPENING)
-        verify(splitShadeOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
-        verifyZeroInteractions(noOpOverScroller)
-    }
-
-    @Test
-    fun onPanelStateChanged_inSplitShade_propertiesNotInitialized_forwardsToNoOpOverScroller() {
-        enableSplitShade()
-
-        startPanelExpansion()
-
-        verify(noOpOverScroller).onPanelStateChanged(STATE_OPENING)
-        verify(noOpOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
-        verifyZeroInteractions(splitShadeOverScroller)
-    }
-
-    @Test
-    fun onPanelStateChanged_inSplitShade_onKeyguard_forwardsToNoOpOverScroller() {
-        initLateProperties()
-        enableSplitShade()
-        setOnKeyguard()
-
-        startPanelExpansion()
-
-        verify(noOpOverScroller).onPanelStateChanged(STATE_OPENING)
-        verify(noOpOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
-        verifyZeroInteractions(splitShadeOverScroller)
-    }
-
-    @Test
-    fun onPanelStateChanged_inSplitShade_onLockedShade_forwardsToNoOpOverScroller() {
-        initLateProperties()
-        enableSplitShade()
-        setOnLockedShade()
-
-        startPanelExpansion()
-
-        verify(noOpOverScroller).onPanelStateChanged(STATE_OPENING)
-        verify(noOpOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
-        verifyZeroInteractions(splitShadeOverScroller)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_onUnlockedShade_forwardsToSplitShadeOverScroller() {
-        initLateProperties()
-        enableSplitShade()
-        setOnUnlockedShade()
-
-        startPanelExpansion()
-
-        verify(splitShadeOverScroller).onPanelStateChanged(STATE_OPENING)
-        verify(splitShadeOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
-        verifyZeroInteractions(noOpOverScroller)
-    }
-
-    @Test
-    fun onPanelStateChanged_notInSplitShade_forwardsToNoOpOverScroller() {
-        initLateProperties()
-        disableSplitShade()
-
-        startPanelExpansion()
-
-        verify(noOpOverScroller).onPanelStateChanged(STATE_OPENING)
-        verify(noOpOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
-        verifyZeroInteractions(splitShadeOverScroller)
     }
 
     @Test
     fun onPanelStateChanged_forwardsToScrimTransitionController() {
-        initLateProperties()
-
         startPanelExpansion()
 
         verify(scrimShadeTransitionController).onPanelStateChanged(STATE_OPENING)
         verify(scrimShadeTransitionController).onPanelExpansionChanged(DEFAULT_EXPANSION_EVENT)
     }
 
-    private fun initLateProperties() {
-        controller.qs = qs
-        controller.notificationStackScrollLayoutController = nsslController
-        controller.notificationPanelViewController = npvc
-    }
-
-    private fun disableSplitShade() {
-        setSplitShadeEnabled(false)
-    }
-
-    private fun enableSplitShade() {
-        setSplitShadeEnabled(true)
-    }
-
-    private fun setSplitShadeEnabled(enabled: Boolean) {
-        overrideResource(R.bool.config_use_split_notification_shade, enabled)
-        configurationController.notifyConfigurationChanged()
-    }
-
     private fun startPanelExpansion() {
         shadeExpansionStateManager.onPanelExpansionChanged(
             DEFAULT_EXPANSION_EVENT.fraction,
@@ -174,23 +61,6 @@
         )
     }
 
-    private fun setOnKeyguard() {
-        setShadeState(StatusBarState.KEYGUARD)
-    }
-
-    private fun setOnLockedShade() {
-        setShadeState(StatusBarState.SHADE_LOCKED)
-    }
-
-    private fun setOnUnlockedShade() {
-        setShadeState(StatusBarState.SHADE)
-    }
-
-    private fun setShadeState(state: Int) {
-        whenever(statusBarStateController.state).thenReturn(state)
-        whenever(statusBarStateController.currentOrUpcomingState).thenReturn(state)
-    }
-
     companion object {
         private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f
         private val DEFAULT_EXPANSION_EVENT =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
deleted file mode 100644
index 0e48b48..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.android.systemui.shade.transition
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.qs.QS
-import com.android.systemui.shade.STATE_CLOSED
-import com.android.systemui.shade.STATE_OPEN
-import com.android.systemui.shade.STATE_OPENING
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@SmallTest
-class SplitShadeOverScrollerTest : SysuiTestCase() {
-
-    @Mock private lateinit var dumpManager: DumpManager
-    @Mock private lateinit var scrimController: ScrimController
-    @Mock private lateinit var qs: QS
-    @Mock private lateinit var nsslController: NotificationStackScrollLayoutController
-
-    private val configurationController = FakeConfigurationController()
-    private lateinit var overScroller: SplitShadeOverScroller
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        whenever(nsslController.height).thenReturn(1000)
-        overScroller =
-            SplitShadeOverScroller(
-                configurationController,
-                dumpManager,
-                context,
-                scrimController,
-                { qs },
-                { nsslController })
-    }
-
-    @Test
-    fun onDragDownAmountChanged_panelOpening_overScrolls_basedOnHeightAndMaxAmount() {
-        val maxOverScrollAmount = 50
-        val dragDownAmount = 100f
-        overrideResource(R.dimen.shade_max_over_scroll_amount, maxOverScrollAmount)
-        configurationController.notifyConfigurationChanged()
-
-        overScroller.onPanelStateChanged(STATE_OPENING)
-        overScroller.onDragDownAmountChanged(dragDownAmount)
-
-        val expectedOverScrollAmount =
-            (dragDownAmount / nsslController.height * maxOverScrollAmount).toInt()
-        verify(qs).setOverScrollAmount(expectedOverScrollAmount)
-        verify(nsslController).setOverScrollAmount(expectedOverScrollAmount)
-        verify(scrimController).setNotificationsOverScrollAmount(expectedOverScrollAmount)
-    }
-
-    @Test
-    fun onDragDownAmountChanged_panelClosed_doesNotOverScroll() {
-        overScroller.onPanelStateChanged(STATE_CLOSED)
-        overScroller.onDragDownAmountChanged(100f)
-
-        verifyZeroInteractions(qs, scrimController, nsslController)
-    }
-
-    @Test
-    fun onDragDownAmountChanged_panelOpen_doesNotOverScroll() {
-        overScroller.onPanelStateChanged(STATE_OPEN)
-        overScroller.onDragDownAmountChanged(100f)
-
-        verifyZeroInteractions(qs, scrimController, nsslController)
-    }
-
-    @Test
-    fun onPanelStateChanged_opening_thenOpen_releasesOverScroll() {
-        overScroller.onPanelStateChanged(STATE_OPENING)
-        overScroller.onDragDownAmountChanged(100f)
-
-        overScroller.onPanelStateChanged(STATE_OPEN)
-        overScroller.finishAnimations()
-
-        verify(qs, atLeastOnce()).setOverScrollAmount(0)
-        verify(scrimController, atLeastOnce()).setNotificationsOverScrollAmount(0)
-        verify(nsslController, atLeastOnce()).setOverScrollAmount(0)
-    }
-
-    @Test
-    fun onPanelStateChanged_opening_thenClosed_releasesOverScroll() {
-        overScroller.onPanelStateChanged(STATE_OPENING)
-        overScroller.onDragDownAmountChanged(100f)
-
-        overScroller.onPanelStateChanged(STATE_CLOSED)
-        overScroller.finishAnimations()
-
-        verify(qs, atLeastOnce()).setOverScrollAmount(0)
-        verify(scrimController, atLeastOnce()).setNotificationsOverScrollAmount(0)
-        verify(nsslController, atLeastOnce()).setOverScrollAmount(0)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
index 7a74b12..56fc0c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
@@ -36,21 +36,26 @@
 
     private val progressProvider = TestUnfoldTransitionProvider()
 
-    @Mock private lateinit var parent: ViewGroup
+    @Mock
+    private lateinit var parent: ViewGroup
+
+    @Mock
+    private lateinit var shouldBeAnimated: () -> Boolean
 
     private lateinit var animator: UnfoldConstantTranslateAnimator
 
-    private val viewsIdToRegister =
-        setOf(
-            ViewIdToTranslate(START_VIEW_ID, Direction.START),
-            ViewIdToTranslate(END_VIEW_ID, Direction.END))
+    private val viewsIdToRegister
+        get() =
+            setOf(
+                    ViewIdToTranslate(START_VIEW_ID, Direction.START, shouldBeAnimated),
+                    ViewIdToTranslate(END_VIEW_ID, Direction.END, shouldBeAnimated)
+            )
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-
-        animator =
-            UnfoldConstantTranslateAnimator(viewsIdToRegister, progressProvider)
+        whenever(shouldBeAnimated.invoke()).thenReturn(true)
+        animator = UnfoldConstantTranslateAnimator(viewsIdToRegister, progressProvider)
 
         animator.init(parent, MAX_TRANSLATION)
     }
@@ -96,6 +101,20 @@
         moveAndValidate(listOf(leftView to START, rightView to END), View.LAYOUT_DIRECTION_LTR)
     }
 
+    @Test
+    fun onTransition_completeStartedTranslation() {
+        // GIVEN
+        val leftView = View(context)
+        whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(leftView)
+        // To start animation, shouldBeAnimated should return true.
+        // There is a possibility for shouldBeAnimated to return false during the animation.
+        whenever(shouldBeAnimated.invoke()).thenReturn(true).thenReturn(false)
+
+        // shouldBeAnimated state may change during the animation.
+        // However, started animation should be completed.
+        moveAndValidate(listOf(leftView to START), View.LAYOUT_DIRECTION_LTR)
+    }
+
     private fun moveAndValidate(list: List<Pair<View, Int>>, layoutDirection: Int) {
         // Compare values as ints because -0f != 0f
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index 4d7741ad..1fdb364 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -18,8 +18,6 @@
 import android.content.ContentResolver
 import android.content.Context
 import android.graphics.drawable.Drawable
-import android.os.Handler
-import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -27,13 +25,16 @@
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
 import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.ClockSettings
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.plugins.PluginManager
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.fail
-import org.json.JSONException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -48,19 +49,19 @@
 class ClockRegistryTest : SysuiTestCase() {
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
+    private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var scope: TestScope
+
     @Mock private lateinit var mockContext: Context
     @Mock private lateinit var mockPluginManager: PluginManager
     @Mock private lateinit var mockClock: ClockController
     @Mock private lateinit var mockDefaultClock: ClockController
     @Mock private lateinit var mockThumbnail: Drawable
-    @Mock private lateinit var mockHandler: Handler
     @Mock private lateinit var mockContentResolver: ContentResolver
     private lateinit var fakeDefaultProvider: FakeClockPlugin
     private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
     private lateinit var registry: ClockRegistry
 
-    private var settingValue: String = ""
-
     companion object {
         private fun failFactory(): ClockController {
             fail("Unexpected call to createClock")
@@ -79,7 +80,8 @@
         private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>()
 
         override fun getClocks() = metadata
-        override fun createClock(id: ClockId): ClockController = createCallbacks[id]!!()
+        override fun createClock(settings: ClockSettings): ClockController =
+            createCallbacks[settings.clockId!!]!!()
         override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!()
 
         fun addClock(
@@ -97,6 +99,9 @@
 
     @Before
     fun setUp() {
+        dispatcher = StandardTestDispatcher()
+        scope = TestScope(dispatcher)
+
         fakeDefaultProvider = FakeClockPlugin()
             .addClock(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME, { mockDefaultClock }, { mockThumbnail })
         whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
@@ -105,15 +110,22 @@
         registry = object : ClockRegistry(
             mockContext,
             mockPluginManager,
-            mockHandler,
+            scope = scope.backgroundScope,
+            mainDispatcher = dispatcher,
+            bgDispatcher = dispatcher,
             isEnabled = true,
-            userHandle = UserHandle.USER_ALL,
-            defaultClockProvider = fakeDefaultProvider
+            handleAllUsers = true,
+            defaultClockProvider = fakeDefaultProvider,
         ) {
-            override var currentClockId: ClockId
-                get() = settingValue
-                set(value) { settingValue = value }
+            override fun querySettings() { }
+            override fun applySettings(value: ClockSettings?) {
+                settings = value
+            }
+            // Unit Test does not validate threading
+            override fun assertMainThread() {}
+            override fun assertNotMainThread() {}
         }
+        registry.registerListeners()
 
         verify(mockPluginManager)
             .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
@@ -185,16 +197,16 @@
             .addClock("clock_1", "clock 1")
             .addClock("clock_2", "clock 2")
 
-        settingValue = "clock_3"
         val plugin2 = FakeClockPlugin()
             .addClock("clock_3", "clock 3", { mockClock })
             .addClock("clock_4", "clock 4")
 
+        registry.applySettings(ClockSettings("clock_3", null))
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
 
         val clock = registry.createCurrentClock()
-        assertEquals(clock, mockClock)
+        assertEquals(mockClock, clock)
     }
 
     @Test
@@ -203,11 +215,11 @@
             .addClock("clock_1", "clock 1")
             .addClock("clock_2", "clock 2")
 
-        settingValue = "clock_3"
         val plugin2 = FakeClockPlugin()
             .addClock("clock_3", "clock 3")
             .addClock("clock_4", "clock 4")
 
+        registry.applySettings(ClockSettings("clock_3", null))
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
         pluginListener.onPluginDisconnected(plugin2)
@@ -217,74 +229,98 @@
     }
 
     @Test
-    fun pluginRemoved_clockChanged() {
+    fun pluginRemoved_clockAndListChanged() {
         val plugin1 = FakeClockPlugin()
             .addClock("clock_1", "clock 1")
             .addClock("clock_2", "clock 2")
 
-        settingValue = "clock_3"
         val plugin2 = FakeClockPlugin()
             .addClock("clock_3", "clock 3", { mockClock })
             .addClock("clock_4", "clock 4")
 
-        pluginListener.onPluginConnected(plugin1, mockContext)
-        pluginListener.onPluginConnected(plugin2, mockContext)
 
         var changeCallCount = 0
-        registry.registerClockChangeListener { changeCallCount++ }
+        var listChangeCallCount = 0
+        registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
+            override fun onCurrentClockChanged() { changeCallCount++ }
+            override fun onAvailableClocksChanged() { listChangeCallCount++ }
+        })
+
+        registry.applySettings(ClockSettings("clock_3", null))
+        assertEquals(0, changeCallCount)
+        assertEquals(0, listChangeCallCount)
+
+        pluginListener.onPluginConnected(plugin1, mockContext)
+        assertEquals(0, changeCallCount)
+        assertEquals(1, listChangeCallCount)
+
+        pluginListener.onPluginConnected(plugin2, mockContext)
+        assertEquals(1, changeCallCount)
+        assertEquals(2, listChangeCallCount)
 
         pluginListener.onPluginDisconnected(plugin1)
-        assertEquals(0, changeCallCount)
+        assertEquals(1, changeCallCount)
+        assertEquals(3, listChangeCallCount)
 
         pluginListener.onPluginDisconnected(plugin2)
-        assertEquals(1, changeCallCount)
+        assertEquals(2, changeCallCount)
+        assertEquals(4, listChangeCallCount)
     }
 
+
     @Test
     fun jsonDeserialization_gotExpectedObject() {
-        val expected = ClockRegistry.ClockSetting("ID", 500)
-        val actual = ClockRegistry.ClockSetting.deserialize("""{
+        val expected = ClockSettings("ID", null).apply {
+            metadata.put("appliedTimestamp", 500)
+        }
+        val actual = ClockSettings.deserialize("""{
             "clockId":"ID",
-            "_applied_timestamp":500
+            "metadata": {
+                "appliedTimestamp":500
+            }
         }""")
         assertEquals(expected, actual)
     }
 
     @Test
     fun jsonDeserialization_noTimestamp_gotExpectedObject() {
-        val expected = ClockRegistry.ClockSetting("ID", null)
-        val actual = ClockRegistry.ClockSetting.deserialize("{\"clockId\":\"ID\"}")
+        val expected = ClockSettings("ID", null)
+        val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}")
         assertEquals(expected, actual)
     }
 
     @Test
     fun jsonDeserialization_nullTimestamp_gotExpectedObject() {
-        val expected = ClockRegistry.ClockSetting("ID", null)
-        val actual = ClockRegistry.ClockSetting.deserialize("""{
+        val expected = ClockSettings("ID", null)
+        val actual = ClockSettings.deserialize("""{
             "clockId":"ID",
-            "_applied_timestamp":null
+            "metadata":null
         }""")
         assertEquals(expected, actual)
     }
 
-    @Test(expected = JSONException::class)
-    fun jsonDeserialization_noId_threwException() {
-        val expected = ClockRegistry.ClockSetting("ID", 500)
-        val actual = ClockRegistry.ClockSetting.deserialize("{\"_applied_timestamp\":500}")
+    @Test
+    fun jsonDeserialization_noId_deserializedEmpty() {
+        val expected = ClockSettings(null, null).apply {
+            metadata.put("appliedTimestamp", 500)
+        }
+        val actual = ClockSettings.deserialize("{\"metadata\":{\"appliedTimestamp\":500}}")
         assertEquals(expected, actual)
     }
 
     @Test
     fun jsonSerialization_gotExpectedString() {
-        val expected = "{\"clockId\":\"ID\",\"_applied_timestamp\":500}"
-        val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", 500))
+        val expected = "{\"clockId\":\"ID\",\"metadata\":{\"appliedTimestamp\":500}}"
+        val actual = ClockSettings.serialize(ClockSettings("ID", null).apply {
+            metadata.put("appliedTimestamp", 500)
+        })
         assertEquals(expected, actual)
     }
 
     @Test
     fun jsonSerialization_noTimestamp_gotExpectedString() {
-        val expected = "{\"clockId\":\"ID\"}"
-        val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", null))
+        val expected = "{\"clockId\":\"ID\",\"metadata\":{}}"
+        val actual = ClockSettings.serialize(ClockSettings("ID", null))
         assertEquals(expected, actual)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index a7588dd..7fa27f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -26,6 +26,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.ClockSettings
 import com.android.systemui.shared.clocks.DefaultClockController.Companion.DOZE_COLOR
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -40,7 +41,6 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyFloat
-import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.notNull
 import org.mockito.Mock
 import org.mockito.Mockito.never
@@ -97,13 +97,14 @@
     @Test
     fun defaultClock_initialize() {
         val clock = provider.createClock(DEFAULT_CLOCK_ID)
-        verify(mockSmallClockView).setColors(Color.MAGENTA, Color.MAGENTA)
-        verify(mockLargeClockView).setColors(Color.MAGENTA, Color.MAGENTA)
+        verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+        verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA)
 
         clock.initialize(resources, 0f, 0f)
 
-        verify(mockSmallClockView).setColors(eq(DOZE_COLOR), anyInt())
-        verify(mockLargeClockView).setColors(eq(DOZE_COLOR), anyInt())
+        val expectedColor = 0
+        verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor)
+        verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor)
         verify(mockSmallClockView).onTimeZoneChanged(notNull())
         verify(mockLargeClockView).onTimeZoneChanged(notNull())
         verify(mockSmallClockView).refreshTime()
@@ -114,7 +115,8 @@
     @Test
     fun defaultClock_events_onTimeTick() {
         val clock = provider.createClock(DEFAULT_CLOCK_ID)
-        clock.events.onTimeTick()
+        clock.smallClock.events.onTimeTick()
+        clock.largeClock.events.onTimeTick()
 
         verify(mockSmallClockView).refreshTime()
         verify(mockLargeClockView).refreshTime()
@@ -158,15 +160,31 @@
 
     @Test
     fun defaultClock_events_onColorPaletteChanged() {
+        val expectedColor = 0
         val clock = provider.createClock(DEFAULT_CLOCK_ID)
 
-        verify(mockSmallClockView).setColors(Color.MAGENTA, Color.MAGENTA)
-        verify(mockLargeClockView).setColors(Color.MAGENTA, Color.MAGENTA)
+        verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+        verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA)
 
         clock.events.onColorPaletteChanged(resources)
 
-        verify(mockSmallClockView).setColors(eq(DOZE_COLOR), anyInt())
-        verify(mockLargeClockView).setColors(eq(DOZE_COLOR), anyInt())
+        verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor)
+        verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor)
+    }
+
+    @Test
+    fun defaultClock_events_onSeedColorChanged() {
+        val initSeedColor = 10
+        val newSeedColor = 20
+        val clock = provider.createClock(ClockSettings(DEFAULT_CLOCK_ID, initSeedColor))
+
+        verify(mockSmallClockView).setColors(DOZE_COLOR, initSeedColor)
+        verify(mockLargeClockView).setColors(DOZE_COLOR, initSeedColor)
+
+        clock.events.onSeedColorChanged(newSeedColor)
+
+        verify(mockSmallClockView).setColors(DOZE_COLOR, newSeedColor)
+        verify(mockLargeClockView).setColors(DOZE_COLOR, newSeedColor)
     }
 
     @Test
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..aa1636d 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
@@ -249,6 +249,21 @@
     }
 
     @Test
+    public void addCallback_preCondition_noConditions_reportAllConditionsMet() {
+        final Monitor
+                monitor = new Monitor(mExecutor, new HashSet<>(Arrays.asList(mCondition1)));
+        final Monitor.Callback callback = mock(
+                Monitor.Callback.class);
+
+        monitor.addSubscription(new Monitor.Subscription.Builder(callback).build());
+        mExecutor.runAllReady();
+        verify(callback, never()).onConditionsChanged(true);
+        mCondition1.fakeUpdateCondition(true);
+        mExecutor.runAllReady();
+        verify(callback).onConditionsChanged(true);
+    }
+
+    @Test
     public void removeCallback_noFailureOnDoubleRemove() {
         final Condition condition = mock(
                 Condition.class);
@@ -471,4 +486,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/shared/regionsampling/RegionSamplerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
index 5a62cc1..ae1c8cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
@@ -1,21 +1,17 @@
 package com.android.systemui.shared.regionsampling
 
-import android.graphics.Rect
+import android.app.WallpaperManager
 import android.testing.AndroidTestingRunner
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper
 import java.io.PrintWriter
 import java.util.concurrent.Executor
-import org.junit.Assert
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
@@ -28,9 +24,8 @@
     @Mock private lateinit var sampledView: View
     @Mock private lateinit var mainExecutor: Executor
     @Mock private lateinit var bgExecutor: Executor
-    @Mock private lateinit var regionSampler: RegionSamplingHelper
     @Mock private lateinit var pw: PrintWriter
-    @Mock private lateinit var callback: RegionSamplingHelper.SamplingCallback
+    @Mock private lateinit var wallpaperManager: WallpaperManager
 
     private lateinit var mRegionSampler: RegionSampler
     private var updateFun: UpdateColorCallback = {}
@@ -38,65 +33,18 @@
     @Before
     fun setUp() {
         whenever(sampledView.isAttachedToWindow).thenReturn(true)
-        whenever(regionSampler.callback).thenReturn(this@RegionSamplerTest.callback)
 
         mRegionSampler =
-            object : RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun) {
-                override fun createRegionSamplingHelper(
-                    sampledView: View,
-                    callback: RegionSamplingHelper.SamplingCallback,
-                    mainExecutor: Executor?,
-                    bgExecutor: Executor?
-                ): RegionSamplingHelper {
-                    return this@RegionSamplerTest.regionSampler
-                }
-            }
+            RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun, wallpaperManager)
     }
 
     @Test
     fun testStartRegionSampler() {
         mRegionSampler.startRegionSampler()
-
-        verify(regionSampler).start(Rect(0, 0, 0, 0))
-    }
-
-    @Test
-    fun testStopRegionSampler() {
-        mRegionSampler.stopRegionSampler()
-
-        verify(regionSampler).stop()
     }
 
     @Test
     fun testDump() {
         mRegionSampler.dump(pw)
-
-        verify(regionSampler).dump(pw)
-    }
-
-    @Test
-    fun testUpdateColorCallback() {
-        regionSampler.callback.onRegionDarknessChanged(false)
-        verify(regionSampler.callback).onRegionDarknessChanged(false)
-        clearInvocations(regionSampler.callback)
-        regionSampler.callback.onRegionDarknessChanged(true)
-        verify(regionSampler.callback).onRegionDarknessChanged(true)
-    }
-
-    @Test
-    fun testFlagFalse() {
-        mRegionSampler =
-            object : RegionSampler(sampledView, mainExecutor, bgExecutor, false, updateFun) {
-                override fun createRegionSamplingHelper(
-                    sampledView: View,
-                    callback: RegionSamplingHelper.SamplingCallback,
-                    mainExecutor: Executor?,
-                    bgExecutor: Executor?
-                ): RegionSamplingHelper {
-                    return this@RegionSamplerTest.regionSampler
-                }
-            }
-
-        Assert.assertEquals(mRegionSampler.regionSampler, null)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
new file mode 100644
index 0000000..5fb1e79
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.smartspace
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.smartspace.config.BcSmartspaceConfigProvider
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BcSmartspaceConfigProviderTest : SysuiTestCase() {
+    @Mock private lateinit var featureFlags: FeatureFlags
+
+    private lateinit var configProvider: BcSmartspaceConfigProvider
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        configProvider = BcSmartspaceConfigProvider(featureFlags)
+    }
+
+    @Test
+    fun isDefaultDateWeatherDisabled_flagIsTrue_returnsTrue() {
+        whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+
+        assertTrue(configProvider.isDefaultDateWeatherDisabled)
+    }
+
+    @Test
+    fun isDefaultDateWeatherDisabled_flagIsFalse_returnsFalse() {
+        whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
+
+        assertFalse(configProvider.isDefaultDateWeatherDisabled)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
index 001e1f4..a280510 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dreams.smartspace.DreamSmartspaceController
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
 import com.android.systemui.plugins.FalsingManager
@@ -94,9 +95,9 @@
     private class TestView(context: Context?) : View(context), SmartspaceView {
         override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {}
 
-        override fun setPrimaryTextColor(color: Int) {}
+        override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {}
 
-        override fun setIsDreaming(isDreaming: Boolean) {}
+        override fun setPrimaryTextColor(color: Int) {}
 
         override fun setUiSurface(uiSurface: String) {}
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
index d29e9a6..fa7d869 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
@@ -20,8 +20,6 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.smartspace.preconditions.LockscreenPrecondition
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.util.concurrency.Execution
@@ -41,9 +39,6 @@
 @TestableLooper.RunWithLooper
 class LockscreenPreconditionTest : SysuiTestCase() {
     @Mock
-    private lateinit var featureFlags: FeatureFlags
-
-    @Mock
     private lateinit var deviceProvisionedController: DeviceProvisionedController
 
     @Mock
@@ -64,10 +59,7 @@
     fun testFullyEnabled() {
         `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
         `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
-        `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE))
-                .thenReturn(true)
-        val precondition = LockscreenPrecondition(featureFlags, deviceProvisionedController,
-                execution)
+        val precondition = LockscreenPrecondition(deviceProvisionedController, execution)
         precondition.addListener(listener)
 
         `verify`(listener).onCriteriaChanged()
@@ -81,10 +73,8 @@
     fun testProvisioning() {
         `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
         `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(false)
-        `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE))
-                .thenReturn(true)
         val precondition =
-                LockscreenPrecondition(featureFlags, deviceProvisionedController, execution)
+                LockscreenPrecondition(deviceProvisionedController, execution)
         precondition.addListener(listener)
 
         verify(listener).onCriteriaChanged()
@@ -109,10 +99,8 @@
     fun testUserSetup() {
         `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
         `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
-        `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE))
-                .thenReturn(true)
         val precondition =
-                LockscreenPrecondition(featureFlags, deviceProvisionedController, execution)
+                LockscreenPrecondition(deviceProvisionedController, execution)
         precondition.addListener(listener)
 
         verify(listener).onCriteriaChanged()
@@ -129,4 +117,4 @@
         verify(listener).onCriteriaChanged()
         assertThat(precondition.conditionsMet()).isTrue()
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 8aaa181..e68d3b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -45,6 +45,7 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 
 import org.junit.After;
@@ -62,12 +63,14 @@
     };
 
     private CommandQueue mCommandQueue;
+
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private Callbacks mCallbacks;
     private static final int SECONDARY_DISPLAY = 1;
 
     @Before
     public void setup() {
-        mCommandQueue = new CommandQueue(mContext);
+        mCommandQueue = new CommandQueue(mContext, mDisplayTracker);
         mCallbacks = mock(Callbacks.class);
         mCommandQueue.addCallback(mCallbacks);
         verify(mCallbacks).disable(anyInt(), eq(0), eq(0), eq(false));
@@ -415,7 +418,7 @@
 
     @Test
     public void testOnDisplayRemoved() {
-        mCommandQueue.onDisplayRemoved(SECONDARY_DISPLAY);
+        mDisplayTracker.triggerOnDisplayRemoved(SECONDARY_DISPLAY);
         waitForIdleSync();
         verify(mCallbacks).onDisplayRemoved(eq(SECONDARY_DISPLAY));
     }
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 d2dd433..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;
@@ -58,6 +59,7 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.AlarmManager;
 import android.app.Instrumentation;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyResourcesManager;
@@ -99,6 +101,7 @@
 import com.android.systemui.keyguard.KeyguardIndication;
 import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController;
 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.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -177,9 +180,13 @@
     @Mock
     private FaceHelpMessageDeferral mFaceHelpMessageDeferral;
     @Mock
+    private AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @Mock
     private ScreenLifecycle mScreenLifecycle;
     @Mock
     private AuthController mAuthController;
+    @Mock
+    private AlarmManager mAlarmManager;
     @Captor
     private ArgumentCaptor<DockManager.AlignmentStateListener> mAlignmentListener;
     @Captor
@@ -273,7 +280,10 @@
                 mUserManager, mExecutor, mExecutor, mFalsingManager,
                 mAuthController, mLockPatternUtils, mScreenLifecycle,
                 mKeyguardBypassController, mAccessibilityManager,
-                mFaceHelpMessageDeferral, mock(KeyguardLogger.class));
+                mFaceHelpMessageDeferral, mock(KeyguardLogger.class),
+                mAlternateBouncerInteractor,
+                mAlarmManager
+        );
         mController.init();
         mController.setIndicationArea(mIndicationArea);
         verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
@@ -611,6 +621,109 @@
     }
 
     @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();
+
+        // GIVEN face has already unlocked the device
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithFace(anyInt())).thenReturn(true);
+
+        String message = "A message";
+        mController.setVisible(true);
+
+        // WHEN there's a fingerprint not recognized message
+        mController.getKeyguardCallback().onBiometricHelp(
+                BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+                message,
+                BiometricSourceType.FINGERPRINT);
+
+        // THEN show sequential messages such as: 'Unlocked by face' and
+        // 'Swipe up to open'
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                mContext.getString(R.string.keyguard_face_successful_unlock));
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onBiometricHelp_coEx_fpFailure_trustAgentAlreadyUnlocked() {
+        createController();
+
+        // GIVEN trust agent has already unlocked the device
+        when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
+
+        String message = "A message";
+        mController.setVisible(true);
+
+        // WHEN there's a fingerprint not recognized message
+        mController.getKeyguardCallback().onBiometricHelp(
+                BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+                message,
+                BiometricSourceType.FINGERPRINT);
+
+        // THEN show sequential messages such as: 'Kept unlocked by TrustAgent' and
+        // 'Swipe up to open'
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                mContext.getString(R.string.keyguard_indication_trust_unlocked));
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onBiometricHelp_coEx_fpFailure_trustAgentUnlocked_emptyTrustGrantedMessage() {
+        createController();
+
+        // GIVEN trust agent has already unlocked the device & trust granted message is empty
+        when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
+        mController.showTrustGrantedMessage(false, "");
+
+        String message = "A message";
+        mController.setVisible(true);
+
+        // WHEN there's a fingerprint not recognized message
+        mController.getKeyguardCallback().onBiometricHelp(
+                BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+                message,
+                BiometricSourceType.FINGERPRINT);
+
+        // THEN show action to unlock (ie: 'Swipe up to open')
+        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
     public void transientIndication_visibleWhenDozing_unlessSwipeUp_fromError() {
         createController();
         String message = mContext.getString(R.string.keyguard_unlock);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 3d11ced..d99cdd51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -79,6 +79,7 @@
     @Mock lateinit var singleShadeOverScroller: SingleShadeLockScreenOverScroller
     @Mock lateinit var splitShadeOverScroller: SplitShadeLockScreenOverScroller
     @Mock lateinit var qsTransitionController: LockscreenShadeQsTransitionController
+    @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
     private val configurationController = FakeConfigurationController()
@@ -124,6 +125,7 @@
                 },
                 qsTransitionControllerFactory = { qsTransitionController },
             )
+        transitionController.addCallback(transitionControllerCallback)
         whenever(nsslController.view).thenReturn(stackscroller)
         whenever(nsslController.expandHelperCallback).thenReturn(expandHelperCallback)
         transitionController.notificationPanelController = notificationPanelController
@@ -244,13 +246,21 @@
     }
 
     @Test
+    fun testGoToLockedShadeAlwaysCreatesQSAnimationInSplitShade() {
+        enableSplitShade()
+        transitionController.goToLockedShade(null, needsQSAnimation = true)
+        verify(notificationPanelController).animateToFullShade(anyLong())
+        assertNotNull(transitionController.dragDownAnimator)
+    }
+
+    @Test
     fun testDragDownAmountDoesntCallOutInLockedDownShade() {
         whenever(nsslController.isInLockedDownShade).thenReturn(true)
         transitionController.dragDownAmount = 10f
         verify(nsslController, never()).setTransitionToFullShadeAmount(anyFloat())
         verify(mediaHierarchyManager, never()).setTransitionToFullShadeAmount(anyFloat())
         verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat(), anyFloat())
-        verify(notificationPanelController, never()).setTransitionToFullShadeAmount(anyFloat(),
+        verify(transitionControllerCallback, never()).setTransitionToFullShadeAmount(anyFloat(),
                 anyBoolean(), anyLong())
         verify(qsTransitionController, never()).dragDownAmount = anyFloat()
     }
@@ -261,7 +271,7 @@
         verify(nsslController).setTransitionToFullShadeAmount(anyFloat())
         verify(mediaHierarchyManager).setTransitionToFullShadeAmount(anyFloat())
         verify(scrimController).setTransitionToFullShadeProgress(anyFloat(), anyFloat())
-        verify(notificationPanelController).setTransitionToFullShadeAmount(anyFloat(),
+        verify(transitionControllerCallback).setTransitionToFullShadeAmount(anyFloat(),
                 anyBoolean(), anyLong())
         verify(qsTransitionController).dragDownAmount = 10f
         verify(depthController).transitionToFullShadeProgress = anyFloat()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 452606d..8ee1ea8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -44,6 +44,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -354,7 +355,8 @@
                     mDeviceProvisionedController,
                     mKeyguardStateController,
                     mSettings,
-                    mock(DumpManager.class));
+                    mock(DumpManager.class),
+                    mock(LockPatternUtils.class));
         }
 
         public BroadcastReceiver getBaseBroadcastReceiverForTest() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 5124eb9..e6f272b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -37,6 +37,7 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
+import org.mockito.Mockito
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -152,4 +153,18 @@
         // and cause us to drop a frame during the LOCKSCREEN_TRANSITION_FROM_AOD CUJ.
         assertEquals(0.99f, controller.dozeAmount, 0.009f)
     }
+
+    @Test
+    fun testSetDreamState_invokesCallback() {
+        val listener = mock(StatusBarStateController.StateListener::class.java)
+        controller.addCallback(listener)
+
+        controller.setIsDreaming(true)
+        verify(listener).onDreamingChanged(true)
+
+        Mockito.clearInvocations(listener)
+
+        controller.setIsDreaming(false)
+        verify(listener).onDreamingChanged(false)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt
new file mode 100644
index 0000000..cd06465
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.events
+
+/**
+ * This is a freely configurable implementation of [StatusEvent]. It is intended to be used in
+ * tests.
+ */
+class FakeStatusEvent(
+    override val viewCreator: ViewCreator,
+    override val priority: Int = 50,
+    override var forceVisible: Boolean = false,
+    override val showAnimation: Boolean = true,
+    override var contentDescription: String? = "",
+) : StatusEvent
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
new file mode 100644
index 0000000..08a9f31
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -0,0 +1,470 @@
+/*
+ * 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.events
+
+import android.graphics.Rect
+import android.os.Process
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.statusbar.BatteryStatusChip
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var systemEventCoordinator: SystemEventCoordinator
+    @Mock private lateinit var statusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var statusBarContentInsetProvider: StatusBarContentInsetsProvider
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var listener: SystemStatusAnimationCallback
+
+    private lateinit var systemClock: FakeSystemClock
+    private lateinit var chipAnimationController: SystemEventChipAnimationController
+    private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler
+    private val fakeFeatureFlags = FakeFeatureFlags()
+
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        fakeFeatureFlags.set(Flags.PLUG_IN_STATUS_BAR_CHIP, true)
+
+        systemClock = FakeSystemClock()
+        chipAnimationController =
+            SystemEventChipAnimationController(
+                mContext,
+                statusBarWindowController,
+                statusBarContentInsetProvider,
+                fakeFeatureFlags
+            )
+
+        // ensure that isTooEarly() check in SystemStatusAnimationScheduler does not return true
+        systemClock.advanceTime(Process.getStartUptimeMillis() + MIN_UPTIME)
+
+        // StatusBarContentInsetProvider is mocked. Ensure that it returns some mocked values.
+        whenever(statusBarContentInsetProvider.getStatusBarContentInsetsForCurrentRotation())
+            .thenReturn(android.util.Pair(10, 10))
+        whenever(statusBarContentInsetProvider.getStatusBarContentAreaForCurrentRotation())
+            .thenReturn(Rect(10, 0, 990, 100))
+
+        // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
+        // ensure that the chip view is added to a parent view
+        whenever(statusBarWindowController.addViewToWindow(any(), any())).then {
+            val statusbarFake = FrameLayout(mContext)
+            statusbarFake.layout(0, 0, 1000, 100)
+            statusbarFake.addView(
+                it.arguments[0] as View,
+                it.arguments[1] as FrameLayout.LayoutParams
+            )
+        }
+    }
+
+    @Test
+    fun testBatteryStatusEvent_standardAnimationLifecycle() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        val batteryChip = createAndScheduleFakeBatteryEvent()
+
+        // assert that animation is queued
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip debounce delay
+        advanceTimeBy(DEBOUNCE_DELAY + 1)
+        // status chip starts animating in after debounce delay
+        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(0f, batteryChip.contentView.alpha)
+        assertEquals(0f, batteryChip.view.alpha)
+        verify(listener, times(1)).onSystemEventAnimationBegin()
+
+        // skip appear animation
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+        advanceTimeBy(APPEAR_ANIMATION_DURATION)
+        // assert that status chip is visible
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, batteryChip.contentView.alpha)
+        assertEquals(1f, batteryChip.view.alpha)
+
+        // skip status chip display time
+        advanceTimeBy(DISPLAY_LENGTH + 1)
+        // assert that it is still visible but switched to the ANIMATING_OUT state
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, batteryChip.contentView.alpha)
+        assertEquals(1f, batteryChip.view.alpha)
+        verify(listener, times(1)).onSystemEventAnimationFinish(false)
+
+        // skip disappear animation
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+        // assert that it is not visible anymore
+        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(0f, batteryChip.contentView.alpha)
+        assertEquals(0f, batteryChip.view.alpha)
+    }
+
+    @Test
+    fun testPrivacyStatusEvent_standardAnimationLifecycle() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        val privacyChip = createAndScheduleFakePrivacyEvent()
+
+        // assert that animation is queued
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip debounce delay
+        advanceTimeBy(DEBOUNCE_DELAY + 1)
+        // status chip starts animating in after debounce delay
+        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(0f, privacyChip.view.alpha)
+        verify(listener, times(1)).onSystemEventAnimationBegin()
+
+        // skip appear animation
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+        advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
+        // assert that status chip is visible
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+
+        // skip status chip display time
+        advanceTimeBy(DISPLAY_LENGTH + 1)
+        // assert that it is still visible but switched to the ANIMATING_OUT state
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+        verify(listener, times(1)).onSystemEventAnimationFinish(true)
+        verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
+
+        // skip transition to persistent dot
+        advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1)
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+        // assert that it the dot is now visible
+        assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+
+        // notify SystemStatusAnimationScheduler to remove persistent dot
+        systemStatusAnimationScheduler.removePersistentDot()
+        // assert that IDLE state is entered
+        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        verify(listener, times(1)).onHidePersistentDot()
+    }
+
+    @Test
+    fun testHighPriorityEvent_takesPrecedenceOverScheduledLowPriorityEvent() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule low priority event
+        val batteryChip = createAndScheduleFakeBatteryEvent()
+        batteryChip.view.alpha = 0f
+
+        // assert that animation is queued
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+        // create and schedule high priority event
+        val privacyChip = createAndScheduleFakePrivacyEvent()
+
+        // assert that animation is queued
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip debounce delay and appear animation duration
+        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+
+        // high priority status chip is visible while low priority status chip is not visible
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+        assertEquals(0f, batteryChip.view.alpha)
+    }
+
+    @Test
+    fun testHighPriorityEvent_cancelsCurrentlyDisplayedLowPriorityEvent() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule low priority event
+        val batteryChip = createAndScheduleFakeBatteryEvent()
+
+        // fast forward to RUNNING_CHIP_ANIM state
+        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+
+        // assert that chip is displayed
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, batteryChip.view.alpha)
+
+        // create and schedule high priority event
+        val privacyChip = createAndScheduleFakePrivacyEvent()
+
+        // ensure that the event cancellation coroutine is started by the test scope
+        testScheduler.runCurrent()
+
+        // assert that currently displayed chip is immediately animated out
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip disappear animation
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+
+        // assert that high priority privacy chip animation is queued
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip debounce delay and appear animation
+        advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+
+        // high priority status chip is visible while low priority status chip is not visible
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+        assertEquals(0f, batteryChip.view.alpha)
+    }
+
+    @Test
+    fun testHighPriorityEvent_cancelsCurrentlyAnimatedLowPriorityEvent() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule low priority event
+        val batteryChip = createAndScheduleFakeBatteryEvent()
+
+        // skip debounce delay
+        advanceTimeBy(DEBOUNCE_DELAY + 1)
+
+        // assert that chip is animated in
+        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+
+        // create and schedule high priority event
+        val privacyChip = createAndScheduleFakePrivacyEvent()
+
+        // ensure that the event cancellation coroutine is started by the test scope
+        testScheduler.runCurrent()
+
+        // assert that currently animated chip keeps animating
+        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip appear animation
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+        advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
+
+        // assert that low priority chip is animated out immediately after finishing the appear
+        // animation
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip disappear animation
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+
+        // assert that high priority privacy chip animation is queued
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip debounce delay and appear animation
+        advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+
+        // high priority status chip is visible while low priority status chip is not visible
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+        assertEquals(0f, batteryChip.view.alpha)
+    }
+
+    @Test
+    fun testHighPriorityEvent_isNotReplacedByLowPriorityEvent() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule high priority event
+        val privacyChip = createAndScheduleFakePrivacyEvent()
+
+        // create and schedule low priority event
+        val batteryChip = createAndScheduleFakeBatteryEvent()
+        batteryChip.view.alpha = 0f
+
+        // skip debounce delay and appear animation
+        advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+
+        // high priority status chip is visible while low priority status chip is not visible
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(1f, privacyChip.view.alpha)
+        assertEquals(0f, batteryChip.view.alpha)
+    }
+
+    @Test
+    fun testPrivacyDot_isRemoved() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule high priority event
+        createAndScheduleFakePrivacyEvent()
+
+        // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state
+        fastForwardAnimationToState(SHOWING_PERSISTENT_DOT)
+        assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+        verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
+
+        // remove persistent dot and verify that animationState changes to IDLE
+        systemStatusAnimationScheduler.removePersistentDot()
+        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        verify(listener, times(1)).onHidePersistentDot()
+    }
+
+    @Test
+    fun testPrivacyDot_isRemovedDuringChipAnimation() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule high priority event
+        createAndScheduleFakePrivacyEvent()
+
+        // skip chip animation lifecycle and fast forward to RUNNING_CHIP_ANIM state
+        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+
+        // request removal of persistent dot
+        systemStatusAnimationScheduler.removePersistentDot()
+
+        // skip display time and verify that disappear animation is run
+        advanceTimeBy(DISPLAY_LENGTH + 1)
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip disappear animation and verify that animationState changes to IDLE instead of
+        // SHOWING_PERSISTENT_DOT
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        // verify that the persistent dot callbacks are not invoked
+        verify(listener, never()).onSystemStatusAnimationTransitionToPersistentDot(any())
+        verify(listener, never()).onHidePersistentDot()
+    }
+
+    @Test
+    fun testNewEvent_isScheduled_whenPostedDuringRemovalAnimation() = runTest {
+        // Instantiate class under test with TestScope from runTest
+        initializeSystemStatusAnimationScheduler(testScope = this)
+
+        // create and schedule high priority event
+        createAndScheduleFakePrivacyEvent()
+
+        // skip chip animation lifecycle and fast forward to ANIMATING_OUT state
+        fastForwardAnimationToState(ANIMATING_OUT)
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
+
+        // request removal of persistent dot
+        systemStatusAnimationScheduler.removePersistentDot()
+        testScheduler.runCurrent()
+
+        // schedule another high priority event while the event is animating out
+        createAndScheduleFakePrivacyEvent()
+
+        // verify that the state is still ANIMATING_OUT
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+        // skip disappear animation duration and verify that new state is ANIMATION_QUEUED
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+        testScheduler.runCurrent()
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        // also verify that onHidePersistentDot callback is called
+        verify(listener, times(1)).onHidePersistentDot()
+    }
+
+    private fun TestScope.fastForwardAnimationToState(@SystemAnimationState animationState: Int) {
+        // this function should only be called directly after posting a status event
+        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        if (animationState == IDLE || animationState == ANIMATION_QUEUED) return
+        // skip debounce delay
+        advanceTimeBy(DEBOUNCE_DELAY + 1)
+
+        // status chip starts animating in after debounce delay
+        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        verify(listener, times(1)).onSystemEventAnimationBegin()
+        if (animationState == ANIMATING_IN) return
+
+        // skip appear animation
+        animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+        advanceTimeBy(APPEAR_ANIMATION_DURATION)
+        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        if (animationState == RUNNING_CHIP_ANIM) return
+
+        // skip status chip display time
+        advanceTimeBy(DISPLAY_LENGTH + 1)
+        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean())
+        if (animationState == ANIMATING_OUT) return
+
+        // skip disappear animation
+        animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+    }
+
+    private fun createAndScheduleFakePrivacyEvent(): OngoingPrivacyChip {
+        val privacyChip = OngoingPrivacyChip(mContext)
+        val fakePrivacyStatusEvent =
+            FakeStatusEvent(viewCreator = { privacyChip }, priority = 100, forceVisible = true)
+        systemStatusAnimationScheduler.onStatusEvent(fakePrivacyStatusEvent)
+        return privacyChip
+    }
+
+    private fun createAndScheduleFakeBatteryEvent(): BatteryStatusChip {
+        val batteryChip = BatteryStatusChip(mContext)
+        val fakeBatteryEvent =
+            FakeStatusEvent(viewCreator = { batteryChip }, priority = 50, forceVisible = false)
+        systemStatusAnimationScheduler.onStatusEvent(fakeBatteryEvent)
+        return batteryChip
+    }
+
+    private fun initializeSystemStatusAnimationScheduler(testScope: TestScope) {
+        systemStatusAnimationScheduler =
+            SystemStatusAnimationSchedulerImpl(
+                systemEventCoordinator,
+                chipAnimationController,
+                statusBarWindowController,
+                dumpManager,
+                systemClock,
+                CoroutineScope(StandardTestDispatcher(testScope.testScheduler))
+            )
+        // add a mock listener
+        systemStatusAnimationScheduler.addCallback(listener)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt
index ea06647..a9c3d5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt
@@ -6,6 +6,7 @@
 import android.view.MotionEvent
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -17,6 +18,7 @@
 class GenericGestureDetectorTest : SysuiTestCase() {
 
     private lateinit var gestureDetector: TestGestureDetector
+    private val displayTracker = FakeDisplayTracker(mContext)
 
     @Before
     fun setUp() {
@@ -101,12 +103,21 @@
         gestureDetector.addOnGestureDetectedCallback("tag2"){}
 
         gestureDetector.removeOnGestureDetectedCallback("tag")
-        gestureDetector.onInputEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, CORRECT_X, 0f, 0))
+        gestureDetector.onInputEvent(
+            MotionEvent.obtain(
+                0,
+                0,
+                MotionEvent.ACTION_DOWN,
+                CORRECT_X,
+                0f,
+                0
+            )
+        )
 
         assertThat(oldCallbackNotified).isFalse()
     }
 
-    inner class TestGestureDetector : GenericGestureDetector("fakeTag") {
+    inner class TestGestureDetector : GenericGestureDetector("fakeTag", displayTracker) {
         var isGestureListening = false
 
         override fun onInputEvent(ev: InputEvent) {
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 ddcf59e..2de5705 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.lockscreen
 
+import android.app.smartspace.SmartspaceAction
 import android.app.smartspace.SmartspaceManager
 import android.app.smartspace.SmartspaceSession
 import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener
@@ -26,20 +27,25 @@
 import android.database.ContentObserver
 import android.graphics.drawable.Drawable
 import android.net.Uri
+import android.os.Bundle
 import android.os.Handler
 import android.os.UserHandle
 import android.provider.Settings
 import android.view.View
 import android.widget.FrameLayout
 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
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.WeatherData
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener
 import com.android.systemui.settings.UserTracker
@@ -51,6 +57,7 @@
 import com.android.systemui.util.concurrency.FakeExecution
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argThat
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.settings.SecureSettings
@@ -66,6 +73,7 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import java.util.Optional
@@ -73,6 +81,13 @@
 
 @SmallTest
 class LockscreenSmartspaceControllerTest : SysuiTestCase() {
+    companion object {
+        const val SMARTSPACE_TIME_TOO_EARLY = 1000L
+        const val SMARTSPACE_TIME_JUST_RIGHT = 4000L
+        const val SMARTSPACE_TIME_TOO_LATE = 9000L
+        const val SMARTSPACE_CREATION_TIME = 1234L
+        const val SMARTSPACE_EXPIRY_TIME = 5678L
+    }
     @Mock
     private lateinit var featureFlags: FeatureFlags
     @Mock
@@ -103,6 +118,9 @@
     private lateinit var keyguardBypassController: KeyguardBypassController
 
     @Mock
+    private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    @Mock
     private lateinit var deviceProvisionedController: DeviceProvisionedController
 
     @Mock
@@ -112,9 +130,21 @@
     private lateinit var handler: Handler
 
     @Mock
+    private lateinit var datePlugin: BcSmartspaceDataPlugin
+
+    @Mock
+    private lateinit var weatherPlugin: BcSmartspaceDataPlugin
+
+    @Mock
     private lateinit var plugin: BcSmartspaceDataPlugin
 
     @Mock
+    private lateinit var configPlugin: BcSmartspaceConfigPlugin
+
+    @Mock
+    private lateinit var dumpManager: DumpManager
+
+    @Mock
     private lateinit var controllerListener: SmartspaceTargetListener
 
     @Captor
@@ -148,6 +178,8 @@
         KeyguardBypassController.OnBypassStateChangedListener
     private lateinit var deviceProvisionedListener: DeviceProvisionedListener
 
+    private lateinit var dateSmartspaceView: SmartspaceView
+    private lateinit var weatherSmartspaceView: SmartspaceView
     private lateinit var smartspaceView: SmartspaceView
 
     private val clock = FakeSystemClock()
@@ -173,18 +205,24 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE)).thenReturn(true)
+        // Todo(b/261760571): flip the flag value here when feature is launched, and update relevant
+        //  tests.
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
 
         `when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING))
                 .thenReturn(fakePrivateLockscreenSettingUri)
         `when`(secureSettings.getUriFor(NOTIF_ON_LOCKSCREEN_SETTING))
                 .thenReturn(fakeNotifOnLockscreenSettingUri)
         `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
+        `when`(datePlugin.getView(any())).thenReturn(
+                createDateSmartspaceView(), createDateSmartspaceView())
+        `when`(weatherPlugin.getView(any())).thenReturn(
+                createWeatherSmartspaceView(), createWeatherSmartspaceView())
         `when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView())
         `when`(userTracker.userProfiles).thenReturn(userList)
         `when`(statusBarStateController.dozeAmount).thenReturn(0.5f)
-        `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
-        `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true)
+        `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+        `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
 
         setActiveUser(userHandlePrimary)
         setAllowPrivateNotifications(userHandlePrimary, true)
@@ -198,6 +236,7 @@
                 smartspaceManager,
                 activityStarter,
                 falsingManager,
+                clock,
                 secureSettings,
                 userTracker,
                 contentResolver,
@@ -205,11 +244,16 @@
                 statusBarStateController,
                 deviceProvisionedController,
                 keyguardBypassController,
+                keyguardUpdateMonitor,
+                dumpManager,
                 execution,
                 executor,
                 bgExecutor,
                 handler,
-                Optional.of(plugin)
+                Optional.of(datePlugin),
+                Optional.of(weatherPlugin),
+                Optional.of(plugin),
+                Optional.of(configPlugin),
         )
 
         verify(deviceProvisionedController).addCallback(capture(deviceProvisionedCaptor))
@@ -217,21 +261,21 @@
     }
 
     @Test(expected = RuntimeException::class)
-    fun testThrowsIfFlagIsDisabled() {
+    fun testBuildAndConnectWeatherView_throwsIfDecouplingDisabled() {
         // GIVEN the feature flag is disabled
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE)).thenReturn(false)
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
 
         // WHEN we try to build the view
-        controller.buildAndConnectView(fakeParent)
+        controller.buildAndConnectWeatherView(fakeParent)
 
         // THEN an exception is thrown
     }
 
     @Test
-    fun connectOnlyAfterDeviceIsProvisioned() {
+    fun testBuildAndConnectView_connectsOnlyAfterDeviceIsProvisioned() {
         // GIVEN an unprovisioned device and an attempt to connect
-        `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(false)
-        `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(false)
+        `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(false)
+        `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
 
         // WHEN a connection attempt is made and view is attached
         val view = controller.buildAndConnectView(fakeParent)
@@ -241,8 +285,8 @@
         verify(smartspaceManager, never()).createSmartspaceSession(any())
 
         // WHEN it does become provisioned
-        `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
-        `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true)
+        `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+        `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
         deviceProvisionedListener.onUserSetupChanged()
 
         // THEN the session is created
@@ -252,7 +296,7 @@
     }
 
     @Test
-    fun testListenersAreRegistered() {
+    fun testAddListener_registersListenersForPlugin() {
         // GIVEN a listener is added after a session is created
         connectSession()
 
@@ -261,10 +305,13 @@
 
         // THEN the listener is registered to the underlying plugin
         verify(plugin).registerListener(controllerListener)
+        // The listener is registered only for the plugin, not the date, or weather plugin.
+        verify(datePlugin, never()).registerListener(any())
+        verify(weatherPlugin, never()).registerListener(any())
     }
 
     @Test
-    fun testEarlyRegisteredListenersAreAttachedAfterConnected() {
+    fun testAddListener_earlyRegisteredListenersAreAttachedAfterConnected() {
         // GIVEN a listener that is registered before the session is created
         controller.addListener(controllerListener)
 
@@ -273,10 +320,13 @@
 
         // THEN the listener is subsequently registered
         verify(plugin).registerListener(controllerListener)
+        // The listener is registered only for the plugin, not the date, or the weather plugin.
+        verify(datePlugin, never()).registerListener(any())
+        verify(weatherPlugin, never()).registerListener(any())
     }
 
     @Test
-    fun testEmptyListIsEmittedAndNotifierRemovedAfterDisconnect() {
+    fun testDisconnect_emitsEmptyListAndRemovesNotifier() {
         // GIVEN a registered listener on an active session
         connectSession()
         clearInvocations(plugin)
@@ -288,10 +338,13 @@
         // THEN the listener receives an empty list of targets and unregisters the notifier
         verify(plugin).onTargetsAvailable(emptyList())
         verify(plugin).registerSmartspaceEventNotifier(null)
+        verify(weatherPlugin).onTargetsAvailable(emptyList())
+        verify(weatherPlugin).registerSmartspaceEventNotifier(null)
+        verify(datePlugin).registerSmartspaceEventNotifier(null)
     }
 
     @Test
-    fun testUserChangeReloadsSmartspace() {
+    fun testUserChange_reloadsSmartspace() {
         // GIVEN a connected smartspace session
         connectSession()
 
@@ -303,7 +356,7 @@
     }
 
     @Test
-    fun testSettingsChangeReloadsSmartspace() {
+    fun testSettingsChange_reloadsSmartspace() {
         // GIVEN a connected smartspace session
         connectSession()
 
@@ -315,7 +368,7 @@
     }
 
     @Test
-    fun testThemeChangeUpdatesTextColor() {
+    fun testThemeChange_updatesTextColor() {
         // GIVEN a connected smartspace session
         connectSession()
 
@@ -327,7 +380,23 @@
     }
 
     @Test
-    fun testDozeAmountChangeUpdatesView() {
+    fun testThemeChange_ifDecouplingEnabled_updatesTextColor() {
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+
+        // GIVEN a connected smartspace session
+        connectSession()
+
+        // WHEN the theme changes
+        configChangeListener.onThemeChanged()
+
+        // We update the new text color to match the wallpaper color
+        verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+        verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
+        verify(smartspaceView).setPrimaryTextColor(anyInt())
+    }
+
+    @Test
+    fun testDozeAmountChange_updatesView() {
         // GIVEN a connected smartspace session
         connectSession()
 
@@ -339,7 +408,23 @@
     }
 
     @Test
-    fun testKeyguardBypassEnabledUpdatesView() {
+    fun testDozeAmountChange_ifDecouplingEnabled_updatesViews() {
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+
+        // GIVEN a connected smartspace session
+        connectSession()
+
+        // WHEN the doze amount changes
+        statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
+
+        // We pass that along to the view
+        verify(dateSmartspaceView).setDozeAmount(0.7f)
+        verify(weatherSmartspaceView).setDozeAmount(0.7f)
+        verify(smartspaceView).setDozeAmount(0.7f)
+    }
+
+    @Test
+    fun testKeyguardBypassEnabled_updatesView() {
         // GIVEN a connected smartspace session
         connectSession()
         `when`(keyguardBypassController.bypassEnabled).thenReturn(true)
@@ -434,6 +519,213 @@
     }
 
     @Test
+    fun testSessionListener_ifDecouplingEnabled_weatherTargetIsFilteredOut() {
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+        connectSession()
+
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeTarget(2, userHandlePrimary),
+                makeTarget(3, userHandleManaged),
+                makeTarget(4, userHandlePrimary, featureType = SmartspaceTarget.FEATURE_WEATHER)
+        )
+
+        sessionListener.onTargetsAvailable(targets)
+
+        // THEN all non-sensitive content is still shown
+        verify(plugin).onTargetsAvailable(eq(listOf(targets[0], targets[1], targets[2])))
+        // No filtering is applied for the weather plugin
+        verify(weatherPlugin).onTargetsAvailable(eq(targets))
+        // No targets needed for the date plugin
+        verify(datePlugin, never()).onTargetsAvailable(any())
+    }
+
+    @Test
+    fun testSessionListener_ifWeatherExtraMissing_thenWeatherDataNotSent() {
+        connectSession()
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeTarget(2, userHandlePrimary, featureType = SmartspaceTarget.FEATURE_WEATHER)
+
+        )
+        sessionListener.onTargetsAvailable(targets)
+        verify(keyguardUpdateMonitor, times(0)).sendWeatherData(any())
+    }
+
+    @Test
+    fun testSessionListener_ifWeatherExtraIsMissingValues_thenWeatherDataNotSent() {
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeWeatherTargetWithExtras(
+                        id = 2,
+                        userHandle = userHandlePrimary,
+                        description = null,
+                        state = WeatherData.WeatherStateIcon.SUNNY.id,
+                        temperature = "32",
+                        useCelsius = null)
+
+        )
+
+        sessionListener.onTargetsAvailable(targets)
+
+        verify(keyguardUpdateMonitor, times(0)).sendWeatherData(any())
+    }
+
+    @Test
+    fun testSessionListener_ifTooEarly_thenWeatherDataNotSent() {
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_TOO_EARLY)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeWeatherTargetWithExtras(
+                        id = 1,
+                        userHandle = userHandleManaged,
+                        description = "Sunny",
+                        state = WeatherData.WeatherStateIcon.SUNNY.id,
+                        temperature = "32",
+                        useCelsius = false)
+        )
+        sessionListener.onTargetsAvailable(targets)
+        verify(keyguardUpdateMonitor, times(0)).sendWeatherData(any())
+    }
+
+    @Test
+    fun testSessionListener_ifOnTime_thenWeatherDataSent() {
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeWeatherTargetWithExtras(
+                        id = 1,
+                        userHandle = userHandleManaged,
+                        description = "Snow Showers",
+                        state = WeatherData.WeatherStateIcon.SNOW_SHOWERS_SNOW.id,
+                        temperature = "-1",
+                        useCelsius = false)
+        )
+        sessionListener.onTargetsAvailable(targets)
+        verify(keyguardUpdateMonitor).sendWeatherData(argThat { w ->
+            w.description == "Snow Showers" &&
+                    w.state == WeatherData.WeatherStateIcon.SNOW_SHOWERS_SNOW &&
+                    w.temperature == -1 && !w.useCelsius
+        })
+    }
+
+    @Test
+    fun testSessionListener_ifTooLate_thenWeatherDataNotSent() {
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_TOO_LATE)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeWeatherTargetWithExtras(
+                        id = 1,
+                        userHandle = userHandleManaged,
+                        description = "Sunny",
+                        state = WeatherData.WeatherStateIcon.SUNNY.id,
+                        temperature = "72",
+                        useCelsius = false)
+        )
+        sessionListener.onTargetsAvailable(targets)
+        verify(keyguardUpdateMonitor, times(0)).sendWeatherData(any())
+    }
+
+    @Test
+    fun testSessionListener_onlyFirstWeatherDataSent() {
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeWeatherTargetWithExtras(
+                        id = 1,
+                        userHandle = userHandleManaged,
+                        description = "Sunny",
+                        state = WeatherData.WeatherStateIcon.SUNNY.id,
+                        temperature = "72",
+                        useCelsius = false),
+                makeWeatherTargetWithExtras(
+                        id = 2,
+                        userHandle = userHandleManaged,
+                        description = "Showers",
+                        state = WeatherData.WeatherStateIcon.SHOWERS_RAIN.id,
+                        temperature = "62",
+                        useCelsius = true)
+        )
+        sessionListener.onTargetsAvailable(targets)
+        verify(keyguardUpdateMonitor).sendWeatherData(argThat { w ->
+            w.description == "Sunny" &&
+                    w.state == WeatherData.WeatherStateIcon.SUNNY &&
+                    w.temperature == 72 && !w.useCelsius
+        })
+    }
+
+    @Test
+    fun testSessionListener_ifDecouplingEnabled_weatherDataUpdates() {
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeTarget(1, userHandlePrimary, isSensitive = true),
+                makeTarget(2, userHandlePrimary),
+                makeTarget(3, userHandleManaged),
+                makeWeatherTargetWithExtras(
+                        id = 4,
+                        userHandle = userHandlePrimary,
+                        description = "Flurries",
+                        state = WeatherData.WeatherStateIcon.FLURRIES.id,
+                        temperature = "0",
+                        useCelsius = true)
+        )
+
+        sessionListener.onTargetsAvailable(targets)
+
+        verify(keyguardUpdateMonitor).sendWeatherData(argThat { w ->
+            w.description == "Flurries" &&
+                    w.state == WeatherData.WeatherStateIcon.FLURRIES &&
+                    w.temperature == 0 && w.useCelsius
+        })
+    }
+
+    @Test
+    fun testSessionListener_ifDecouplingDisabled_weatherDataUpdates() {
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
+        connectSession()
+
+        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
+        // WHEN we receive a list of targets
+        val targets = listOf(
+                makeWeatherTargetWithExtras(
+                        id = 1,
+                        userHandle = userHandlePrimary,
+                        description = "Sunny",
+                        state = WeatherData.WeatherStateIcon.SUNNY.id,
+                        temperature = "32",
+                        useCelsius = false),
+                makeTarget(2, userHandlePrimary, isSensitive = true)
+        )
+
+        sessionListener.onTargetsAvailable(targets)
+
+        verify(keyguardUpdateMonitor).sendWeatherData(argThat { w ->
+            w.description == "Sunny" &&
+                    w.state == WeatherData.WeatherStateIcon.SUNNY &&
+                    w.temperature == 32 && !w.useCelsius
+        })
+    }
+
+    @Test
     fun testSettingsAreReloaded() {
         // GIVEN a connected session where the privacy settings later flip to false
         connectSession()
@@ -520,6 +812,17 @@
         verify(smartspaceManager, never()).createSmartspaceSession(any())
         verify(smartspaceView2).setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
         verify(smartspaceView2).registerDataProvider(plugin)
+        verify(smartspaceView2).registerConfigProvider(configPlugin)
+    }
+
+    @Test
+    fun testWeatherViewUsesSameSession() {
+        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+        // GIVEN a connected session
+        connectSession()
+
+        // No checks is needed here, since connectSession() already checks internally that
+        // createSmartspaceSession is invoked only once.
     }
 
     @Test
@@ -539,8 +842,8 @@
     fun testConnectAttemptBeforeInitializationShouldNotCreateSession() {
         // GIVEN an uninitalized smartspaceView
         // WHEN the device is provisioned
-        `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
-        `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true)
+        `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+        `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
         deviceProvisionedListener.onDeviceProvisionedChanged()
 
         // THEN no calls to createSmartspaceSession should occur
@@ -550,17 +853,46 @@
     }
 
     private fun connectSession() {
+        if (controller.isDateWeatherDecoupled()) {
+            val dateView = controller.buildAndConnectDateView(fakeParent)
+            dateSmartspaceView = dateView as SmartspaceView
+            fakeParent.addView(dateView)
+            controller.stateChangeListener.onViewAttachedToWindow(dateView)
+
+            verify(dateSmartspaceView).setUiSurface(
+                    BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+            verify(dateSmartspaceView).registerDataProvider(datePlugin)
+
+            verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+            verify(dateSmartspaceView).setDozeAmount(0.5f)
+
+            val weatherView = controller.buildAndConnectWeatherView(fakeParent)
+            weatherSmartspaceView = weatherView as SmartspaceView
+            fakeParent.addView(weatherView)
+            controller.stateChangeListener.onViewAttachedToWindow(weatherView)
+
+            verify(weatherSmartspaceView).setUiSurface(
+                    BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+            verify(weatherSmartspaceView).registerDataProvider(weatherPlugin)
+
+            verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
+            verify(weatherSmartspaceView).setDozeAmount(0.5f)
+        }
+
         val view = controller.buildAndConnectView(fakeParent)
         smartspaceView = view as SmartspaceView
-
+        fakeParent.addView(view)
         controller.stateChangeListener.onViewAttachedToWindow(view)
 
         verify(smartspaceView).setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
         verify(smartspaceView).registerDataProvider(plugin)
+        verify(smartspaceView).registerConfigProvider(configPlugin)
         verify(smartspaceSession)
                 .addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor))
         sessionListener = sessionListenerCaptor.value
 
+        verify(smartspaceManager).createSmartspaceSession(any())
+
         verify(userTracker).addCallback(capture(userTrackerCaptor), any())
         userListener = userTrackerCaptor.value
 
@@ -585,9 +917,12 @@
 
         verify(smartspaceView).setPrimaryTextColor(anyInt())
         verify(smartspaceView).setDozeAmount(0.5f)
-        clearInvocations(view)
 
-        fakeParent.addView(view)
+        if (controller.isDateWeatherDecoupled()) {
+            clearInvocations(dateSmartspaceView)
+            clearInvocations(weatherSmartspaceView)
+        }
+        clearInvocations(smartspaceView)
     }
 
     private fun setActiveUser(userHandle: UserHandle) {
@@ -602,7 +937,7 @@
         return userInfo
     }
 
-    fun makeTarget(
+    private fun makeTarget(
         id: Int,
         userHandle: UserHandle,
         isSensitive: Boolean = false,
@@ -617,6 +952,38 @@
                 .build()
     }
 
+    private fun makeWeatherTargetWithExtras(
+            id: Int,
+            userHandle: UserHandle,
+            description: String?,
+            state: Int?,
+            temperature: String?,
+            useCelsius: Boolean?
+    ): SmartspaceTarget {
+        val mockWeatherBundle = mock(Bundle::class.java).apply {
+            `when`(getString(WeatherData.DESCRIPTION_KEY)).thenReturn(description)
+            if (state != null)
+                `when`(getInt(eq(WeatherData.STATE_KEY), any())).thenReturn(state)
+            `when`(getString(WeatherData.TEMPERATURE_KEY)).thenReturn(temperature)
+            `when`(containsKey(WeatherData.USE_CELSIUS_KEY)).thenReturn(useCelsius != null)
+            if (useCelsius != null)
+                `when`(getBoolean(WeatherData.USE_CELSIUS_KEY)).thenReturn(useCelsius)
+        }
+
+        val mockBaseAction = mock(SmartspaceAction::class.java)
+        `when`(mockBaseAction.extras).thenReturn(mockWeatherBundle)
+        return SmartspaceTarget.Builder(
+                "targetWithWeatherExtras$id",
+                ComponentName("testpackage", "testclass$id"),
+                userHandle)
+                .setSensitive(false)
+                .setFeatureType(SmartspaceTarget.FEATURE_WEATHER)
+                .setBaseAction(mockBaseAction)
+                .setExpiryTimeMillis(SMARTSPACE_EXPIRY_TIME)
+                .setCreationTimeMillis(SMARTSPACE_CREATION_TIME)
+                .build()
+    }
+
     private fun setAllowPrivateNotifications(user: UserHandle, value: Boolean) {
         `when`(secureSettings.getIntForUser(
                 eq(PRIVATE_LOCKSCREEN_SETTING),
@@ -633,7 +1000,8 @@
         ).thenReturn(if (value) 1 else 0)
     }
 
-    private fun createSmartspaceView(): SmartspaceView {
+    // Separate function for the date view, which implements a specific subset of all functions.
+    private fun createDateSmartspaceView(): SmartspaceView {
         return spy(object : View(context), SmartspaceView {
             override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
             }
@@ -641,7 +1009,56 @@
             override fun setPrimaryTextColor(color: Int) {
             }
 
-            override fun setIsDreaming(isDreaming: Boolean) {
+            override fun setUiSurface(uiSurface: String) {
+            }
+
+            override fun setDozeAmount(amount: Float) {
+            }
+
+            override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+            }
+
+            override fun setFalsingManager(falsingManager: FalsingManager?) {
+            }
+
+            override fun setDnd(image: Drawable?, description: String?) {
+            }
+
+            override fun setNextAlarm(image: Drawable?, description: String?) {
+            }
+        })
+    }
+    // Separate function for the weather view, which implements a specific subset of all functions.
+    private fun createWeatherSmartspaceView(): SmartspaceView {
+        return spy(object : View(context), SmartspaceView {
+            override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+            }
+
+            override fun setPrimaryTextColor(color: Int) {
+            }
+
+            override fun setUiSurface(uiSurface: String) {
+            }
+
+            override fun setDozeAmount(amount: Float) {
+            }
+
+            override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+            }
+
+            override fun setFalsingManager(falsingManager: FalsingManager?) {
+            }
+        })
+    }
+    private fun createSmartspaceView(): SmartspaceView {
+        return spy(object : View(context), SmartspaceView {
+            override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+            }
+
+            override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {
+            }
+
+            override fun setPrimaryTextColor(color: Int) {
             }
 
             override fun setUiSurface(uiSurface: String) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt
new file mode 100644
index 0000000..7a67796
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.notification
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogcatEchoTracker
+import com.android.systemui.statusbar.StatusBarState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class NotificationWakeUpCoordinatorLoggerTest : SysuiTestCase() {
+
+    private val logBufferCounter = LogBufferCounter()
+    private lateinit var logger: NotificationWakeUpCoordinatorLogger
+
+    private fun verifyDidLog(times: Int) {
+        logBufferCounter.verifyDidLog(times)
+    }
+
+    @Before
+    fun setup() {
+        logger = NotificationWakeUpCoordinatorLogger(logBufferCounter.logBuffer)
+    }
+
+    @Test
+    fun setDozeAmountWillThrottleFractionalUpdates() {
+        logger.logSetDozeAmount(0f, 0f, "source1", StatusBarState.SHADE, changed = false)
+        verifyDidLog(1)
+        logger.logSetDozeAmount(0.1f, 0.1f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(1)
+        logger.logSetDozeAmount(0.2f, 0.2f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.3f, 0.3f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.4f, 0.4f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(0)
+        logger.logSetDozeAmount(1f, 1f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(1)
+    }
+
+    @Test
+    fun setDozeAmountWillIncludeFractionalUpdatesWhenStateChanges() {
+        logger.logSetDozeAmount(0f, 0f, "source1", StatusBarState.SHADE, changed = false)
+        verifyDidLog(1)
+        logger.logSetDozeAmount(0.1f, 0.1f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(1)
+        logger.logSetDozeAmount(0.2f, 0.2f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.3f, 0.3f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.4f, 0.4f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(0)
+        logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.KEYGUARD, changed = false)
+        verifyDidLog(1)
+    }
+
+    @Test
+    fun setDozeAmountWillIncludeFractionalUpdatesWhenSourceChanges() {
+        logger.logSetDozeAmount(0f, 0f, "source1", StatusBarState.SHADE, changed = false)
+        verifyDidLog(1)
+        logger.logSetDozeAmount(0.1f, 0.1f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(1)
+        logger.logSetDozeAmount(0.2f, 0.2f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.3f, 0.3f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.4f, 0.4f, "source1", StatusBarState.SHADE, changed = true)
+        logger.logSetDozeAmount(0.5f, 0.5f, "source1", StatusBarState.SHADE, changed = true)
+        verifyDidLog(0)
+        logger.logSetDozeAmount(0.5f, 0.5f, "source2", StatusBarState.SHADE, changed = false)
+        verifyDidLog(1)
+    }
+
+    class LogBufferCounter {
+        val recentLogs = mutableListOf<Pair<String, LogLevel>>()
+        val tracker =
+            object : LogcatEchoTracker {
+                override val logInBackgroundThread: Boolean = false
+                override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean = false
+                override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
+                    recentLogs.add(tagName to level)
+                    return true
+                }
+            }
+        val logBuffer =
+            LogBuffer(name = "test", maxSize = 1, logcatEchoTracker = tracker, systrace = false)
+
+        fun verifyDidLog(times: Int) {
+            assertThat(recentLogs).hasSize(times)
+            recentLogs.clear()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
new file mode 100644
index 0000000..95591a4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.notification
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class NotificationWakeUpCoordinatorTest : SysuiTestCase() {
+
+    private val dumpManager: DumpManager = mock()
+    private val headsUpManager: HeadsUpManager = mock()
+    private val statusBarStateController: StatusBarStateController = mock()
+    private val bypassController: KeyguardBypassController = mock()
+    private val dozeParameters: DozeParameters = mock()
+    private val screenOffAnimationController: ScreenOffAnimationController = mock()
+    private val logger: NotificationWakeUpCoordinatorLogger = mock()
+    private val stackScrollerController: NotificationStackScrollLayoutController = mock()
+
+    private lateinit var notificationWakeUpCoordinator: NotificationWakeUpCoordinator
+    private lateinit var statusBarStateCallback: StatusBarStateController.StateListener
+    private lateinit var bypassChangeCallback: KeyguardBypassController.OnBypassStateChangedListener
+
+    private var bypassEnabled: Boolean = false
+    private var statusBarState: Int = StatusBarState.KEYGUARD
+    private var dozeAmount: Float = 0f
+
+    private fun setBypassEnabled(enabled: Boolean) {
+        bypassEnabled = enabled
+        bypassChangeCallback.onBypassStateChanged(enabled)
+    }
+
+    private fun setStatusBarState(state: Int) {
+        statusBarState = state
+        statusBarStateCallback.onStateChanged(state)
+    }
+
+    private fun setDozeAmount(dozeAmount: Float) {
+        this.dozeAmount = dozeAmount
+        statusBarStateCallback.onDozeAmountChanged(dozeAmount, dozeAmount)
+    }
+
+    @Before
+    fun setup() {
+        whenever(bypassController.bypassEnabled).then { bypassEnabled }
+        whenever(statusBarStateController.state).then { statusBarState }
+        notificationWakeUpCoordinator =
+            NotificationWakeUpCoordinator(
+                dumpManager,
+                headsUpManager,
+                statusBarStateController,
+                bypassController,
+                dozeParameters,
+                screenOffAnimationController,
+                logger,
+            )
+        statusBarStateCallback = withArgCaptor {
+            verify(statusBarStateController).addCallback(capture())
+        }
+        bypassChangeCallback = withArgCaptor {
+            verify(bypassController).registerOnBypassStateChangedListener(capture())
+        }
+        notificationWakeUpCoordinator.setStackScroller(stackScrollerController)
+    }
+
+    @Test
+    fun setDozeToOneWillFullyHideNotifications() {
+        setDozeAmount(1f)
+        verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f)
+        assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue()
+    }
+
+    @Test
+    fun setDozeToZeroWillFullyShowNotifications() {
+        setDozeAmount(0f)
+        verifyStackScrollerDozeAndHideAmount(dozeAmount = 0f, hideAmount = 0f)
+        assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse()
+    }
+
+    @Test
+    fun setDozeToOneThenZeroWillFullyShowNotifications() {
+        setDozeToOneWillFullyHideNotifications()
+        clearInvocations(stackScrollerController)
+        setDozeToZeroWillFullyShowNotifications()
+    }
+
+    @Test
+    fun setDozeToHalfWillHalfShowNotifications() {
+        setDozeAmount(0.5f)
+        verifyStackScrollerDozeAndHideAmount(dozeAmount = 0.5f, hideAmount = 0.5f)
+        assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse()
+    }
+
+    @Test
+    fun setDozeToZeroWithBypassWillFullyHideNotifications() {
+        bypassEnabled = true
+        setDozeAmount(0f)
+        verifyStackScrollerDozeAndHideAmount(dozeAmount = 01f, hideAmount = 1f)
+        assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue()
+    }
+
+    @Test
+    fun disablingBypassWillShowNotifications() {
+        setDozeToZeroWithBypassWillFullyHideNotifications()
+        clearInvocations(stackScrollerController)
+        setBypassEnabled(false)
+        verifyStackScrollerDozeAndHideAmount(dozeAmount = 0f, hideAmount = 0f)
+        assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse()
+    }
+
+    @Test
+    fun switchingToShadeWithBypassEnabledWillShowNotifications() {
+        setDozeToZeroWithBypassWillFullyHideNotifications()
+        clearInvocations(stackScrollerController)
+        setStatusBarState(StatusBarState.SHADE)
+        verifyStackScrollerDozeAndHideAmount(dozeAmount = 0f, hideAmount = 0f)
+        assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse()
+        assertThat(notificationWakeUpCoordinator.statusBarState).isEqualTo(StatusBarState.SHADE)
+    }
+
+    private fun verifyStackScrollerDozeAndHideAmount(dozeAmount: Float, hideAmount: Float) {
+        // First verify that we did in-fact receive the correct values
+        verify(stackScrollerController).setDozeAmount(dozeAmount)
+        verify(stackScrollerController).setHideAmount(hideAmount, hideAmount)
+        // Now verify that there was just this ONE call to each of these methods
+        verify(stackScrollerController).setDozeAmount(anyFloat())
+        verify(stackScrollerController).setHideAmount(anyFloat(), anyFloat())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 94e3e6c..edb2965 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -105,6 +105,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
+import org.mockito.stubbing.Answer;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -376,6 +377,90 @@
     }
 
     @Test
+    public void testScheduleBuildNotificationListWhenChannelChanged() {
+        // GIVEN
+        final NotificationEntryBuilder neb = buildNotif(TEST_PACKAGE, 48);
+        final NotificationChannel channel = new NotificationChannel(
+                "channelId",
+                "channelName",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        neb.setChannel(channel);
+
+        final NotifEvent notif = mNoMan.postNotif(neb);
+        final NotificationEntry entry = mCollectionListener.getEntry(notif.key);
+
+        when(mMainHandler.hasCallbacks(any())).thenReturn(false);
+
+        clearInvocations(mBuildListener);
+
+        // WHEN
+        mNotifHandler.onNotificationChannelModified(TEST_PACKAGE,
+                entry.getSbn().getUser(), channel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
+
+        // THEN
+        verify(mMainHandler).postDelayed(any(), eq(1000L));
+    }
+
+    @Test
+    public void testCancelScheduledBuildNotificationListEventWhenNotifUpdatedSynchronously() {
+        // GIVEN
+        final NotificationEntry entry1 = buildNotif(TEST_PACKAGE, 1)
+                .setGroup(mContext, "group_1")
+                .build();
+        final NotificationEntry entry2 = buildNotif(TEST_PACKAGE, 2)
+                .setGroup(mContext, "group_1")
+                .setContentTitle(mContext, "New version")
+                .build();
+        final NotificationEntry entry3 = buildNotif(TEST_PACKAGE, 3)
+                .setGroup(mContext, "group_1")
+                .build();
+
+        final List<CoalescedEvent> entriesToBePosted = Arrays.asList(
+                new CoalescedEvent(entry1.getKey(), 0, entry1.getSbn(), entry1.getRanking(), null),
+                new CoalescedEvent(entry2.getKey(), 1, entry2.getSbn(), entry2.getRanking(), null),
+                new CoalescedEvent(entry3.getKey(), 2, entry3.getSbn(), entry3.getRanking(), null)
+        );
+
+        when(mMainHandler.hasCallbacks(any())).thenReturn(true);
+
+        // WHEN
+        mNotifHandler.onNotificationBatchPosted(entriesToBePosted);
+
+        // THEN
+        verify(mMainHandler).removeCallbacks(any());
+    }
+
+    @Test
+    public void testBuildNotificationListWhenChannelChanged() {
+        // GIVEN
+        final NotificationEntryBuilder neb = buildNotif(TEST_PACKAGE, 48);
+        final NotificationChannel channel = new NotificationChannel(
+                "channelId",
+                "channelName",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        neb.setChannel(channel);
+
+        final NotifEvent notif = mNoMan.postNotif(neb);
+        final NotificationEntry entry = mCollectionListener.getEntry(notif.key);
+
+        when(mMainHandler.hasCallbacks(any())).thenReturn(false);
+        when(mMainHandler.postDelayed(any(), eq(1000L))).thenAnswer((Answer) invocation -> {
+            final Runnable runnable = invocation.getArgument(0);
+            runnable.run();
+            return null;
+        });
+
+        clearInvocations(mBuildListener);
+
+        // WHEN
+        mNotifHandler.onNotificationChannelModified(TEST_PACKAGE,
+                entry.getSbn().getUser(), channel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
+
+        // THEN
+        verifyBuiltList(List.of(entry));
+    }
+
+    @Test
     public void testRankingsAreUpdatedForOtherNotifs() {
         // GIVEN a collection with one notif
         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 09f8a10..a869038 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -39,7 +39,6 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -137,7 +136,6 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         allowTestableLooperAsMainThread();
-        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
 
         mListBuilder = new ShadeListBuilder(
                 mDumpManager,
@@ -1998,29 +1996,7 @@
     }
 
     @Test
-    public void testActiveOrdering_withLegacyStability() {
-        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
-        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
-        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
-    }
-
-    @Test
-    public void testStableOrdering_withLegacyStability() {
-        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
-        mStabilityManager.setAllowEntryReordering(false);
-        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
-        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
-    }
-
-    @Test
     public void testStableOrdering() {
-        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
         mStabilityManager.setAllowEntryReordering(false);
         // No input or output
         assertOrder("", "", "", true);
@@ -2076,7 +2052,6 @@
 
     @Test
     public void testActiveOrdering() {
-        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
         assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
         assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
         assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
@@ -2133,7 +2108,6 @@
 
     @Test
     public void stableOrderingDisregardedWithSectionChange() {
-        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
         // GIVEN the first sectioner's packages can be changed from run-to-run
         List<String> mutableSectionerPackages = new ArrayList<>();
         mutableSectionerPackages.add(PACKAGE_1);
@@ -2229,49 +2203,7 @@
     }
 
     @Test
-    public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
-        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
-
-        // GIVEN a notification group is on screen
-        mStabilityManager.setAllowEntryReordering(false);
-
-        // WHEN the list is originally built with reordering disabled (and section changes allowed)
-        addNotif(0, PACKAGE_1).setRank(2);
-        addNotif(1, PACKAGE_1).setRank(3);
-        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
-        addGroupChild(3, PACKAGE_1, "group").setRank(5);
-        addGroupChild(4, PACKAGE_1, "group").setRank(6);
-        dispatchBuild();
-
-        verifyBuiltList(
-                notif(0),
-                notif(1),
-                group(
-                        summary(2),
-                        child(3),
-                        child(4)
-                )
-        );
-
-        // WHEN the notification summary rank increases and children removed
-        setNewRank(notif(2).entry, 1);
-        mEntrySet.remove(4);
-        mEntrySet.remove(3);
-        dispatchBuild();
-
-        // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
-        // despite visual stability being active
-        verifyBuiltList(
-                notif(2),
-                notif(0),
-                notif(1)
-        );
-    }
-
-    @Test
     public void groupRevertingToSummaryRetainsStablePosition() {
-        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
-
         // GIVEN a notification group is on screen
         mStabilityManager.setAllowEntryReordering(false);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
index 8275c0c..9b3626b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
@@ -127,6 +127,6 @@
                 NotificationManager.IMPORTANCE_DEFAULT,
                 null, null,
                 null, null, null, true, 0, false, -1, false, null, null, false, false,
-                false, null, 0, false)
+                false, null, 0, false, 0)
     }
 }
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 be6b1dc..8109e24 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
@@ -23,6 +23,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.advanceTimeBy
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
@@ -38,6 +39,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 +66,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 +94,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 +118,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 +175,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 +199,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 +225,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 +278,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()
@@ -270,16 +312,19 @@
     fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is showing, unseen notification is present
+        // GIVEN: Keyguard is showing, not dozing, unseen notification is present
         keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setDozing(false)
         runKeyguardCoordinatorTest {
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
 
-            // WHEN: Keyguard is no longer showing for 5 seconds
-            keyguardRepository.setKeyguardShowing(false)
+            // WHEN: five seconds have passed
+            testScheduler.advanceTimeBy(5.seconds)
             testScheduler.runCurrent()
-            testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds)
+
+            // WHEN: Keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
             testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
@@ -292,7 +337,7 @@
     }
 
     @Test
-    fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() {
+    fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
         // GIVEN: Keyguard is showing, unseen notification is present
@@ -301,10 +346,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)
@@ -321,12 +364,13 @@
         val testDispatcher = UnconfinedTestDispatcher()
         val testScope = TestScope(testDispatcher)
         val fakeSettings = FakeSettings().apply {
-            putBool(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, true)
+            putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
         }
         val seenNotificationsProvider = SeenNotificationsProviderImpl()
         val keyguardCoordinator =
             KeyguardCoordinator(
                 testDispatcher,
+                headsUpManager,
                 keyguardNotifVisibilityProvider,
                 keyguardRepository,
                 notifPipelineFlags,
@@ -364,22 +408,31 @@
         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.getBoolForUser(
+                fakeSettings.getIntForUser(
                     Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
                     UserHandle.USER_CURRENT,
-                )
+                ) == 1
             set(value) {
-                fakeSettings.putBoolForUser(
+                fakeSettings.putIntForUser(
                     Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-                    value,
+                    if (value) 1 else 2,
                     UserHandle.USER_CURRENT,
                 )
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 601771d..fbec95b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -41,6 +41,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.PendingIntent;
@@ -59,6 +60,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -107,6 +109,8 @@
     UiEventLoggerFake mUiEventLoggerFake;
     @Mock
     PendingIntent mPendingIntent;
+    @Mock
+    UserTracker mUserTracker;
 
     private NotificationInterruptStateProviderImpl mNotifInterruptionStateProvider;
 
@@ -114,6 +118,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
         mUiEventLoggerFake = new UiEventLoggerFake();
 
@@ -131,7 +136,8 @@
                         mMockHandler,
                         mFlags,
                         mKeyguardNotificationVisibilityProvider,
-                        mUiEventLoggerFake);
+                        mUiEventLoggerFake,
+                        mUserTracker);
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
@@ -735,7 +741,7 @@
     }
 
     @Test
-    public void testShouldFullScreen_snoozed_occluding_withStrictRules() throws Exception {
+    public void testShouldNotFullScreen_snoozed_occluding_withStrictRules() throws Exception {
         when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
@@ -747,16 +753,41 @@
         when(mKeyguardStateController.isOccluded()).thenReturn(true);
 
         assertThat(mNotifInterruptionStateProvider.getFullScreenIntentDecision(entry))
-                .isEqualTo(FullScreenIntentDecision.FSI_KEYGUARD_OCCLUDED);
+                .isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
         assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
-                .isTrue();
-        verify(mLogger, never()).logNoFullscreen(any(), any());
+                .isFalse();
+        verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
         verify(mLogger, never()).logNoFullscreenWarning(any(), any());
-        verify(mLogger).logFullscreen(entry, "Expected not to HUN while keyguard occluded");
+        verify(mLogger, never()).logFullscreen(any(), any());
     }
 
     @Test
-    public void testShouldFullScreen_snoozed_lockedShade_withStrictRules() throws Exception {
+    public void testShouldHeadsUp_snoozed_occluding_withStrictRules() throws Exception {
+        when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
+        NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+        when(mPowerManager.isInteractive()).thenReturn(true);
+        when(mPowerManager.isScreenOn()).thenReturn(true);
+        when(mDreamManager.isDreaming()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(SHADE);
+        when(mHeadsUpManager.isSnoozed("a")).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();
+
+        verify(mLogger).logHeadsUpPackageSnoozeBypassedHasFsi(entry);
+        verify(mLogger, never()).logHeadsUp(any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
+    }
+
+    @Test
+    public void testShouldNotFullScreen_snoozed_lockedShade_withStrictRules() throws Exception {
         when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
@@ -768,12 +799,37 @@
         when(mKeyguardStateController.isOccluded()).thenReturn(false);
 
         assertThat(mNotifInterruptionStateProvider.getFullScreenIntentDecision(entry))
-                .isEqualTo(FullScreenIntentDecision.FSI_LOCKED_SHADE);
+                .isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
         assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
-                .isTrue();
-        verify(mLogger, never()).logNoFullscreen(any(), any());
+                .isFalse();
+        verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
         verify(mLogger, never()).logNoFullscreenWarning(any(), any());
-        verify(mLogger).logFullscreen(entry, "Keyguard is showing and not occluded");
+        verify(mLogger, never()).logFullscreen(any(), any());
+    }
+
+    @Test
+    public void testShouldHeadsUp_snoozed_lockedShade_withStrictRules() throws Exception {
+        when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
+        NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+        when(mPowerManager.isInteractive()).thenReturn(true);
+        when(mPowerManager.isScreenOn()).thenReturn(true);
+        when(mDreamManager.isDreaming()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(SHADE_LOCKED);
+        when(mHeadsUpManager.isSnoozed("a")).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
+
+        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();
+
+        verify(mLogger).logHeadsUpPackageSnoozeBypassedHasFsi(entry);
+        verify(mLogger, never()).logHeadsUp(any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     @Test
@@ -789,21 +845,41 @@
         when(mKeyguardStateController.isOccluded()).thenReturn(false);
 
         assertThat(mNotifInterruptionStateProvider.getFullScreenIntentDecision(entry))
-                .isEqualTo(FullScreenIntentDecision.NO_FSI_NO_HUN_OR_KEYGUARD);
+                .isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
         assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
                 .isFalse();
-        verify(mLogger, never()).logNoFullscreen(any(), any());
-        verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
+        verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+        verify(mLogger, never()).logNoFullscreenWarning(any(), any());
         verify(mLogger, never()).logFullscreen(any(), any());
+    }
+
+    @Test
+    public void testShouldHeadsUp_snoozed_unlocked_withStrictRules() throws Exception {
+        when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
+        NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+        when(mPowerManager.isInteractive()).thenReturn(true);
+        when(mPowerManager.isScreenOn()).thenReturn(true);
+        when(mDreamManager.isDreaming()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(SHADE);
+        when(mHeadsUpManager.isSnoozed("a")).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
+
+        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();
+
+        verify(mLogger).logHeadsUpPackageSnoozeBypassedHasFsi(entry);
+        verify(mLogger, never()).logHeadsUp(any());
 
         assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
         UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
         assertThat(fakeUiEvent.eventId).isEqualTo(
-                NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId());
+                NotificationInterruptEvent.HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI.getId());
         assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
         assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
+    /* TODO: Verify the FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD UiEvent some other way. */
+
     /**
      * Bubbles can happen.
      */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
index 33b94e3..33a838e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
@@ -20,6 +20,7 @@
 import android.app.StatsManager
 import android.graphics.Bitmap
 import android.graphics.drawable.Icon
+import android.stats.sysui.NotificationEnums
 import android.testing.AndroidTestingRunner
 import android.util.StatsEvent
 import androidx.test.filters.SmallTest
@@ -31,9 +32,12 @@
 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.Expect
 import com.google.common.truth.Truth.assertThat
+import java.lang.RuntimeException
 import kotlinx.coroutines.Dispatchers
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -44,6 +48,8 @@
 @RunWith(AndroidTestingRunner::class)
 class NotificationMemoryLoggerTest : SysuiTestCase() {
 
+    @Rule @JvmField val expect = Expect.create()
+
     private val bgExecutor = FakeExecutor(FakeSystemClock())
     private val immediate = Dispatchers.Main.immediate
 
@@ -113,6 +119,141 @@
         assertThat(data).hasSize(2)
     }
 
+    @Test
+    fun onPullAtom_throwsInterruptedException_failsGracefully() {
+        val pipeline: NotifPipeline = mock()
+        whenever(pipeline.allNotifs).thenAnswer { throw InterruptedException("Timeout") }
+        val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+            .isEqualTo(StatsManager.PULL_SKIP)
+    }
+
+    @Test
+    fun onPullAtom_throwsRuntimeException_failsGracefully() {
+        val pipeline: NotifPipeline = mock()
+        whenever(pipeline.allNotifs).thenThrow(RuntimeException("Something broke!"))
+        val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+            .isEqualTo(StatsManager.PULL_SKIP)
+    }
+
+    @Test
+    fun aggregateMemoryUsageData_returnsCorrectlyAggregatedSamePackageData() {
+        val usage = getPresetMemoryUsages()
+        val aggregateUsage = aggregateMemoryUsageData(usage)
+
+        assertThat(aggregateUsage).hasSize(3)
+        assertThat(aggregateUsage)
+            .containsKey(Pair("package 1", NotificationEnums.STYLE_BIG_PICTURE))
+
+        // Aggregated fields
+        val aggregatedData =
+            aggregateUsage[Pair("package 1", NotificationEnums.STYLE_BIG_PICTURE)]!!
+        val presetUsage1 = usage[0]
+        val presetUsage2 = usage[1]
+        assertAggregatedData(
+            aggregatedData,
+            2,
+            2,
+            smallIconObject =
+                presetUsage1.objectUsage.smallIcon + presetUsage2.objectUsage.smallIcon,
+            smallIconBitmapCount = 2,
+            largeIconObject =
+                presetUsage1.objectUsage.largeIcon + presetUsage2.objectUsage.largeIcon,
+            largeIconBitmapCount = 2,
+            bigPictureObject =
+                presetUsage1.objectUsage.bigPicture + presetUsage2.objectUsage.bigPicture,
+            bigPictureBitmapCount = 2,
+            extras = presetUsage1.objectUsage.extras + presetUsage2.objectUsage.extras,
+            extenders = presetUsage1.objectUsage.extender + presetUsage2.objectUsage.extender,
+            // Only totals need to be summarized.
+            smallIconViews =
+                presetUsage1.viewUsage[0].smallIcon + presetUsage2.viewUsage[0].smallIcon,
+            largeIconViews =
+                presetUsage1.viewUsage[0].largeIcon + presetUsage2.viewUsage[0].largeIcon,
+            systemIconViews =
+                presetUsage1.viewUsage[0].systemIcons + presetUsage2.viewUsage[0].systemIcons,
+            styleViews = presetUsage1.viewUsage[0].style + presetUsage2.viewUsage[0].style,
+            customViews =
+                presetUsage1.viewUsage[0].customViews + presetUsage2.viewUsage[0].customViews,
+            softwareBitmaps =
+                presetUsage1.viewUsage[0].softwareBitmapsPenalty +
+                    presetUsage2.viewUsage[0].softwareBitmapsPenalty,
+            seenCount = 0
+        )
+    }
+
+    @Test
+    fun aggregateMemoryUsageData_correctlySeparatesDifferentStyles() {
+        val usage = getPresetMemoryUsages()
+        val aggregateUsage = aggregateMemoryUsageData(usage)
+
+        assertThat(aggregateUsage).hasSize(3)
+        assertThat(aggregateUsage)
+            .containsKey(Pair("package 1", NotificationEnums.STYLE_BIG_PICTURE))
+        assertThat(aggregateUsage).containsKey(Pair("package 1", NotificationEnums.STYLE_BIG_TEXT))
+
+        // Different style should be separate
+        val separateStyleData =
+            aggregateUsage[Pair("package 1", NotificationEnums.STYLE_BIG_TEXT)]!!
+        val presetUsage = usage[2]
+        assertAggregatedData(
+            separateStyleData,
+            1,
+            1,
+            presetUsage.objectUsage.smallIcon,
+            1,
+            presetUsage.objectUsage.largeIcon,
+            1,
+            presetUsage.objectUsage.bigPicture,
+            1,
+            presetUsage.objectUsage.extras,
+            presetUsage.objectUsage.extender,
+            presetUsage.viewUsage[0].smallIcon,
+            presetUsage.viewUsage[0].largeIcon,
+            presetUsage.viewUsage[0].systemIcons,
+            presetUsage.viewUsage[0].style,
+            presetUsage.viewUsage[0].customViews,
+            presetUsage.viewUsage[0].softwareBitmapsPenalty,
+            0
+        )
+    }
+
+    @Test
+    fun aggregateMemoryUsageData_correctlySeparatesDifferentProcess() {
+        val usage = getPresetMemoryUsages()
+        val aggregateUsage = aggregateMemoryUsageData(usage)
+
+        assertThat(aggregateUsage).hasSize(3)
+        assertThat(aggregateUsage)
+            .containsKey(Pair("package 2", NotificationEnums.STYLE_BIG_PICTURE))
+
+        // Different UID/package should also be separate
+        val separatePackageData =
+            aggregateUsage[Pair("package 2", NotificationEnums.STYLE_BIG_PICTURE)]!!
+        val presetUsage = usage[3]
+        assertAggregatedData(
+            separatePackageData,
+            1,
+            1,
+            presetUsage.objectUsage.smallIcon,
+            1,
+            presetUsage.objectUsage.largeIcon,
+            1,
+            presetUsage.objectUsage.bigPicture,
+            1,
+            presetUsage.objectUsage.extras,
+            presetUsage.objectUsage.extender,
+            presetUsage.viewUsage[0].smallIcon,
+            presetUsage.viewUsage[0].largeIcon,
+            presetUsage.viewUsage[0].systemIcons,
+            presetUsage.viewUsage[0].style,
+            presetUsage.viewUsage[0].customViews,
+            presetUsage.viewUsage[0].softwareBitmapsPenalty,
+            0
+        )
+    }
+
     private fun createLoggerWithNotifications(
         notifications: List<Notification>
     ): NotificationMemoryLogger {
@@ -124,4 +265,182 @@
         whenever(pipeline.allNotifs).thenReturn(notifications)
         return NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
     }
+
+    /**
+     * Short hand for making sure the passed NotificationMemoryUseAtomBuilder object contains
+     * expected values.
+     */
+    private fun assertAggregatedData(
+        value: NotificationMemoryLogger.NotificationMemoryUseAtomBuilder,
+        count: Int,
+        countWithInflatedViews: Int,
+        smallIconObject: Int,
+        smallIconBitmapCount: Int,
+        largeIconObject: Int,
+        largeIconBitmapCount: Int,
+        bigPictureObject: Int,
+        bigPictureBitmapCount: Int,
+        extras: Int,
+        extenders: Int,
+        smallIconViews: Int,
+        largeIconViews: Int,
+        systemIconViews: Int,
+        styleViews: Int,
+        customViews: Int,
+        softwareBitmaps: Int,
+        seenCount: Int
+    ) {
+        expect.withMessage("count").that(value.count).isEqualTo(count)
+        expect
+            .withMessage("countWithInflatedViews")
+            .that(value.countWithInflatedViews)
+            .isEqualTo(countWithInflatedViews)
+        expect.withMessage("smallIconObject").that(value.smallIconObject).isEqualTo(smallIconObject)
+        expect
+            .withMessage("smallIconBitmapCount")
+            .that(value.smallIconBitmapCount)
+            .isEqualTo(smallIconBitmapCount)
+        expect.withMessage("largeIconObject").that(value.largeIconObject).isEqualTo(largeIconObject)
+        expect
+            .withMessage("largeIconBitmapCount")
+            .that(value.largeIconBitmapCount)
+            .isEqualTo(largeIconBitmapCount)
+        expect
+            .withMessage("bigPictureObject")
+            .that(value.bigPictureObject)
+            .isEqualTo(bigPictureObject)
+        expect
+            .withMessage("bigPictureBitmapCount")
+            .that(value.bigPictureBitmapCount)
+            .isEqualTo(bigPictureBitmapCount)
+        expect.withMessage("extras").that(value.extras).isEqualTo(extras)
+        expect.withMessage("extenders").that(value.extenders).isEqualTo(extenders)
+        expect.withMessage("smallIconViews").that(value.smallIconViews).isEqualTo(smallIconViews)
+        expect.withMessage("largeIconViews").that(value.largeIconViews).isEqualTo(largeIconViews)
+        expect.withMessage("systemIconViews").that(value.systemIconViews).isEqualTo(systemIconViews)
+        expect.withMessage("styleViews").that(value.styleViews).isEqualTo(styleViews)
+        expect.withMessage("customViews").that(value.customViews).isEqualTo(customViews)
+        expect.withMessage("softwareBitmaps").that(value.softwareBitmaps).isEqualTo(softwareBitmaps)
+        expect.withMessage("seenCount").that(value.seenCount).isEqualTo(seenCount)
+    }
+
+    /** Generates a static set of [NotificationMemoryUsage] objects. */
+    private fun getPresetMemoryUsages() =
+        listOf(
+            // A pair of notifications that have to be aggregated, same UID and style
+            NotificationMemoryUsage(
+                "package 1",
+                384,
+                "key1",
+                Notification.Builder(context).setStyle(Notification.BigPictureStyle()).build(),
+                NotificationObjectUsage(
+                    23,
+                    45,
+                    67,
+                    NotificationEnums.STYLE_BIG_PICTURE,
+                    12,
+                    483,
+                    4382,
+                    true
+                ),
+                listOf(
+                    NotificationViewUsage(ViewType.TOTAL, 493, 584, 4833, 584, 4888, 5843),
+                    NotificationViewUsage(
+                        ViewType.PRIVATE_CONTRACTED_VIEW,
+                        100,
+                        250,
+                        300,
+                        594,
+                        6000,
+                        5843
+                    )
+                )
+            ),
+            NotificationMemoryUsage(
+                "package 1",
+                384,
+                "key2",
+                Notification.Builder(context).setStyle(Notification.BigPictureStyle()).build(),
+                NotificationObjectUsage(
+                    77,
+                    54,
+                    34,
+                    NotificationEnums.STYLE_BIG_PICTURE,
+                    77,
+                    432,
+                    2342,
+                    true
+                ),
+                listOf(
+                    NotificationViewUsage(ViewType.TOTAL, 3245, 1234, 7653, 543, 765, 7655),
+                    NotificationViewUsage(
+                        ViewType.PRIVATE_CONTRACTED_VIEW,
+                        160,
+                        350,
+                        300,
+                        5544,
+                        66500,
+                        5433
+                    )
+                )
+            ),
+            // Different style is different aggregation
+            NotificationMemoryUsage(
+                "package 1",
+                384,
+                "key2",
+                Notification.Builder(context).setStyle(Notification.BigTextStyle()).build(),
+                NotificationObjectUsage(
+                    77,
+                    54,
+                    34,
+                    NotificationEnums.STYLE_BIG_TEXT,
+                    77,
+                    432,
+                    2342,
+                    true
+                ),
+                listOf(
+                    NotificationViewUsage(ViewType.TOTAL, 3245, 1234, 7653, 543, 765, 7655),
+                    NotificationViewUsage(
+                        ViewType.PRIVATE_CONTRACTED_VIEW,
+                        160,
+                        350,
+                        300,
+                        5544,
+                        66500,
+                        5433
+                    )
+                )
+            ),
+            // Different package is also different aggregation
+            NotificationMemoryUsage(
+                "package 2",
+                684,
+                "key2",
+                Notification.Builder(context).setStyle(Notification.BigPictureStyle()).build(),
+                NotificationObjectUsage(
+                    32,
+                    654,
+                    234,
+                    NotificationEnums.STYLE_BIG_PICTURE,
+                    211,
+                    776,
+                    435,
+                    true
+                ),
+                listOf(
+                    NotificationViewUsage(ViewType.TOTAL, 4355, 6543, 4322, 5435, 6546, 65485),
+                    NotificationViewUsage(
+                        ViewType.PRIVATE_CONTRACTED_VIEW,
+                        6546,
+                        7657,
+                        4353,
+                        6546,
+                        76575,
+                        54654
+                    )
+                )
+            )
+        )
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 9d531a1..9e23d54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -44,16 +44,23 @@
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.graphics.Color;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.DisplayMetrics;
 import android.view.View;
+import android.widget.ImageView;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.internal.widget.CachingIconView;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
@@ -61,6 +68,7 @@
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 
 import org.junit.Assert;
@@ -72,20 +80,16 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.Arrays;
 import java.util.List;
+import java.util.function.Consumer;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 public class ExpandableNotificationRowTest extends SysuiTestCase {
 
-    private ExpandableNotificationRow mGroupRow;
-    private ExpandableNotificationRow mNotifRow;
-    private ExpandableNotificationRow mPublicRow;
-
     private NotificationTestHelper mNotificationTestHelper;
-    boolean mHeadsUpAnimatingAway = false;
-
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
 
     @Before
@@ -96,109 +100,108 @@
                 mDependency,
                 TestableLooper.get(this));
         mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
-        // create a standard private notification row
-        Notification normalNotif = mNotificationTestHelper.createNotification();
-        normalNotif.publicVersion = null;
-        mNotifRow = mNotificationTestHelper.createRow(normalNotif);
+
+        FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
+        fakeFeatureFlags.set(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE, true);
+        mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags);
+    }
+
+    @Test
+    public void testUpdateBackgroundColors_isRecursive() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+        group.setTintColor(Color.RED);
+        group.getChildNotificationAt(0).setTintColor(Color.GREEN);
+        group.getChildNotificationAt(1).setTintColor(Color.BLUE);
+
+        assertThat(group.getCurrentBackgroundTint()).isEqualTo(Color.RED);
+        assertThat(group.getChildNotificationAt(0).getCurrentBackgroundTint())
+                .isEqualTo(Color.GREEN);
+        assertThat(group.getChildNotificationAt(1).getCurrentBackgroundTint())
+                .isEqualTo(Color.BLUE);
+
+        group.updateBackgroundColors();
+
+        int resetTint = group.getCurrentBackgroundTint();
+        assertThat(resetTint).isNotEqualTo(Color.RED);
+        assertThat(group.getChildNotificationAt(0).getCurrentBackgroundTint())
+                .isEqualTo(resetTint);
+        assertThat(group.getChildNotificationAt(1).getCurrentBackgroundTint())
+                .isEqualTo(resetTint);
+    }
+
+    @Test
+    public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
+        // GIVEN a sensitive notification row that's currently redacted
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        measureAndLayout(row);
+        row.setHideSensitiveForIntrinsicHeight(true);
+        row.setSensitive(true, true);
+        assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+        assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
+
+        // GIVEN that the row has a height change listener
+        OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
+        row.setOnHeightChangedListener(listener);
+
+        // WHEN the row is set to no longer be sensitive
+        row.setSensitive(false, true);
+
+        // VERIFY that the height change listener is invoked
+        assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
+        assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
+        verify(listener).onHeightChanged(eq(row), eq(false));
+    }
+
+    @Test
+    public void testSetSensitiveOnGroupRowNotifiesOfHeightChange() throws Exception {
+        // GIVEN a sensitive group row that's currently redacted
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+        measureAndLayout(group);
+        group.setHideSensitiveForIntrinsicHeight(true);
+        group.setSensitive(true, true);
+        assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPublicLayout());
+        assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
+
+        // GIVEN that the row has a height change listener
+        OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
+        group.setOnHeightChangedListener(listener);
+
+        // WHEN the row is set to no longer be sensitive
+        group.setSensitive(false, true);
+
+        // VERIFY that the height change listener is invoked
+        assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPrivateLayout());
+        assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
+        verify(listener).onHeightChanged(eq(group), eq(false));
+    }
+
+    @Test
+    public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange() throws Exception {
         // create a notification row whose public version is identical
         Notification publicNotif = mNotificationTestHelper.createNotification();
         publicNotif.publicVersion = mNotificationTestHelper.createNotification();
-        mPublicRow = mNotificationTestHelper.createRow(publicNotif);
-        // create a group row
-        mGroupRow = mNotificationTestHelper.createGroup();
-        mGroupRow.setHeadsUpAnimatingAwayListener(
-                animatingAway -> mHeadsUpAnimatingAway = animatingAway);
+        ExpandableNotificationRow publicRow = mNotificationTestHelper.createRow(publicNotif);
 
-    }
-
-    @Test
-    public void testUpdateBackgroundColors_isRecursive() {
-        mGroupRow.setTintColor(Color.RED);
-        mGroupRow.getChildNotificationAt(0).setTintColor(Color.GREEN);
-        mGroupRow.getChildNotificationAt(1).setTintColor(Color.BLUE);
-
-        assertThat(mGroupRow.getCurrentBackgroundTint()).isEqualTo(Color.RED);
-        assertThat(mGroupRow.getChildNotificationAt(0).getCurrentBackgroundTint())
-                .isEqualTo(Color.GREEN);
-        assertThat(mGroupRow.getChildNotificationAt(1).getCurrentBackgroundTint())
-                .isEqualTo(Color.BLUE);
-
-        mGroupRow.updateBackgroundColors();
-
-        int resetTint = mGroupRow.getCurrentBackgroundTint();
-        assertThat(resetTint).isNotEqualTo(Color.RED);
-        assertThat(mGroupRow.getChildNotificationAt(0).getCurrentBackgroundTint())
-                .isEqualTo(resetTint);
-        assertThat(mGroupRow.getChildNotificationAt(1).getCurrentBackgroundTint())
-                .isEqualTo(resetTint);
-    }
-
-    @Test
-    public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws InterruptedException {
-        // GIVEN a sensitive notification row that's currently redacted
-        measureAndLayout(mNotifRow);
-        mNotifRow.setHideSensitiveForIntrinsicHeight(true);
-        mNotifRow.setSensitive(true, true);
-        assertThat(mNotifRow.getShowingLayout()).isSameInstanceAs(mNotifRow.getPublicLayout());
-        assertThat(mNotifRow.getIntrinsicHeight()).isGreaterThan(0);
-
-        // GIVEN that the row has a height change listener
-        OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
-        mNotifRow.setOnHeightChangedListener(listener);
-
-        // WHEN the row is set to no longer be sensitive
-        mNotifRow.setSensitive(false, true);
-
-        // VERIFY that the height change listener is invoked
-        assertThat(mNotifRow.getShowingLayout()).isSameInstanceAs(mNotifRow.getPrivateLayout());
-        assertThat(mNotifRow.getIntrinsicHeight()).isGreaterThan(0);
-        verify(listener).onHeightChanged(eq(mNotifRow), eq(false));
-    }
-
-    @Test
-    public void testSetSensitiveOnGroupRowNotifiesOfHeightChange() {
-        // GIVEN a sensitive group row that's currently redacted
-        measureAndLayout(mGroupRow);
-        mGroupRow.setHideSensitiveForIntrinsicHeight(true);
-        mGroupRow.setSensitive(true, true);
-        assertThat(mGroupRow.getShowingLayout()).isSameInstanceAs(mGroupRow.getPublicLayout());
-        assertThat(mGroupRow.getIntrinsicHeight()).isGreaterThan(0);
-
-        // GIVEN that the row has a height change listener
-        OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
-        mGroupRow.setOnHeightChangedListener(listener);
-
-        // WHEN the row is set to no longer be sensitive
-        mGroupRow.setSensitive(false, true);
-
-        // VERIFY that the height change listener is invoked
-        assertThat(mGroupRow.getShowingLayout()).isSameInstanceAs(mGroupRow.getPrivateLayout());
-        assertThat(mGroupRow.getIntrinsicHeight()).isGreaterThan(0);
-        verify(listener).onHeightChanged(eq(mGroupRow), eq(false));
-    }
-
-    @Test
-    public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange() {
         // GIVEN a sensitive public row that's currently redacted
-        measureAndLayout(mPublicRow);
-        mPublicRow.setHideSensitiveForIntrinsicHeight(true);
-        mPublicRow.setSensitive(true, true);
-        assertThat(mPublicRow.getShowingLayout()).isSameInstanceAs(mPublicRow.getPublicLayout());
-        assertThat(mPublicRow.getIntrinsicHeight()).isGreaterThan(0);
+        measureAndLayout(publicRow);
+        publicRow.setHideSensitiveForIntrinsicHeight(true);
+        publicRow.setSensitive(true, true);
+        assertThat(publicRow.getShowingLayout()).isSameInstanceAs(publicRow.getPublicLayout());
+        assertThat(publicRow.getIntrinsicHeight()).isGreaterThan(0);
 
         // GIVEN that the row has a height change listener
         OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
-        mPublicRow.setOnHeightChangedListener(listener);
+        publicRow.setOnHeightChangedListener(listener);
 
         // WHEN the row is set to no longer be sensitive
-        mPublicRow.setSensitive(false, true);
+        publicRow.setSensitive(false, true);
 
         // VERIFY that the height change listener is not invoked, because the height didn't change
-        assertThat(mPublicRow.getShowingLayout()).isSameInstanceAs(mPublicRow.getPrivateLayout());
-        assertThat(mPublicRow.getIntrinsicHeight()).isGreaterThan(0);
-        assertThat(mPublicRow.getPrivateLayout().getMinHeight())
-                .isEqualTo(mPublicRow.getPublicLayout().getMinHeight());
-        verify(listener, never()).onHeightChanged(eq(mPublicRow), eq(false));
+        assertThat(publicRow.getShowingLayout()).isSameInstanceAs(publicRow.getPrivateLayout());
+        assertThat(publicRow.getIntrinsicHeight()).isGreaterThan(0);
+        assertThat(publicRow.getPrivateLayout().getMinHeight())
+                .isEqualTo(publicRow.getPublicLayout().getMinHeight());
+        verify(listener, never()).onHeightChanged(eq(publicRow), eq(false));
     }
 
     private void measureAndLayout(ExpandableNotificationRow row) {
@@ -215,36 +218,43 @@
     }
 
     @Test
-    public void testGroupSummaryNotShowingIconWhenPublic() {
-        mGroupRow.setSensitive(true, true);
-        mGroupRow.setHideSensitiveForIntrinsicHeight(true);
-        assertTrue(mGroupRow.isSummaryWithChildren());
-        assertFalse(mGroupRow.isShowingIcon());
+    public void testGroupSummaryNotShowingIconWhenPublic() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+        group.setSensitive(true, true);
+        group.setHideSensitiveForIntrinsicHeight(true);
+        assertTrue(group.isSummaryWithChildren());
+        assertFalse(group.isShowingIcon());
     }
 
     @Test
-    public void testNotificationHeaderVisibleWhenAnimating() {
-        mGroupRow.setSensitive(true, true);
-        mGroupRow.setHideSensitive(true, false, 0, 0);
-        mGroupRow.setHideSensitive(false, true, 0, 0);
-        assertEquals(View.VISIBLE, mGroupRow.getChildrenContainer().getVisibleWrapper()
+    public void testNotificationHeaderVisibleWhenAnimating() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+        group.setSensitive(true, true);
+        group.setHideSensitive(true, false, 0, 0);
+        group.setHideSensitive(false, true, 0, 0);
+        assertEquals(View.VISIBLE, group.getChildrenContainer().getVisibleWrapper()
                 .getNotificationHeader().getVisibility());
     }
 
     @Test
-    public void testUserLockedResetEvenWhenNoChildren() {
-        mGroupRow.setUserLocked(true);
-        mGroupRow.setUserLocked(false);
+    public void testUserLockedResetEvenWhenNoChildren() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+        group.setUserLocked(true);
+        group.setUserLocked(false);
         assertFalse("The childrencontainer should not be userlocked but is, the state "
-                + "seems out of sync.", mGroupRow.getChildrenContainer().isUserLocked());
+                + "seems out of sync.", group.getChildrenContainer().isUserLocked());
     }
 
     @Test
-    public void testReinflatedOnDensityChange() {
+    public void testReinflatedOnDensityChange() throws Exception {
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
         NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
-        mNotifRow.setChildrenContainer(mockContainer);
+        row.setChildrenContainer(mockContainer);
 
-        mNotifRow.onDensityOrFontScaleChanged();
+        row.onDensityOrFontScaleChanged();
 
         verify(mockContainer).reInflateViews(any(), any());
     }
@@ -287,64 +297,73 @@
     @Test
     public void testAboveShelfChangedListenerCalledWhenGoingBelow() throws Exception {
         ExpandableNotificationRow row = mNotificationTestHelper.createRow();
-        row.setHeadsUp(true);
         AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class);
         row.setAboveShelfChangedListener(listener);
+        Mockito.reset(listener);
+        row.setHeadsUp(true);
         row.setAboveShelf(false);
         verify(listener).onAboveShelfStateChanged(false);
     }
 
     @Test
     public void testClickSound() throws Exception {
-        assertTrue("Should play sounds by default.", mGroupRow.isSoundEffectsEnabled());
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+        assertTrue("Should play sounds by default.", group.isSoundEffectsEnabled());
         StatusBarStateController mock = mNotificationTestHelper.getStatusBarStateController();
         when(mock.isDozing()).thenReturn(true);
-        mGroupRow.setSecureStateProvider(()-> false);
+        group.setSecureStateProvider(()-> false);
         assertFalse("Shouldn't play sounds when dark and trusted.",
-                mGroupRow.isSoundEffectsEnabled());
-        mGroupRow.setSecureStateProvider(()-> true);
+                group.isSoundEffectsEnabled());
+        group.setSecureStateProvider(()-> true);
         assertTrue("Should always play sounds when not trusted.",
-                mGroupRow.isSoundEffectsEnabled());
+                group.isSoundEffectsEnabled());
     }
 
     @Test
-    public void testSetDismissed_longPressListenerRemoved() {
+    public void testSetDismissed_longPressListenerRemoved() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
         ExpandableNotificationRow.LongPressListener listener =
                 mock(ExpandableNotificationRow.LongPressListener.class);
-        mGroupRow.setLongPressListener(listener);
-        mGroupRow.doLongClickCallback(0,0);
-        verify(listener, times(1)).onLongPress(eq(mGroupRow), eq(0), eq(0),
+        group.setLongPressListener(listener);
+        group.doLongClickCallback(0, 0);
+        verify(listener, times(1)).onLongPress(eq(group), eq(0), eq(0),
                 any(NotificationMenuRowPlugin.MenuItem.class));
         reset(listener);
 
-        mGroupRow.dismiss(true);
-        mGroupRow.doLongClickCallback(0,0);
-        verify(listener, times(0)).onLongPress(eq(mGroupRow), eq(0), eq(0),
+        group.dismiss(true);
+        group.doLongClickCallback(0, 0);
+        verify(listener, times(0)).onLongPress(eq(group), eq(0), eq(0),
                 any(NotificationMenuRowPlugin.MenuItem.class));
     }
 
     @Test
-    public void testFeedback_noHeader() {
+    public void testFeedback_noHeader() throws Exception {
+        ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
+
         // public notification is custom layout - no header
-        mGroupRow.setSensitive(true, true);
-        mGroupRow.setOnFeedbackClickListener(null);
-        mGroupRow.setFeedbackIcon(null);
+        groupRow.setSensitive(true, true);
+        groupRow.setOnFeedbackClickListener(null);
+        groupRow.setFeedbackIcon(null);
     }
 
     @Test
-    public void testFeedback_header() {
+    public void testFeedback_header() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
         NotificationContentView publicLayout = mock(NotificationContentView.class);
-        mGroupRow.setPublicLayout(publicLayout);
+        group.setPublicLayout(publicLayout);
         NotificationContentView privateLayout = mock(NotificationContentView.class);
-        mGroupRow.setPrivateLayout(privateLayout);
+        group.setPrivateLayout(privateLayout);
         NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
         when(mockContainer.getNotificationChildCount()).thenReturn(1);
-        mGroupRow.setChildrenContainer(mockContainer);
+        group.setChildrenContainer(mockContainer);
 
         final boolean show = true;
         final FeedbackIcon icon = new FeedbackIcon(
                 R.drawable.ic_feedback_alerted, R.string.notification_feedback_indicator_alerted);
-        mGroupRow.setFeedbackIcon(icon);
+        group.setFeedbackIcon(icon);
 
         verify(mockContainer, times(1)).setFeedbackIcon(icon);
         verify(privateLayout, times(1)).setFeedbackIcon(icon);
@@ -352,43 +371,60 @@
     }
 
     @Test
-    public void testFeedbackOnClick() {
+    public void testFeedbackOnClick() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
         ExpandableNotificationRow.CoordinateOnClickListener l = mock(
                 ExpandableNotificationRow.CoordinateOnClickListener.class);
         View view = mock(View.class);
 
-        mGroupRow.setOnFeedbackClickListener(l);
+        group.setOnFeedbackClickListener(l);
 
-        mGroupRow.getFeedbackOnClickListener().onClick(view);
+        group.getFeedbackOnClickListener().onClick(view);
         verify(l, times(1)).onClick(any(), anyInt(), anyInt(), any());
     }
 
     @Test
-    public void testHeadsUpAnimatingAwayListener() {
-        mGroupRow.setHeadsUpAnimatingAway(true);
-        Assert.assertEquals(true, mHeadsUpAnimatingAway);
-        mGroupRow.setHeadsUpAnimatingAway(false);
-        Assert.assertEquals(false, mHeadsUpAnimatingAway);
+    public void testHeadsUpAnimatingAwayListener() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+        Consumer<Boolean> headsUpListener = mock(Consumer.class);
+        AboveShelfChangedListener aboveShelfChangedListener = mock(AboveShelfChangedListener.class);
+        group.setHeadsUpAnimatingAwayListener(headsUpListener);
+        group.setAboveShelfChangedListener(aboveShelfChangedListener);
+
+        group.setHeadsUpAnimatingAway(true);
+        verify(headsUpListener).accept(true);
+        verify(aboveShelfChangedListener).onAboveShelfStateChanged(true);
+
+        group.setHeadsUpAnimatingAway(false);
+        verify(headsUpListener).accept(false);
+        verify(aboveShelfChangedListener).onAboveShelfStateChanged(false);
     }
 
     @Test
-    public void testIsBlockingHelperShowing_isCorrectlyUpdated() {
-        mGroupRow.setBlockingHelperShowing(true);
-        assertTrue(mGroupRow.isBlockingHelperShowing());
+    public void testIsBlockingHelperShowing_isCorrectlyUpdated() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
 
-        mGroupRow.setBlockingHelperShowing(false);
-        assertFalse(mGroupRow.isBlockingHelperShowing());
+        group.setBlockingHelperShowing(true);
+        assertTrue(group.isBlockingHelperShowing());
+
+        group.setBlockingHelperShowing(false);
+        assertFalse(group.isBlockingHelperShowing());
     }
 
     @Test
-    public void testGetNumUniqueChildren_defaultChannel() {
-        assertEquals(1, mGroupRow.getNumUniqueChannels());
+    public void testGetNumUniqueChildren_defaultChannel() throws Exception {
+        ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
+
+        assertEquals(1, groupRow.getNumUniqueChannels());
     }
 
     @Test
-    public void testGetNumUniqueChildren_multiChannel() {
+    public void testGetNumUniqueChildren_multiChannel() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
         List<ExpandableNotificationRow> childRows =
-                mGroupRow.getChildrenContainer().getAttachedChildren();
+                group.getChildrenContainer().getAttachedChildren();
         // Give each child a unique channel id/name.
         int i = 0;
         for (ExpandableNotificationRow childRow : childRows) {
@@ -400,25 +436,29 @@
             i++;
         }
 
-        assertEquals(3, mGroupRow.getNumUniqueChannels());
+        assertEquals(3, group.getNumUniqueChannels());
     }
 
     @Test
     public void testIconScrollXAfterTranslationAndReset() throws Exception {
-        mGroupRow.setDismissUsingRowTranslationX(false);
-        mGroupRow.setTranslation(50);
-        assertEquals(50, -mGroupRow.getEntry().getIcons().getShelfIcon().getScrollX());
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
 
-        mGroupRow.resetTranslation();
-        assertEquals(0, mGroupRow.getEntry().getIcons().getShelfIcon().getScrollX());
+        group.setDismissUsingRowTranslationX(false);
+        group.setTranslation(50);
+        assertEquals(50, -group.getEntry().getIcons().getShelfIcon().getScrollX());
+
+        group.resetTranslation();
+        assertEquals(0, group.getEntry().getIcons().getShelfIcon().getScrollX());
     }
 
     @Test
-    public void testIsExpanded_userExpanded() {
-        mGroupRow.setExpandable(true);
-        Assert.assertFalse(mGroupRow.isExpanded());
-        mGroupRow.setUserExpanded(true);
-        Assert.assertTrue(mGroupRow.isExpanded());
+    public void testIsExpanded_userExpanded() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+        group.setExpandable(true);
+        Assert.assertFalse(group.isExpanded());
+        group.setUserExpanded(true);
+        Assert.assertTrue(group.isExpanded());
     }
 
     @Test
@@ -537,26 +577,155 @@
     }
 
     @Test
-    public void applyRoundnessAndInv_should_be_immediately_applied_on_childrenContainer_legacy() {
-        mGroupRow.useRoundnessSourceTypes(false);
-        Assert.assertEquals(0f, mGroupRow.getBottomRoundness(), 0.001f);
-        Assert.assertEquals(0f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+    public void applyRoundnessAndInv_should_be_immediately_applied_on_childrenContainer_legacy()
+            throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+        group.useRoundnessSourceTypes(false);
+        Assert.assertEquals(0f, group.getBottomRoundness(), 0.001f);
+        Assert.assertEquals(0f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
 
-        mGroupRow.requestBottomRoundness(1f, SourceType.from(""), false);
+        group.requestBottomRoundness(1f, SourceType.from(""), false);
 
-        Assert.assertEquals(1f, mGroupRow.getBottomRoundness(), 0.001f);
-        Assert.assertEquals(1f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+        Assert.assertEquals(1f, group.getBottomRoundness(), 0.001f);
+        Assert.assertEquals(1f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
     }
 
     @Test
-    public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_childrenContainer() {
-        mGroupRow.useRoundnessSourceTypes(true);
-        Assert.assertEquals(0f, mGroupRow.getBottomRoundness(), 0.001f);
-        Assert.assertEquals(0f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+    public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_childrenContainer()
+            throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+        group.useRoundnessSourceTypes(true);
+        Assert.assertEquals(0f, group.getBottomRoundness(), 0.001f);
+        Assert.assertEquals(0f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
 
-        mGroupRow.requestBottomRoundness(1f, SourceType.from(""), false);
+        group.requestBottomRoundness(1f, SourceType.from(""), false);
 
-        Assert.assertEquals(1f, mGroupRow.getBottomRoundness(), 0.001f);
-        Assert.assertEquals(1f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+        Assert.assertEquals(1f, group.getBottomRoundness(), 0.001f);
+        Assert.assertEquals(1f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
+    }
+
+    @Test
+    public void testSetContentAnimationRunning_Run() throws Exception {
+        // Create views for the notification row.
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        NotificationContentView publicLayout = mock(NotificationContentView.class);
+        row.setPublicLayout(publicLayout);
+        NotificationContentView privateLayout = mock(NotificationContentView.class);
+        row.setPrivateLayout(privateLayout);
+
+        row.setAnimationRunning(true);
+        verify(publicLayout, times(1)).setContentAnimationRunning(true);
+        verify(privateLayout, times(1)).setContentAnimationRunning(true);
+    }
+
+    @Test
+    public void testSetContentAnimationRunning_Stop() throws Exception {
+        // Create views for the notification row.
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        NotificationContentView publicLayout = mock(NotificationContentView.class);
+        row.setPublicLayout(publicLayout);
+        NotificationContentView privateLayout = mock(NotificationContentView.class);
+        row.setPrivateLayout(privateLayout);
+
+        row.setAnimationRunning(false);
+        verify(publicLayout, times(1)).setContentAnimationRunning(false);
+        verify(privateLayout, times(1)).setContentAnimationRunning(false);
+    }
+
+    @Test
+    public void testSetContentAnimationRunningInGroupChild_Run() throws Exception {
+        // Creates parent views on groupRow.
+        ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
+        NotificationContentView publicParentLayout = mock(NotificationContentView.class);
+        groupRow.setPublicLayout(publicParentLayout);
+        NotificationContentView privateParentLayout = mock(NotificationContentView.class);
+        groupRow.setPrivateLayout(privateParentLayout);
+
+        // Create child views on row.
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        NotificationContentView publicChildLayout = mock(NotificationContentView.class);
+        row.setPublicLayout(publicChildLayout);
+        NotificationContentView privateChildLayout = mock(NotificationContentView.class);
+        row.setPrivateLayout(privateChildLayout);
+        when(row.isGroupExpanded()).thenReturn(true);
+        setMockChildrenContainer(groupRow, row);
+
+        groupRow.setAnimationRunning(true);
+        verify(publicParentLayout, times(1)).setContentAnimationRunning(true);
+        verify(privateParentLayout, times(1)).setContentAnimationRunning(true);
+        // The child layouts should be started too.
+        verify(publicChildLayout, times(1)).setContentAnimationRunning(true);
+        verify(privateChildLayout, times(1)).setContentAnimationRunning(true);
+    }
+
+
+    @Test
+    public void testSetIconAnimationRunningGroup_Run() throws Exception {
+        // Create views for a group row.
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+        ExpandableNotificationRow child = mNotificationTestHelper.createRow();
+        NotificationContentView publicParentLayout = mock(NotificationContentView.class);
+        group.setPublicLayout(publicParentLayout);
+        NotificationContentView privateParentLayout = mock(NotificationContentView.class);
+        group.setPrivateLayout(privateParentLayout);
+        when(group.isGroupExpanded()).thenReturn(true);
+
+        // Add the child to the group.
+        NotificationContentView publicChildLayout = mock(NotificationContentView.class);
+        child.setPublicLayout(publicChildLayout);
+        NotificationContentView privateChildLayout = mock(NotificationContentView.class);
+        child.setPrivateLayout(privateChildLayout);
+        when(child.isGroupExpanded()).thenReturn(true);
+
+        NotificationChildrenContainer mockContainer =
+                setMockChildrenContainer(group, child);
+
+        // Mock the children view wrappers, and give them each an icon.
+        NotificationViewWrapper mockViewWrapper = mock(NotificationViewWrapper.class);
+        when(mockContainer.getNotificationViewWrapper()).thenReturn(mockViewWrapper);
+        CachingIconView mockIcon = mock(CachingIconView.class);
+        when(mockViewWrapper.getIcon()).thenReturn(mockIcon);
+
+        NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class);
+        when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper);
+        CachingIconView mockLowPriorityIcon = mock(CachingIconView.class);
+        when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon);
+
+        // Give the icon image views drawables, so we can make sure they animate.
+        // We use both AnimationDrawables and AnimatedVectorDrawables to ensure both work.
+        AnimationDrawable drawable = mock(AnimationDrawable.class);
+        AnimatedVectorDrawable vectorDrawable = mock(AnimatedVectorDrawable.class);
+        setDrawableIconsInImageView(mockIcon, drawable, vectorDrawable);
+
+        AnimationDrawable lowPriDrawable = mock(AnimationDrawable.class);
+        AnimatedVectorDrawable lowPriVectorDrawable = mock(AnimatedVectorDrawable.class);
+        setDrawableIconsInImageView(mockLowPriorityIcon, lowPriDrawable, lowPriVectorDrawable);
+
+        group.setAnimationRunning(true);
+        verify(drawable, times(1)).start();
+        verify(vectorDrawable, times(1)).start();
+        verify(lowPriDrawable, times(1)).start();
+        verify(lowPriVectorDrawable, times(1)).start();
+    }
+
+    private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable,
+            Drawable rightIconDrawable) {
+        ImageView iconView = mock(ImageView.class);
+        when(icon.findViewById(com.android.internal.R.id.icon)).thenReturn(iconView);
+        when(iconView.getDrawable()).thenReturn(iconDrawable);
+
+        ImageView rightIconView = mock(ImageView.class);
+        when(icon.findViewById(com.android.internal.R.id.right_icon)).thenReturn(rightIconView);
+        when(rightIconView.getDrawable()).thenReturn(rightIconDrawable);
+    }
+
+    private NotificationChildrenContainer setMockChildrenContainer(
+            ExpandableNotificationRow parentRow, ExpandableNotificationRow childRow) {
+        List<ExpandableNotificationRow> rowList = Arrays.asList(childRow);
+        NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
+        when(mockContainer.getNotificationChildCount()).thenReturn(1);
+        when(mockContainer.getAttachedChildren()).thenReturn(rowList);
+        parentRow.setChildrenContainer(mockContainer);
+        return mockContainer;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
index 1f92b0a..819a75b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
@@ -98,5 +100,16 @@
 
         mView.setSecondaryVisible(true /* visible */, true /* animate */);
     }
+
+    @Test
+    public void testSetFooterLabelTextAndIcon() {
+        mView.setFooterLabelTextAndIcon(
+                R.string.unlock_to_see_notif_text,
+                R.drawable.ic_friction_lock_closed);
+        assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.GONE);
+        assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.GONE);
+        assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility())
+                .isEqualTo(View.VISIBLE);
+    }
 }
 
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/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 562b4df..7b2051d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -34,9 +34,12 @@
 import com.android.systemui.statusbar.notification.FeedbackIcon
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -44,6 +47,7 @@
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations.initMocks
 
@@ -305,6 +309,86 @@
         assertEquals(0, getMarginBottom(actionListMarginTarget))
     }
 
+    @Test
+    fun onSetAnimationRunning() {
+        // Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set
+        val mockContracted = mock<NotificationViewWrapper>()
+        val mockExpanded = mock<NotificationViewWrapper>()
+        val mockHeadsUp = mock<NotificationViewWrapper>()
+
+        view.setContractedWrapper(mockContracted)
+        view.setExpandedWrapper(mockExpanded)
+        view.setHeadsUpWrapper(mockHeadsUp)
+
+        // When: we set content animation running.
+        assertTrue(view.setContentAnimationRunning(true))
+
+        // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
+        // called on them.
+        verify(mockContracted, times(1)).setAnimationsRunning(true)
+        verify(mockExpanded, times(1)).setAnimationsRunning(true)
+        verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+
+        // When: we set content animation running true _again_.
+        assertFalse(view.setContentAnimationRunning(true))
+
+        // Then: the children should not have setAnimationRunning called on them again.
+        // Verify counts number of calls so far on the object, so these still register as 1.
+        verify(mockContracted, times(1)).setAnimationsRunning(true)
+        verify(mockExpanded, times(1)).setAnimationsRunning(true)
+        verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+    }
+
+    @Test
+    fun onSetAnimationStopped() {
+        // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
+        val mockContracted = mock<NotificationViewWrapper>()
+        val mockExpanded = mock<NotificationViewWrapper>()
+        val mockHeadsUp = mock<NotificationViewWrapper>()
+
+        view.setContractedWrapper(mockContracted)
+        view.setExpandedWrapper(mockExpanded)
+        view.setHeadsUpWrapper(mockHeadsUp)
+
+        // When: we set content animation running.
+        assertTrue(view.setContentAnimationRunning(true))
+
+        // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
+        // called on them.
+        verify(mockContracted).setAnimationsRunning(true)
+        verify(mockExpanded).setAnimationsRunning(true)
+        verify(mockHeadsUp).setAnimationsRunning(true)
+
+        // When: we set content animation running false, the state changes, so the function
+        // returns true.
+        assertTrue(view.setContentAnimationRunning(false))
+
+        // Then: the children have their animations stopped.
+        verify(mockContracted).setAnimationsRunning(false)
+        verify(mockExpanded).setAnimationsRunning(false)
+        verify(mockHeadsUp).setAnimationsRunning(false)
+    }
+
+    @Test
+    fun onSetAnimationInitStopped() {
+        // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
+        val mockContracted = mock<NotificationViewWrapper>()
+        val mockExpanded = mock<NotificationViewWrapper>()
+        val mockHeadsUp = mock<NotificationViewWrapper>()
+
+        view.setContractedWrapper(mockContracted)
+        view.setExpandedWrapper(mockExpanded)
+        view.setHeadsUpWrapper(mockHeadsUp)
+
+        // When: we try to stop the animations before they've been started.
+        assertFalse(view.setContentAnimationRunning(false))
+
+        // Then: the children should not have setAnimationRunning called on them again.
+        verify(mockContracted, never()).setAnimationsRunning(false)
+        verify(mockExpanded, never()).setAnimationsRunning(false)
+        verify(mockHeadsUp, never()).setAnimationsRunning(false)
+    }
+
     private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
         mock<ExpandableNotificationRow>().apply {
             whenever(this.entry).thenReturn(notificationEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index b6a1bb3..d7ac6b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -66,9 +66,9 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -133,18 +133,13 @@
     @Mock private ShadeController mShadeController;
     @Mock private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager;
     @Mock private AssistantFeedbackController mAssistantFeedbackController;
+    @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
+    @Mock private StatusBarStateController mStatusBarStateController;
 
     @Before
     public void setUp() {
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
-        mDependency.injectTestDependency(DeviceProvisionedController.class,
-                mDeviceProvisionedController);
-        mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
-        mDependency.injectTestDependency(
-                OnUserInteractionCallback.class,
-                mOnUserInteractionCallback);
-        mDependency.injectMockDependency(NotificationLockscreenUserManager.class);
         mHandler = Handler.createAsync(mTestableLooper.getLooper());
         mHelper = new NotificationTestHelper(mContext, mDependency, TestableLooper.get(this));
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
@@ -155,7 +150,11 @@
                 mPeopleSpaceWidgetManager, mLauncherApps, mShortcutManager,
                 mChannelEditorDialogController, mContextTracker, mAssistantFeedbackController,
                 Optional.of(mBubblesManager), new UiEventLoggerFake(), mOnUserInteractionCallback,
-                mShadeController);
+                mShadeController,
+                mNotificationLockscreenUserManager,
+                mStatusBarStateController,
+                mDeviceProvisionedController,
+                mMetricsLogger);
         mGutsManager.setUpWithPresenter(mPresenter, mNotificationListContainer,
                 mOnSettingsClickListener);
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
@@ -372,7 +371,8 @@
                 eq(false),
                 eq(false),
                 eq(true), /* wasShownHighPriority */
-                eq(mAssistantFeedbackController));
+                eq(mAssistantFeedbackController),
+                any(MetricsLogger.class));
     }
 
     @Test
@@ -406,7 +406,8 @@
                 eq(true),
                 eq(false),
                 eq(false), /* wasShownHighPriority */
-                eq(mAssistantFeedbackController));
+                eq(mAssistantFeedbackController),
+                any(MetricsLogger.class));
     }
 
     @Test
@@ -438,7 +439,8 @@
                 eq(false),
                 eq(false),
                 eq(false), /* wasShownHighPriority */
-                eq(mAssistantFeedbackController));
+                eq(mAssistantFeedbackController),
+                any(MetricsLogger.class));
     }
 
     ////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
index 80a81a5..8dd0488 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
@@ -130,7 +130,6 @@
         mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
 
         mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
-        mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
         // Inflate the layout
         final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
         mNotificationInfo = (NotificationInfo) layoutInflater.inflate(R.layout.notification_info,
@@ -194,7 +193,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
         assertTrue(textView.getText().toString().contains("App Name"));
         assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
@@ -220,7 +220,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon);
         assertEquals(iconDrawable, iconView.getDrawable());
     }
@@ -242,7 +243,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
         assertEquals(GONE, nameView.getVisibility());
     }
@@ -273,7 +275,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
         assertEquals(VISIBLE, nameView.getVisibility());
         assertTrue(nameView.getText().toString().contains("Proxied"));
@@ -296,7 +299,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
         assertEquals(GONE, groupNameView.getVisibility());
     }
@@ -324,7 +328,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
         assertEquals(View.VISIBLE, groupNameView.getVisibility());
         assertEquals("Test Group Name", groupNameView.getText());
@@ -347,7 +352,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
         assertEquals(TEST_CHANNEL_NAME, textView.getText());
     }
@@ -369,7 +375,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
         assertEquals(GONE, textView.getVisibility());
     }
@@ -395,7 +402,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
         assertEquals(VISIBLE, textView.getVisibility());
     }
@@ -417,7 +425,8 @@
                 true,
                 true,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
         assertEquals(VISIBLE, textView.getVisibility());
     }
@@ -443,7 +452,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         settingsButton.performClick();
@@ -468,7 +478,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         assertTrue(settingsButton.getVisibility() != View.VISIBLE);
     }
@@ -493,7 +504,8 @@
                 false,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         assertTrue(settingsButton.getVisibility() != View.VISIBLE);
     }
@@ -515,7 +527,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         mNotificationInfo.bindNotification(
                 mMockPackageManager,
                 mMockINotificationManager,
@@ -531,7 +544,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
         assertEquals(View.VISIBLE, settingsButton.getVisibility());
     }
@@ -556,7 +570,8 @@
                 true,
                 true,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.info).performClick();
         // Verify that listener was triggered.
@@ -582,7 +597,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView channelNameView =
                 mNotificationInfo.findViewById(R.id.channel_name);
         assertEquals(GONE, channelNameView.getVisibility());
@@ -606,7 +622,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertEquals(GONE, mNotificationInfo.findViewById(
                 R.id.interruptiveness_settings).getVisibility());
         assertEquals(VISIBLE, mNotificationInfo.findViewById(
@@ -630,7 +647,8 @@
                 true,
                 true,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text);
         assertEquals(View.VISIBLE, view.getVisibility());
         assertEquals(mContext.getString(R.string.notification_unblockable_desc),
@@ -673,7 +691,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text);
         assertEquals(View.VISIBLE, view.getVisibility());
         assertEquals(mContext.getString(R.string.notification_unblockable_call_desc),
@@ -716,7 +735,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertEquals(GONE,
                 mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility());
         assertEquals(VISIBLE,
@@ -743,7 +763,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
         assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
     }
@@ -765,7 +786,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
         assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
     }
@@ -789,7 +811,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected());
     }
 
@@ -810,7 +833,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected());
     }
 
@@ -831,7 +855,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected());
     }
 
@@ -852,7 +877,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         mTestableLooper.processAllMessages();
         verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
                 anyString(), eq(TEST_UID), any());
@@ -875,7 +901,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
         assertEquals(1, mUiEventLogger.numLogs());
         assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
                 mUiEventLogger.eventId(0));
@@ -899,7 +926,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.alert).performClick();
         mTestableLooper.processAllMessages();
@@ -926,7 +954,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.silence).performClick();
         mTestableLooper.processAllMessages();
@@ -953,7 +982,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.automatic).performClick();
         mTestableLooper.processAllMessages();
@@ -981,7 +1011,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.handleCloseControls(true, false);
         mTestableLooper.processAllMessages();
@@ -1008,7 +1039,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.handleCloseControls(true, false);
         mTestableLooper.processAllMessages();
@@ -1043,7 +1075,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.handleCloseControls(true, false);
 
@@ -1071,7 +1104,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.silence).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1112,7 +1146,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.alert).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1149,7 +1184,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.automatic).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1181,7 +1217,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.silence).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1217,7 +1254,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         assertEquals(mContext.getString(R.string.inline_done_button),
                 ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1255,7 +1293,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.silence).performClick();
         mNotificationInfo.handleCloseControls(false, false);
@@ -1286,7 +1325,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         assertEquals(mContext.getString(R.string.inline_done_button),
                 ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1324,7 +1364,8 @@
                 true,
                 false,
                 true,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.silence).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1353,7 +1394,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         assertEquals(mContext.getString(R.string.inline_done_button),
                 ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1384,7 +1426,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.alert).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1419,7 +1462,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.alert).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1452,7 +1496,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.alert).performClick();
         mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1485,7 +1530,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         mNotificationInfo.findViewById(R.id.alert).performClick();
 
@@ -1511,7 +1557,8 @@
                 true,
                 false,
                 false,
-                mAssistantFeedbackController);
+                mAssistantFeedbackController,
+                mMetricsLogger);
 
         assertFalse(mNotificationInfo.willBeRemoved());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 728e026..aca9c56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -40,7 +40,6 @@
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.graphics.drawable.Icon;
-import android.os.Handler;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.testing.TestableLooper;
@@ -49,14 +48,13 @@
 import android.widget.RemoteViews;
 
 import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.TestableDependency;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -67,7 +65,6 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.icon.IconBuilder;
@@ -76,11 +73,8 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
-import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
 import com.android.systemui.statusbar.policy.SmartReplyConstants;
@@ -120,16 +114,17 @@
     private final GroupMembershipManager mGroupMembershipManager;
     private final GroupExpansionManager mGroupExpansionManager;
     private ExpandableNotificationRow mRow;
-    private HeadsUpManagerPhone mHeadsUpManager;
+    private final HeadsUpManagerPhone mHeadsUpManager;
     private final NotifBindPipeline mBindPipeline;
     private final NotifCollectionListener mBindPipelineEntryListener;
     private final RowContentBindStage mBindStage;
     private final IconManager mIconManager;
-    private StatusBarStateController mStatusBarStateController;
+    private final StatusBarStateController mStatusBarStateController;
     private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
     public final OnUserInteractionCallback mOnUserInteractionCallback;
     public final Runnable mFutureDismissalRunnable;
     private @InflationFlag int mDefaultInflationFlags;
+    private FeatureFlags mFeatureFlags;
 
     public NotificationTestHelper(
             Context context,
@@ -144,21 +139,7 @@
         mStatusBarStateController = mock(StatusBarStateController.class);
         mGroupMembershipManager = mock(GroupMembershipManager.class);
         mGroupExpansionManager = mock(GroupExpansionManager.class);
-        mHeadsUpManager = new HeadsUpManagerPhone(
-                mContext,
-                mock(HeadsUpManagerLogger.class),
-                mStatusBarStateController,
-                mock(KeyguardBypassController.class),
-                mock(GroupMembershipManager.class),
-                mock(VisualStabilityProvider.class),
-                mock(ConfigurationControllerImpl.class),
-                new Handler(mTestLooper.getLooper()),
-                mock(AccessibilityManagerWrapper.class),
-                mock(UiEventLogger.class),
-                mock(ShadeExpansionStateManager.class)
-        );
-        mHeadsUpManager.mHandler.removeCallbacksAndMessages(null);
-        mHeadsUpManager.mHandler = new Handler(mTestLooper.getLooper());
+        mHeadsUpManager = mock(HeadsUpManagerPhone.class);
         mIconManager = new IconManager(
                 mock(CommonNotifCollection.class),
                 mock(LauncherApps.class),
@@ -193,12 +174,17 @@
         mFutureDismissalRunnable = mock(Runnable.class);
         when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
                 .thenReturn(mFutureDismissalRunnable);
+        mFeatureFlags = mock(FeatureFlags.class);
     }
 
     public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) {
         mDefaultInflationFlags = defaultInflationFlags;
     }
 
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
+
     public ExpandableNotificationRowLogger getMockLogger() {
         return mMockLogger;
     }
@@ -561,7 +547,8 @@
                 mock(NotificationGutsManager.class),
                 mock(MetricsLogger.class),
                 mock(SmartReplyConstants.class),
-                mock(SmartReplyController.class));
+                mock(SmartReplyController.class),
+                mFeatureFlags);
 
         row.setAboveShelfChangedListener(aboveShelf -> { });
         mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
index 509ba41..8f88501 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
@@ -16,11 +16,12 @@
 
 package com.android.systemui.statusbar.notification.row.wrapper;
 
+import static org.junit.Assert.assertNotNull;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
 
 import android.app.Notification;
-import android.content.Context;
+import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.testing.AndroidTestingRunner;
@@ -28,11 +29,11 @@
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
+import com.android.internal.widget.BigPictureNotificationImageView;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -73,4 +74,38 @@
                 Notification.EXTRA_LARGE_ICON_BIG, new Bundle());
         wrapper.onContentUpdated(mRow);
     }
+
+    @Test
+    public void setAnimationsRunning_Run() {
+        BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture);
+        AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class);
+
+        assertNotNull(imageView);
+        imageView.setImageDrawable(mockDrawable);
+
+        NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext,
+                mView, mRow);
+        // Required to re-initialize the imageView to the imageView created above.
+        wrapper.onContentUpdated(mRow);
+
+        wrapper.setAnimationsRunning(true);
+        verify(mockDrawable).start();
+    }
+
+    @Test
+    public void setAnimationsRunning_Stop() {
+        BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture);
+        AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class);
+
+        assertNotNull(imageView);
+        imageView.setImageDrawable(mockDrawable);
+
+        NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext,
+                mView, mRow);
+        // Required to re-initialize the imageView to the imageView created above.
+        wrapper.onContentUpdated(mRow);
+
+        wrapper.setAnimationsRunning(false);
+        verify(mockDrawable).stop();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
new file mode 100644
index 0000000..3fa68bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.wrapper
+
+import android.graphics.drawable.AnimatedImageDrawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.widget.CachingIconView
+import com.android.internal.widget.ConversationLayout
+import com.android.internal.widget.MessagingGroup
+import com.android.internal.widget.MessagingImageMessage
+import com.android.internal.widget.MessagingLinearLayout
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() {
+
+    private lateinit var mRow: ExpandableNotificationRow
+    private lateinit var helper: NotificationTestHelper
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+        mRow = helper.createRow()
+    }
+
+    @Test
+    fun setAnimationsRunning_Run() {
+        // Creates a mocked out NotificationEntry of ConversationLayout type,
+        // with a mock imageMessage.drawable embedded in its MessagingImageMessages
+        // (both top level, and in a group).
+        val mockDrawable = mock<AnimatedImageDrawable>()
+        val mockDrawable2 = mock<AnimatedImageDrawable>()
+        val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2)
+
+        val wrapper: NotificationViewWrapper =
+            NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow)
+        wrapper.onContentUpdated(mRow)
+        wrapper.setAnimationsRunning(true)
+
+        // Verifies that each AnimatedImageDrawable is started animating.
+        verify(mockDrawable).start()
+        verify(mockDrawable2).start()
+    }
+
+    @Test
+    fun setAnimationsRunning_Stop() {
+        // Creates a mocked out NotificationEntry of ConversationLayout type,
+        // with a mock imageMessage.drawable embedded in its MessagingImageMessages
+        // (both top level, and in a group).
+        val mockDrawable = mock<AnimatedImageDrawable>()
+        val mockDrawable2 = mock<AnimatedImageDrawable>()
+        val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2)
+
+        val wrapper: NotificationViewWrapper =
+            NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow)
+        wrapper.onContentUpdated(mRow)
+        wrapper.setAnimationsRunning(false)
+
+        // Verifies that each AnimatedImageDrawable is started animating.
+        verify(mockDrawable).stop()
+        verify(mockDrawable2).stop()
+    }
+
+    private fun fakeConversationLayout(
+        mockDrawableGroupMessage: AnimatedImageDrawable,
+        mockDrawableImageMessage: AnimatedImageDrawable
+    ): View {
+        val mockMessagingImageMessage: MessagingImageMessage =
+            mock<MessagingImageMessage>().apply {
+                whenever(drawable).thenReturn(mockDrawableImageMessage)
+            }
+        val mockImageMessageContainer: MessagingLinearLayout =
+            mock<MessagingLinearLayout>().apply {
+                whenever(childCount).thenReturn(1)
+                whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage)
+            }
+
+        val mockMessagingImageMessageForGroup: MessagingImageMessage =
+            mock<MessagingImageMessage>().apply {
+                whenever(drawable).thenReturn(mockDrawableGroupMessage)
+            }
+        val mockMessageContainer: MessagingLinearLayout =
+            mock<MessagingLinearLayout>().apply {
+                whenever(childCount).thenReturn(1)
+                whenever(getChildAt(any())).thenReturn(mockMessagingImageMessageForGroup)
+            }
+        val mockGroup: MessagingGroup =
+            mock<MessagingGroup>().apply {
+                whenever(messageContainer).thenReturn(mockMessageContainer)
+            }
+        val mockView: View =
+            mock<ConversationLayout>().apply {
+                whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup)))
+                whenever(imageMessageContainer).thenReturn(mockImageMessageContainer)
+                whenever(messagingLinearLayout).thenReturn(mockMessageContainer)
+
+                // These must be mocked as they're required to be nonnull.
+                whenever(requireViewById<View>(R.id.conversation_icon_container)).thenReturn(mock())
+                whenever(requireViewById<CachingIconView>(R.id.conversation_icon))
+                    .thenReturn(mock())
+                whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock())
+                whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock())
+                whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock())
+                whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock())
+                whenever(requireViewById<View>(R.id.conversation_icon_badge_ring))
+                    .thenReturn(mock())
+                whenever(requireViewById<View>(R.id.app_name_text)).thenReturn(mock())
+                whenever(requireViewById<View>(R.id.conversation_text)).thenReturn(mock())
+            }
+        return mockView
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
new file mode 100644
index 0000000..c0444b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.wrapper
+
+import android.graphics.drawable.AnimatedImageDrawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.MessagingGroup
+import com.android.internal.widget.MessagingImageMessage
+import com.android.internal.widget.MessagingLayout
+import com.android.internal.widget.MessagingLinearLayout
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() {
+
+    private lateinit var mRow: ExpandableNotificationRow
+    private lateinit var helper: NotificationTestHelper
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+        mRow = helper.createRow()
+    }
+
+    @Test
+    fun setAnimationsRunning_Run() {
+        // Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type,
+        // with a mock imageMessage.drawable embedded in its MessagingImageMessage.
+        val mockDrawable = mock<AnimatedImageDrawable>()
+        val mockLayoutView: View = fakeMessagingLayout(mockDrawable)
+
+        val wrapper: NotificationViewWrapper =
+            NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow)
+        wrapper.setAnimationsRunning(true)
+
+        // Verifies that each AnimatedImageDrawable is started animating.
+        verify(mockDrawable).start()
+    }
+
+    @Test
+    fun setAnimationsRunning_Stop() {
+        // Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type,
+        // with a mock imageMessage.drawable embedded in its MessagingImageMessage.
+        val mockDrawable = mock<AnimatedImageDrawable>()
+        val mockLayoutView: View = fakeMessagingLayout(mockDrawable)
+
+        val wrapper: NotificationViewWrapper =
+            NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow)
+        wrapper.setAnimationsRunning(false)
+
+        // Verifies that each AnimatedImageDrawable is started animating.
+        verify(mockDrawable).stop()
+    }
+
+    private fun fakeMessagingLayout(mockDrawable: AnimatedImageDrawable): View {
+        val mockMessagingImageMessage: MessagingImageMessage =
+            mock<MessagingImageMessage>().apply { whenever(drawable).thenReturn(mockDrawable) }
+        val mockMessageContainer: MessagingLinearLayout =
+            mock<MessagingLinearLayout>().apply {
+                whenever(childCount).thenReturn(1)
+                whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage)
+            }
+        val mockGroup: MessagingGroup =
+            mock<MessagingGroup>().apply {
+                whenever(messageContainer).thenReturn(mockMessageContainer)
+            }
+        val mockView: View =
+            mock<MessagingLayout>().apply {
+                whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup)))
+            }
+        return mockView
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 645052f..ff26a43 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -21,6 +21,7 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
@@ -65,6 +66,7 @@
 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl;
 import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
+import com.android.systemui.statusbar.notification.collection.render.NotifStats;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -78,6 +80,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -135,10 +138,14 @@
     @Mock private ShadeTransitionController mShadeTransitionController;
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
+    @Mock private SecureSettings mSecureSettings;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
 
+    private final SeenNotificationsProviderImpl mSeenNotificationsProvider =
+            new SeenNotificationsProviderImpl();
+
     private NotificationStackScrollLayoutController mController;
 
     @Before
@@ -180,14 +187,15 @@
                 mUiEventLogger,
                 mRemoteInputManager,
                 mVisibilityLocationProviderDelegator,
-                new SeenNotificationsProviderImpl(),
+                mSeenNotificationsProvider,
                 mShadeController,
                 mJankMonitor,
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
                 mFeatureFlags,
-                mNotificationTargetsHelper
+                mNotificationTargetsHelper,
+                mSecureSettings
         );
 
         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
@@ -233,16 +241,14 @@
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ true,
-                /* notifVisibleInShade= */ true,
-                /* areSeenNotifsFiltered= */false);
+                /* notifVisibleInShade= */ true);
 
         setupShowEmptyShadeViewState(false);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ false,
-                /* notifVisibleInShade= */ true,
-                /* areSeenNotifsFiltered= */false);
+                /* notifVisibleInShade= */ true);
     }
 
     @Test
@@ -255,16 +261,14 @@
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ true,
-                /* notifVisibleInShade= */ false,
-                /* areSeenNotifsFiltered= */false);
+                /* notifVisibleInShade= */ false);
 
         setupShowEmptyShadeViewState(false);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ false,
-                /* notifVisibleInShade= */ false,
-                /* areSeenNotifsFiltered= */false);
+                /* notifVisibleInShade= */ false);
     }
 
     @Test
@@ -283,16 +287,14 @@
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ true,
-                /* notifVisibleInShade= */ false,
-                /* areSeenNotifsFiltered= */false);
+                /* notifVisibleInShade= */ false);
 
         mController.setQsFullScreen(true);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ true,
-                /* notifVisibleInShade= */ false,
-                /* areSeenNotifsFiltered= */false);
+                /* notifVisibleInShade= */ false);
     }
 
     @Test
@@ -400,6 +402,48 @@
         verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true);
     }
 
+    @Test
+    public void testSetNotifStats_updatesHasFilteredOutSeenNotifications() {
+        when(mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()).thenReturn(true);
+        mSeenNotificationsProvider.setHasFilteredOutSeenNotifications(true);
+        mController.attach(mNotificationStackScrollLayout);
+        mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
+        verify(mNotificationStackScrollLayout).setHasFilteredOutSeenNotifications(true);
+        verify(mNotificationStackScrollLayout).updateFooter();
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(anyBoolean(), anyBoolean());
+    }
+
+    @Test
+    public void testAttach_updatesViewStatusBarState() {
+        // GIVEN: Controller is attached
+        mController.attach(mNotificationStackScrollLayout);
+        ArgumentCaptor<StatusBarStateController.StateListener> captor =
+                ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+        verify(mSysuiStatusBarStateController).addCallback(captor.capture(), anyInt());
+        StatusBarStateController.StateListener stateListener = captor.getValue();
+
+        // WHEN: StatusBarState changes to SHADE
+        when(mSysuiStatusBarStateController.getState()).thenReturn(SHADE);
+        stateListener.onStateChanged(SHADE);
+
+        // THEN: NSSL is updated with the current state
+        verify(mNotificationStackScrollLayout).setStatusBarState(SHADE);
+
+        // WHEN: Controller is detached
+        mController.mOnAttachStateChangeListener
+                .onViewDetachedFromWindow(mNotificationStackScrollLayout);
+
+        // WHEN: StatusBarState changes to KEYGUARD
+        when(mSysuiStatusBarStateController.getState()).thenReturn(KEYGUARD);
+
+        // WHEN: Controller is re-attached
+        mController.mOnAttachStateChangeListener
+                .onViewAttachedToWindow(mNotificationStackScrollLayout);
+
+        // THEN: NSSL is updated with the current state
+        verify(mNotificationStackScrollLayout).setStatusBarState(KEYGUARD);
+    }
+
     private LogMaker logMatcher(int category, int type) {
         return argThat(new LogMatcher(category, type));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 7622549..dd7143a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -30,6 +30,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertFalse;
+import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
@@ -53,6 +54,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextView;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
@@ -328,7 +330,7 @@
     public void updateEmptyView_dndSuppressing() {
         when(mEmptyShadeView.willBeGone()).thenReturn(true);
 
-        mStackScroller.updateEmptyShadeView(true, true, false);
+        mStackScroller.updateEmptyShadeView(true, true);
 
         verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
     }
@@ -338,7 +340,7 @@
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mEmptyShadeView.willBeGone()).thenReturn(true);
 
-        mStackScroller.updateEmptyShadeView(true, false, false);
+        mStackScroller.updateEmptyShadeView(true, false);
 
         verify(mEmptyShadeView).setText(R.string.empty_shade_text);
     }
@@ -347,10 +349,10 @@
     public void updateEmptyView_noNotificationsToDndSuppressing() {
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mEmptyShadeView.willBeGone()).thenReturn(true);
-        mStackScroller.updateEmptyShadeView(true, false, false);
+        mStackScroller.updateEmptyShadeView(true, false);
         verify(mEmptyShadeView).setText(R.string.empty_shade_text);
 
-        mStackScroller.updateEmptyShadeView(true, true, false);
+        mStackScroller.updateEmptyShadeView(true, true);
         verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
     }
 
@@ -818,6 +820,29 @@
         assertEquals(0f, mAmbientState.getStackY());
     }
 
+    @Test
+    public void hasFilteredOutSeenNotifs_updateFooter() {
+        mStackScroller.setCurrentUserSetup(true);
+
+        // add footer
+        mStackScroller.inflateFooterView();
+        TextView footerLabel =
+                mStackScroller.mFooterView.requireViewById(R.id.unlock_prompt_footer);
+
+        mStackScroller.setHasFilteredOutSeenNotifications(true);
+        mStackScroller.updateFooter();
+
+        assertThat(footerLabel.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void hasFilteredOutSeenNotifs_updateEmptyShadeView() {
+        mStackScroller.setHasFilteredOutSeenNotifications(true);
+        mStackScroller.updateEmptyShadeView(true, false);
+
+        verify(mEmptyShadeView).setFooterText(not(0));
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 680a323..78da782 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -20,6 +20,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -227,14 +228,154 @@
     }
 
     @Test
-    public void testHandleUpEvent_menuRow() {
-        when(mSwipeHelper.getCurrentMenuRow()).thenReturn(mMenuRow);
-        doNothing().when(mSwipeHelper).handleMenuRowSwipe(mEvent, mView, 0, mMenuRow);
+    public void testHandleUpEvent_menuRowWithoutMenu_dismiss() {
+        doNothing().when(mSwipeHelper).dismiss(any(), anyFloat());
+        doReturn(true).when(mSwipeHelper).isDismissGesture(any());
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        when(mMenuRow.shouldShowMenu()).thenReturn(true);
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
 
         assertTrue("Menu row exists",
                 mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
         verify(mMenuRow, times(1)).onTouchEnd();
-        verify(mSwipeHelper, times(1)).handleMenuRowSwipe(mEvent, mView, 0, mMenuRow);
+        verify(mSwipeHelper, times(1)).isDismissGesture(mEvent);
+        verify(mSwipeHelper, times(1)).dismiss(mView, 0);
+        verify(mSwipeHelper, never()).isFalseGesture();
+    }
+
+    @Test
+    public void testHandleUpEvent_menuRowWithoutMenu_snapback() {
+        doNothing().when(mSwipeHelper).snapChild(any(), anyInt(), anyFloat());
+        doReturn(false).when(mSwipeHelper).isDismissGesture(any());
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        when(mMenuRow.shouldShowMenu()).thenReturn(true);
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
+
+        assertTrue("Menu row exists",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+        verify(mMenuRow, times(1)).onTouchEnd();
+        verify(mSwipeHelper, times(1)).isDismissGesture(mEvent);
+        verify(mSwipeHelper, times(1)).snapClosed(mView, 0);
+        verify(mMenuRow, times(1)).onSnapClosed();
+        verify(mSwipeHelper, never()).isFalseGesture();
+    }
+
+    @Test
+    public void testHandleUpEvent_menuRowWithOpenMenu_dismissed() {
+        doNothing().when(mSwipeHelper).dismiss(any(), anyFloat());
+        doReturn(true).when(mSwipeHelper).isDismissGesture(any());
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        when(mMenuRow.shouldShowMenu()).thenReturn(true);
+        when(mMenuRow.isSnappedAndOnSameSide()).thenReturn(true);
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
+
+        assertTrue("Menu row exists",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+        verify(mMenuRow, times(1)).onTouchEnd();
+        verify(mSwipeHelper, times(1)).isDismissGesture(mEvent);
+        verify(mSwipeHelper, times(1)).dismiss(mView, 0);
+        verify(mSwipeHelper, never()).isFalseGesture();
+    }
+
+    @Test
+    public void testHandleUpEvent_menuRowWithOpenMenu_snapback() {
+        doNothing().when(mSwipeHelper).snapChild(any(), anyInt(), anyFloat());
+        doReturn(false).when(mSwipeHelper).isDismissGesture(any());
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        when(mMenuRow.shouldShowMenu()).thenReturn(true);
+        when(mMenuRow.isSnappedAndOnSameSide()).thenReturn(true);
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
+
+        assertTrue("Menu row exists",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+        verify(mMenuRow, times(1)).onTouchEnd();
+        verify(mSwipeHelper, times(1)).isDismissGesture(mEvent);
+        verify(mSwipeHelper, times(1)).snapClosed(mView, 0);
+        verify(mMenuRow, times(1)).onSnapClosed();
+        verify(mSwipeHelper, never()).isFalseGesture();
+    }
+
+    @Test
+    public void testHandleUpEvent_menuRowWithClosedMenu_dismissed() {
+        doNothing().when(mSwipeHelper).dismiss(any(), anyFloat());
+        doReturn(true).when(mSwipeHelper).isDismissGesture(any());
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        when(mMenuRow.shouldShowMenu()).thenReturn(true);
+        when(mMenuRow.isSnappedAndOnSameSide()).thenReturn(false);
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
+
+        assertTrue("Menu row exists",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+        verify(mMenuRow, times(1)).onTouchEnd();
+        verify(mSwipeHelper, times(1)).isDismissGesture(mEvent);
+        verify(mSwipeHelper, times(1)).dismiss(mView, 0);
+        verify(mSwipeHelper, never()).isFalseGesture();
+    }
+
+    @Test
+    public void testHandleUpEvent_menuRowWithClosedMenu_snapback() {
+        doNothing().when(mSwipeHelper).snapChild(any(), anyInt(), anyFloat());
+        doReturn(false).when(mSwipeHelper).isDismissGesture(any());
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        when(mMenuRow.shouldShowMenu()).thenReturn(true);
+        when(mMenuRow.isSnappedAndOnSameSide()).thenReturn(false);
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
+
+        assertTrue("Menu row exists",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+        verify(mMenuRow, times(1)).onTouchEnd();
+        verify(mSwipeHelper, times(1)).isDismissGesture(mEvent);
+        verify(mSwipeHelper, times(1)).snapClosed(mView, 0);
+        verify(mMenuRow, times(1)).onSnapClosed();
+        verify(mSwipeHelper, never()).isFalseGesture();
+    }
+
+    @Test
+    public void testIsDismissGesture() {
+        doReturn(false).when(mSwipeHelper).isFalseGesture();
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        doReturn(true).when(mSwipeHelper).swipedFastEnough();
+        when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true);
+        when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP);
+
+        assertTrue("Should be a dismiss gesture", mSwipeHelper.isDismissGesture(mEvent));
+        verify(mSwipeHelper, times(1)).isFalseGesture();
+    }
+
+    @Test
+    public void testIsDismissGesture_falseGesture() {
+        doReturn(true).when(mSwipeHelper).isFalseGesture();
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        doReturn(true).when(mSwipeHelper).swipedFastEnough();
+        when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true);
+        when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP);
+
+        assertFalse("False gesture should stop dismissal", mSwipeHelper.isDismissGesture(mEvent));
+        verify(mSwipeHelper, times(1)).isFalseGesture();
+    }
+
+    @Test
+    public void testIsDismissGesture_farEnough() {
+        doReturn(false).when(mSwipeHelper).isFalseGesture();
+        doReturn(true).when(mSwipeHelper).swipedFarEnough();
+        doReturn(false).when(mSwipeHelper).swipedFastEnough();
+        when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true);
+        when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP);
+
+        assertTrue("Should be a dismissal", mSwipeHelper.isDismissGesture(mEvent));
+        verify(mSwipeHelper, times(1)).isFalseGesture();
+    }
+
+    @Test
+    public void testIsDismissGesture_notFarOrFastEnough() {
+        doReturn(false).when(mSwipeHelper).isFalseGesture();
+        doReturn(false).when(mSwipeHelper).swipedFarEnough();
+        doReturn(false).when(mSwipeHelper).swipedFastEnough();
+        when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true);
+        when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP);
+
+        assertFalse("Should not be a dismissal", mSwipeHelper.isDismissGesture(mEvent));
+        verify(mSwipeHelper, times(1)).isFalseGesture();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 5832569..4d9db8c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -518,7 +518,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = true,
                 fullyVisible = false,
-                headerVisibleAmount = 1f,
+                headerVisibleAmount = 1f
         )
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
         algorithmState.visibleChildren.add(childHunView)
@@ -526,6 +526,7 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -545,7 +546,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = true,
                 fullyVisible = false,
-                headerVisibleAmount = 1f,
+                headerVisibleAmount = 1f
         )
         // Use half of the HUN's height as overlap
         childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat()
@@ -555,6 +556,7 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -578,7 +580,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = true,
                 fullyVisible = true,
-                headerVisibleAmount = 1f,
+                headerVisibleAmount = 1f
         )
         // HUN doesn't overlap with QQS Panel
         childHunView.viewState.yTranslation = ambientState.topPadding +
@@ -589,6 +591,7 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -608,7 +611,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = false,
                 fullyVisible = false,
-                headerVisibleAmount = 0f,
+                headerVisibleAmount = 0f
         )
         childHunView.viewState.yTranslation = 0f
         // Shade is closed, thus childHunView's headerVisibleAmount is 0
@@ -619,6 +622,7 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -638,7 +642,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = false,
                 fullyVisible = false,
-                headerVisibleAmount = 0.5f,
+                headerVisibleAmount = 0.5f
         )
         childHunView.viewState.yTranslation = 0f
         // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1
@@ -650,6 +654,7 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -664,7 +669,7 @@
     private fun createHunViewMock(
             isShadeOpen: Boolean,
             fullyVisible: Boolean,
-            headerVisibleAmount: Float,
+            headerVisibleAmount: Float
     ) =
             mock<ExpandableNotificationRow>().apply {
                 val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
@@ -675,10 +680,7 @@
             }
 
 
-    private fun createHunChildViewState(
-            isShadeOpen: Boolean,
-            fullyVisible: Boolean,
-    ) =
+    private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) =
             ExpandableViewState().apply {
                 // Mock the HUN's height with ambientState.topPadding +
                 // ambientState.stackTranslation
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 091bb54..e680a4e 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,8 +51,9 @@
 
 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.QSHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.SettingObserver;
 import com.android.systemui.qs.external.CustomTile;
@@ -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;
 
@@ -100,7 +104,7 @@
 
     private static final int USER = 0;
 
-    @Mock private QSTileHost mQsTileHost;
+    @Mock private QSHost mQsHost;
     @Mock private AutoAddTracker mAutoAddTracker;
     @Mock private CastController mCastController;
     @Mock private HotspotController mHotspotController;
@@ -140,7 +144,7 @@
                 R.string.safety_quick_settings_tile_class, TEST_CUSTOM_SAFETY_CLASS);
 
         when(mAutoAddTrackerBuilder.build()).thenReturn(mAutoAddTracker);
-        when(mQsTileHost.getUserContext()).thenReturn(mUserContext);
+        when(mQsHost.getUserContext()).thenReturn(mUserContext);
         when(mUserContext.getUser()).thenReturn(UserHandle.of(USER));
         mPackageManager = Mockito.spy(mContext.getPackageManager());
         when(mPackageManager.getPermissionControllerPackageName())
@@ -170,7 +174,7 @@
             WalletController walletController,
             SafetyController safetyController,
             @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) {
-        return new AutoTileManager(context, autoAddTrackerBuilder, mQsTileHost,
+        return new AutoTileManager(context, autoAddTrackerBuilder, mQsHost,
                 Handler.createAsync(TestableLooper.get(this).getLooper()),
                 mSecureSettings,
                 hotspotController,
@@ -355,7 +359,7 @@
             return;
         }
         mAutoTileManager.mNightDisplayCallback.onActivated(true);
-        verify(mQsTileHost).addTile("night");
+        verify(mQsHost).addTile("night");
     }
 
     @Test
@@ -364,7 +368,7 @@
             return;
         }
         mAutoTileManager.mNightDisplayCallback.onActivated(false);
-        verify(mQsTileHost, never()).addTile("night");
+        verify(mQsHost, never()).addTile("night");
     }
 
     @Test
@@ -374,7 +378,7 @@
         }
         mAutoTileManager.mNightDisplayCallback.onAutoModeChanged(
                 ColorDisplayManager.AUTO_MODE_TWILIGHT);
-        verify(mQsTileHost).addTile("night");
+        verify(mQsHost).addTile("night");
     }
 
     @Test
@@ -384,7 +388,7 @@
         }
         mAutoTileManager.mNightDisplayCallback.onAutoModeChanged(
                 ColorDisplayManager.AUTO_MODE_CUSTOM_TIME);
-        verify(mQsTileHost).addTile("night");
+        verify(mQsHost).addTile("night");
     }
 
     @Test
@@ -394,19 +398,19 @@
         }
         mAutoTileManager.mNightDisplayCallback.onAutoModeChanged(
                 ColorDisplayManager.AUTO_MODE_DISABLED);
-        verify(mQsTileHost, never()).addTile("night");
+        verify(mQsHost, never()).addTile("night");
     }
 
     @Test
     public void reduceBrightColorsTileAdded_whenActivated() {
         mAutoTileManager.mReduceBrightColorsCallback.onActivated(true);
-        verify(mQsTileHost).addTile("reduce_brightness");
+        verify(mQsHost).addTile("reduce_brightness");
     }
 
     @Test
     public void reduceBrightColorsTileNotAdded_whenDeactivated() {
         mAutoTileManager.mReduceBrightColorsCallback.onActivated(false);
-        verify(mQsTileHost, never()).addTile("reduce_brightness");
+        verify(mQsHost, never()).addTile("reduce_brightness");
     }
 
     private static List<CastDevice> buildFakeCastDevice(boolean isCasting) {
@@ -419,28 +423,28 @@
     public void castTileAdded_whenDeviceIsCasting() {
         doReturn(buildFakeCastDevice(true)).when(mCastController).getCastDevices();
         mAutoTileManager.mCastCallback.onCastDevicesChanged();
-        verify(mQsTileHost).addTile("cast");
+        verify(mQsHost).addTile("cast");
     }
 
     @Test
     public void castTileNotAdded_whenDeviceIsNotCasting() {
         doReturn(buildFakeCastDevice(false)).when(mCastController).getCastDevices();
         mAutoTileManager.mCastCallback.onCastDevicesChanged();
-        verify(mQsTileHost, never()).addTile("cast");
+        verify(mQsHost, never()).addTile("cast");
     }
 
     @Test
     public void testSettingTileAdded_onChanged() {
         changeValue(TEST_SETTING, 1);
         verify(mAutoAddTracker).setTileAdded(TEST_SPEC);
-        verify(mQsTileHost).addTile(TEST_SPEC);
+        verify(mQsHost).addTile(TEST_SPEC);
     }
 
     @Test
     public void testSettingTileAddedComponentAtEnd_onChanged() {
         changeValue(TEST_SETTING_COMPONENT, 1);
         verify(mAutoAddTracker).setTileAdded(TEST_CUSTOM_SPEC);
-        verify(mQsTileHost).addTile(ComponentName.unflattenFromString(TEST_COMPONENT)
+        verify(mQsHost).addTile(ComponentName.unflattenFromString(TEST_COMPONENT)
             , /* end */ true);
     }
 
@@ -449,14 +453,14 @@
         changeValue(TEST_SETTING, 1);
         changeValue(TEST_SETTING, 2);
         verify(mAutoAddTracker).setTileAdded(TEST_SPEC);
-        verify(mQsTileHost).addTile(TEST_SPEC);
+        verify(mQsHost).addTile(TEST_SPEC);
     }
 
     @Test
     public void testSettingTileNotAdded_onChangedTo0() {
         changeValue(TEST_SETTING, 0);
         verify(mAutoAddTracker, never()).setTileAdded(TEST_SPEC);
-        verify(mQsTileHost, never()).addTile(TEST_SPEC);
+        verify(mQsHost, never()).addTile(TEST_SPEC);
     }
 
     @Test
@@ -465,27 +469,27 @@
 
         changeValue(TEST_SETTING, 1);
         verify(mAutoAddTracker, never()).setTileAdded(TEST_SPEC);
-        verify(mQsTileHost, never()).addTile(TEST_SPEC);
+        verify(mQsHost, never()).addTile(TEST_SPEC);
     }
 
     @Test
     public void testSafetyTileNotAdded_ifPreviouslyAdded() {
         ComponentName safetyComponent = CustomTile.getComponentFromSpec(TEST_CUSTOM_SAFETY_SPEC);
         mAutoTileManager.init();
-        verify(mQsTileHost, times(1)).addTile(safetyComponent, true);
+        verify(mQsHost, times(1)).addTile(safetyComponent, true);
         when(mAutoAddTracker.isAdded(TEST_CUSTOM_SAFETY_SPEC)).thenReturn(true);
         mAutoTileManager.init();
-        verify(mQsTileHost, times(1)).addTile(safetyComponent, true);
+        verify(mQsHost, times(1)).addTile(safetyComponent, true);
     }
 
     @Test
     public void testSafetyTileAdded_onUserChange() {
         ComponentName safetyComponent = CustomTile.getComponentFromSpec(TEST_CUSTOM_SAFETY_SPEC);
         mAutoTileManager.init();
-        verify(mQsTileHost, times(1)).addTile(safetyComponent, true);
+        verify(mQsHost, times(1)).addTile(safetyComponent, true);
         when(mAutoAddTracker.isAdded(TEST_CUSTOM_SAFETY_SPEC)).thenReturn(false);
         mAutoTileManager.changeUser(UserHandle.of(USER + 1));
-        verify(mQsTileHost, times(2)).addTile(safetyComponent, true);
+        verify(mQsHost, times(2)).addTile(safetyComponent, true);
     }
 
     @Test
@@ -494,22 +498,23 @@
         mAutoTileManager.init();
         when(mAutoAddTracker.isAdded(TEST_CUSTOM_SAFETY_SPEC)).thenReturn(true);
         mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(false);
-        verify(mQsTileHost, times(1)).removeTile(TEST_CUSTOM_SAFETY_SPEC);
+        verify(mQsHost, times(1)).removeTile(TEST_CUSTOM_SAFETY_SPEC);
     }
 
     @Test
     public void testSafetyTileAdded_onSafetyCenterEnable() {
         ComponentName safetyComponent = CustomTile.getComponentFromSpec(TEST_CUSTOM_SAFETY_SPEC);
         mAutoTileManager.init();
-        verify(mQsTileHost, times(1)).addTile(safetyComponent, true);
+        verify(mQsHost, times(1)).addTile(safetyComponent, true);
         mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(false);
         mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(true);
-        verify(mQsTileHost, times(2)).addTile(safetyComponent, true);
+        verify(mQsHost, times(2)).addTile(safetyComponent, true);
     }
 
     @Test
     public void managedProfileAdded_tileAdded() {
         when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(false);
+        when(mAutoAddTracker.getRestoredTilePosition(eq("work"))).thenReturn(2);
         mAutoTileManager = createAutoTileManager(mContext);
         Mockito.doAnswer((Answer<Object>) invocation -> {
             mManagedProfileCallback = invocation.getArgument(0);
@@ -520,7 +525,7 @@
 
         mManagedProfileCallback.onManagedProfileChanged();
 
-        verify(mQsTileHost, times(1)).addTile(eq("work"));
+        verify(mQsHost, times(1)).addTile(eq("work"), eq(2));
         verify(mAutoAddTracker, times(1)).setTileAdded(eq("work"));
     }
 
@@ -537,11 +542,66 @@
 
         mManagedProfileCallback.onManagedProfileChanged();
 
-        verify(mQsTileHost, times(1)).removeTile(eq("work"));
+        verify(mQsHost, times(1)).removeTile(eq("work"));
         verify(mAutoAddTracker, times(1)).setTileRemoved(eq("work"));
     }
 
     @Test
+    public void testAddControlsTileIfNotPresent() {
+        String spec = DEVICE_CONTROLS;
+        when(mAutoAddTracker.isAdded(eq(spec))).thenReturn(false);
+        when(mQsHost.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(mQsHost).addTile(spec, 3);
+        verify(mAutoAddTracker).setTileAdded(spec);
+    }
+
+    @Test
+    public void testDontAddControlsTileIfPresent() {
+        String spec = DEVICE_CONTROLS;
+        when(mAutoAddTracker.isAdded(eq(spec))).thenReturn(false);
+        when(mQsHost.getTiles()).thenReturn(new ArrayList<>());
+
+        mAutoTileManager.init();
+        ArgumentCaptor<DeviceControlsController.Callback> captor =
+                ArgumentCaptor.forClass(DeviceControlsController.Callback.class);
+
+        verify(mDeviceControlsController).setCallback(captor.capture());
+
+        captor.getValue().removeControlsAutoTracker();
+        verify(mQsHost, 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(mQsHost.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(mQsHost, 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/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 74f8c61..97226e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -57,6 +59,7 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -122,6 +125,7 @@
     private VibratorHelper mVibratorHelper;
     @Mock
     private BiometricUnlockLogger mLogger;
+    private final FakeSystemClock mSystemClock = new FakeSystemClock();
     private BiometricUnlockController mBiometricUnlockController;
 
     @Before
@@ -144,7 +148,9 @@
                 mMetricsLogger, mDumpManager, mPowerManager, mLogger,
                 mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
                 mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController,
-                mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper);
+                mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
+                mSystemClock
+        );
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener);
         when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
@@ -207,7 +213,7 @@
 
         verify(mKeyguardViewMediator).onWakeAndUnlocking();
         assertThat(mBiometricUnlockController.getMode())
-                .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
+                .isEqualTo(MODE_WAKE_AND_UNLOCK);
     }
 
     @Test
@@ -457,4 +463,103 @@
         // THEN wakeup the device
         verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
     }
+
+    @Test
+    public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() {
+        // GIVEN side fingerprint enrolled, last wake reason was power button
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+
+        // GIVEN last wake time just occurred
+        when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+        // WHEN biometric fingerprint succeeds
+        givenFingerprintModeUnlockCollapsing();
+        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+                true);
+
+        // THEN DO NOT vibrate the device
+        verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString());
+    }
+
+    @Test
+    public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
+        // GIVEN side fingerprint enrolled, last wake reason was power button
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+
+        // GIVEN last wake time was 500ms ago
+        when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+        mSystemClock.advanceTime(500);
+
+        // WHEN biometric fingerprint succeeds
+        givenFingerprintModeUnlockCollapsing();
+        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+                true);
+
+        // THEN vibrate the device
+        verify(mVibratorHelper).vibrateAuthSuccess(anyString());
+    }
+
+    @Test
+    public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() {
+        // GIVEN side fingerprint enrolled, wakeup just happened
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+        // GIVEN last wake reason was from a gesture
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_GESTURE);
+
+        // WHEN biometric fingerprint succeeds
+        givenFingerprintModeUnlockCollapsing();
+        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+                true);
+
+        // THEN vibrate the device
+        verify(mVibratorHelper).vibrateAuthSuccess(anyString());
+    }
+
+    @Test
+    public void onSideFingerprintFail_alwaysPlaysHaptic() {
+        // GIVEN side fingerprint enrolled, last wake reason was recent power button
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+        when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+        // WHEN biometric fingerprint fails
+        mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+
+        // THEN always vibrate the device
+        verify(mVibratorHelper).vibrateAuthError(anyString());
+    }
+
+    @Test
+    public void onFingerprintDetect_showBouncer() {
+        // WHEN fingerprint detect occurs
+        mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT,
+                BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
+
+        // THEN shows primary bouncer
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+    }
+
+    @Test
+    public void onFaceDetect_showBouncer() {
+        // WHEN face detect occurs
+        mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT,
+                BiometricSourceType.FACE, false /* isStrongBiometric */);
+
+        // THEN shows primary bouncer
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+    }
+
+    private void givenFingerprintModeUnlockCollapsing() {
+        when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+        when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index 3fccd37..2e6f62c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -25,8 +25,10 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.StatusBarManager;
 import android.os.PowerManager;
+import android.os.UserHandle;
 import android.os.Vibrator;
 import android.testing.AndroidTestingRunner;
 import android.view.InsetsVisibilities;
@@ -41,8 +43,11 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger;
@@ -70,6 +75,7 @@
     @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private ShadeController mShadeController;
     @Mock private CommandQueue mCommandQueue;
+    @Mock private QuickSettingsController mQuickSettingsController;
     @Mock private NotificationPanelViewController mNotificationPanelViewController;
     @Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
     private final MetricsLogger mMetricsLogger = new FakeMetricsLogger();
@@ -88,6 +94,8 @@
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private SystemBarAttributesListener mSystemBarAttributesListener;
     @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
+    @Mock private UserTracker mUserTracker;
+    @Mock private QSHost mQSHost;
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -97,6 +105,7 @@
 
         mSbcqCallbacks = new CentralSurfacesCommandQueueCallbacks(
                 mCentralSurfaces,
+                mQuickSettingsController,
                 mContext,
                 mContext.getResources(),
                 mShadeController,
@@ -120,8 +129,12 @@
                 new DisableFlagsLogger(),
                 DEFAULT_DISPLAY,
                 mSystemBarAttributesListener,
-                mCameraLauncherLazy);
+                mCameraLauncherLazy,
+                mUserTracker,
+                mQSHost);
 
+        when(mUserTracker.getUserHandle()).thenReturn(
+                UserHandle.of(ActivityManager.getCurrentUser()));
         when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
         when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt()))
                 .thenAnswer((Answer<Integer>) invocation -> invocation.getArgument(0));
@@ -140,7 +153,7 @@
 
         // Trying to open it does nothing.
         mSbcqCallbacks.animateExpandNotificationsPanel();
-        verify(mNotificationPanelViewController, never()).expandWithoutQs();
+        verify(mNotificationPanelViewController, never()).expandShadeToNotifications();
         mSbcqCallbacks.animateExpandSettingsPanel(null);
         verify(mNotificationPanelViewController, never()).expand(anyBoolean());
     }
@@ -158,7 +171,7 @@
 
         // Can now be opened.
         mSbcqCallbacks.animateExpandNotificationsPanel();
-        verify(mNotificationPanelViewController).expandWithoutQs();
+        verify(mNotificationPanelViewController).expandShadeToNotifications();
         mSbcqCallbacks.animateExpandSettingsPanel(null);
         verify(mNotificationPanelViewController).expandWithQs();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 09254ad..5a5b142 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -44,6 +44,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.IWallpaperManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -110,6 +111,7 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
@@ -117,12 +119,14 @@
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelView;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
+import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeControllerImpl;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -177,8 +181,6 @@
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.startingsurface.StartingSurface;
 
-import dagger.Lazy;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -191,6 +193,8 @@
 import java.io.PrintWriter;
 import java.util.Optional;
 
+import dagger.Lazy;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
@@ -215,6 +219,7 @@
     @Mock private HeadsUpManagerPhone mHeadsUpManager;
     @Mock private NotificationPanelViewController mNotificationPanelViewController;
     @Mock private NotificationPanelView mNotificationPanelView;
+    @Mock private QuickSettingsController mQuickSettingsController;
     @Mock private IStatusBarService mBarService;
     @Mock private IDreamManager mDreamManager;
     @Mock private LightRevealScrimViewModel mLightRevealScrimViewModel;
@@ -297,6 +302,7 @@
     @Mock private WiredChargingRippleController mWiredChargingRippleController;
     @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
     @Mock private CameraLauncher mCameraLauncher;
+    @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
     /**
      * The process of registering/unregistering a predictive back callback requires a
      * ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test.
@@ -304,6 +310,7 @@
      */
     @Mock private ViewRootImpl mViewRootImpl;
     @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
+    @Mock private UserTracker mUserTracker;
     @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
     @Mock IPowerManager mPowerManagerService;
 
@@ -319,6 +326,10 @@
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
+        // CentralSurfacesImpl's runtime flag check fails if the flag is absent.
+        // This value is unused, because test manifest is opted in.
+        mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI, false);
+
         IThermalService thermalService = mock(IThermalService.class);
         mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,
                 Handler.createAsync(Looper.myLooper()));
@@ -336,7 +347,8 @@
                         new Handler(TestableLooper.get(this).getLooper()),
                         mock(NotifPipelineFlags.class),
                         mock(KeyguardNotificationVisibilityProvider.class),
-                        mock(UiEventLogger.class));
+                        mock(UiEventLogger.class),
+                        mUserTracker);
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -378,7 +390,8 @@
         }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any());
 
         mWakefulnessLifecycle =
-                new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager);
+                new WakefulnessLifecycle(mContext, mIWallpaperManager, mFakeSystemClock,
+                        mDumpManager);
         mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
         mWakefulnessLifecycle.dispatchFinishedWakingUp();
 
@@ -416,6 +429,9 @@
 
         when(mOperatorNameViewControllerFactory.create(any()))
                 .thenReturn(mOperatorNameViewController);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+        when(mUserTracker.getUserHandle()).thenReturn(
+                UserHandle.of(ActivityManager.getCurrentUser()));
 
         mCentralSurfaces = new CentralSurfacesImpl(
                 mContext,
@@ -504,7 +520,10 @@
                 mWiredChargingRippleController,
                 mDreamManager,
                 mCameraLauncherLazy,
-                () -> mLightRevealScrimViewModel) {
+                () -> mLightRevealScrimViewModel,
+                mAlternateBouncerInteractor,
+                mUserTracker
+        ) {
             @Override
             protected ViewRootImpl getViewRootImpl() {
                 return mViewRootImpl;
@@ -529,6 +548,7 @@
         // initialized automatically and make NPVC private.
         mCentralSurfaces.mNotificationShadeWindowView = mNotificationShadeWindowView;
         mCentralSurfaces.mNotificationPanelViewController = mNotificationPanelViewController;
+        mCentralSurfaces.mQsController = mQuickSettingsController;
         mCentralSurfaces.mDozeScrimController = mDozeScrimController;
         mCentralSurfaces.mPresenter = mNotificationPresenter;
         mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController;
@@ -539,6 +559,7 @@
         mCentralSurfaces.startKeyguard();
         mInitController.executePostInitTasks();
         notificationLogger.setUpWithContainer(mNotificationListContainer);
+        mCentralSurfaces.registerCallbacks();
     }
 
     @Test
@@ -992,6 +1013,29 @@
     }
 
     @Test
+    public void testSetDozingNotUnlocking_transitionToAOD_cancelKeyguardFadingAway() {
+        setDozing(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true);
+
+        mCentralSurfaces.updateScrimController();
+
+        verify(mScrimController, times(2)).transitionTo(eq(ScrimState.AOD));
+        verify(mStatusBarKeyguardViewManager).onKeyguardFadedAway();
+    }
+
+    @Test
+    public void testSetDozingNotUnlocking_transitionToAuthScrimmed_cancelKeyguardFadingAway() {
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true);
+
+        mCentralSurfaces.updateScrimController();
+
+        verify(mScrimController).transitionTo(eq(ScrimState.AUTH_SCRIMMED_SHADE));
+        verify(mStatusBarKeyguardViewManager).onKeyguardFadedAway();
+    }
+
+    @Test
     public void testShowKeyguardImplementation_setsState() {
         when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>());
 
@@ -1274,7 +1318,8 @@
                 Handler mainHandler,
                 NotifPipelineFlags flags,
                 KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-                UiEventLogger uiEventLogger) {
+                UiEventLogger uiEventLogger,
+                UserTracker userTracker) {
             super(
                     contentResolver,
                     powerManager,
@@ -1288,7 +1333,8 @@
                     mainHandler,
                     flags,
                     keyguardNotificationVisibilityProvider,
-                    uiEventLogger
+                    uiEventLogger,
+                    userTracker
             );
             mUseHeadsUp = true;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
index 077b41a..eb5edbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
@@ -23,8 +23,13 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.content.res.Resources;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Handler;
@@ -39,10 +44,10 @@
 import com.android.systemui.doze.AlwaysOnDisplayPolicy;
 import com.android.systemui.doze.DozeScreenState;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 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.tuner.TunerService;
 import com.android.systemui.unfold.FoldAodAnimationController;
@@ -52,6 +57,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -69,7 +76,6 @@
     @Mock private PowerManager mPowerManager;
     @Mock private TunerService mTunerService;
     @Mock private BatteryController mBatteryController;
-    @Mock private FeatureFlags mFeatureFlags;
     @Mock private DumpManager mDumpManager;
     @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private FoldAodAnimationController mFoldAodAnimationController;
@@ -78,6 +84,8 @@
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private StatusBarStateController mStatusBarStateController;
     @Mock private ConfigurationController mConfigurationController;
+    @Mock private UserTracker mUserTracker;
+    @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback;
 
     /**
      * The current value of PowerManager's dozeAfterScreenOff property.
@@ -102,6 +110,7 @@
 
         when(mSysUIUnfoldComponent.getFoldAodAnimationController())
                 .thenReturn(mFoldAodAnimationController);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
         mDozeParameters = new DozeParameters(
             mContext,
@@ -113,16 +122,17 @@
             mBatteryController,
             mTunerService,
             mDumpManager,
-            mFeatureFlags,
             mScreenOffAnimationController,
             Optional.of(mSysUIUnfoldComponent),
             mUnlockedScreenOffAnimationController,
             mKeyguardUpdateMonitor,
             mConfigurationController,
-            mStatusBarStateController
+            mStatusBarStateController,
+            mUserTracker
         );
 
-        when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(true);
+        verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture());
+
         setAodEnabledForTest(true);
         setShouldControlUnlockedScreenOffForTest(true);
         setDisplayNeedsBlankingForTest(false);
@@ -173,6 +183,29 @@
         assertThat(mDozeParameters.getAlwaysOn()).isFalse();
     }
 
+    @Test
+    public void testGetAlwaysOn_whenBatterySaverCallback() {
+        DozeParameters.Callback callback = mock(DozeParameters.Callback.class);
+        mDozeParameters.addCallback(callback);
+
+        when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mBatteryController.isAodPowerSave()).thenReturn(true);
+
+        // Both lines should trigger an event
+        mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1");
+        mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true);
+
+        verify(callback, times(2)).onAlwaysOnChange();
+        assertThat(mDozeParameters.getAlwaysOn()).isFalse();
+
+        reset(callback);
+        when(mBatteryController.isAodPowerSave()).thenReturn(false);
+        mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true);
+
+        verify(callback).onAlwaysOnChange();
+        assertThat(mDozeParameters.getAlwaysOn()).isTrue();
+    }
+
     /**
      * PowerManager.setDozeAfterScreenOff(true) means we are not controlling screen off, and calling
      * it with false means we are. Confusing, but sure - make sure that we call PowerManager with
@@ -196,17 +229,6 @@
     }
 
     @Test
-    public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() {
-        when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false);
-
-        assertFalse(mDozeParameters.shouldControlUnlockedScreenOff());
-
-        // Trigger the setter for the current value.
-        mDozeParameters.setControlScreenOffAnimation(mDozeParameters.shouldControlScreenOff());
-        assertFalse(mDozeParameters.shouldControlScreenOff());
-    }
-
-    @Test
     public void propagatesAnimateScreenOff_noAlwaysOn() {
         setAodEnabledForTest(false);
         setDisplayNeedsBlankingForTest(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
index a986777..c669c6f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
@@ -53,7 +53,7 @@
     }
 
     @Override
-    public boolean isBouncerShowing() {
+    public boolean isPrimaryBouncerShowing() {
         return false;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
deleted file mode 100644
index df7ee43..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.phone;
-
-import static com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN;
-import static com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.hardware.biometrics.BiometricSourceType;
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardHostViewController;
-import com.android.keyguard.KeyguardSecurityModel;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.ViewMediatorCallback;
-import com.android.keyguard.dagger.KeyguardBouncerComponent;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class KeyguardBouncerTest extends SysuiTestCase {
-
-    @Mock
-    private FalsingCollector mFalsingCollector;
-    @Mock
-    private ViewMediatorCallback mViewMediatorCallback;
-    @Mock
-    private DismissCallbackRegistry mDismissCallbackRegistry;
-    @Mock
-    private KeyguardHostViewController mKeyguardHostViewController;
-    @Mock
-    private KeyguardBouncer.PrimaryBouncerExpansionCallback mExpansionCallback;
-    @Mock
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock
-    private KeyguardStateController mKeyguardStateController;
-    @Mock
-    private KeyguardBypassController mKeyguardBypassController;
-    @Mock
-    private Handler mHandler;
-    @Mock
-    private KeyguardSecurityModel mKeyguardSecurityModel;
-    @Mock
-    private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
-    @Mock
-    private KeyguardBouncerComponent mKeyguardBouncerComponent;
-    private ViewGroup mContainer;
-    @Rule
-    public MockitoRule mRule = MockitoJUnit.rule();
-    private Integer mRootVisibility = View.INVISIBLE;
-    private KeyguardBouncer mBouncer;
-
-    @Before
-    public void setup() {
-        allowTestableLooperAsMainThread();
-        when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
-                .thenReturn(KeyguardSecurityModel.SecurityMode.None);
-        DejankUtils.setImmediate(true);
-
-        mContainer = spy(new FrameLayout(getContext()));
-        when(mKeyguardBouncerComponentFactory.create(mContainer)).thenReturn(
-                mKeyguardBouncerComponent);
-        when(mKeyguardBouncerComponent.getKeyguardHostViewController())
-                .thenReturn(mKeyguardHostViewController);
-
-        mBouncer = new KeyguardBouncer.Factory(getContext(), mViewMediatorCallback,
-                mDismissCallbackRegistry, mFalsingCollector,
-                mKeyguardStateController, mKeyguardUpdateMonitor,
-                mKeyguardBypassController, mHandler, mKeyguardSecurityModel,
-                mKeyguardBouncerComponentFactory)
-                .create(mContainer, mExpansionCallback);
-    }
-
-    @Test
-    public void testInflateView_doesntCrash() {
-        mBouncer.inflateView();
-    }
-
-    @Test
-    public void testShow_notifiesFalsingManager() {
-        mBouncer.show(true);
-        verify(mFalsingCollector).onBouncerShown();
-
-        mBouncer.show(true, false);
-        verifyNoMoreInteractions(mFalsingCollector);
-    }
-
-    /**
-     * Regression test: Invisible bouncer when occluded.
-     */
-    @Test
-    public void testShow_bouncerIsVisible() {
-        // Expand notification panel as if we were in the keyguard.
-        mBouncer.ensureView();
-        mBouncer.setExpansion(1);
-
-        reset(mKeyguardHostViewController);
-
-        mBouncer.show(true);
-        verify(mKeyguardHostViewController).setExpansion(0);
-    }
-
-    @Test
-    public void testShow_notifiesVisibility() {
-        mBouncer.show(true);
-        verify(mKeyguardStateController).notifyBouncerShowing(eq(true));
-        verify(mExpansionCallback).onStartingToShow();
-
-        // Not called again when visible
-        reset(mViewMediatorCallback);
-        mBouncer.show(true);
-        verifyNoMoreInteractions(mViewMediatorCallback);
-    }
-
-    @Test
-    public void testShow_triesToDismissKeyguard() {
-        mBouncer.show(true);
-        verify(mKeyguardHostViewController).dismiss(anyInt());
-    }
-
-    @Test
-    public void testShow_resetsSecuritySelection() {
-        mBouncer.show(false);
-        verify(mKeyguardHostViewController, never()).showPrimarySecurityScreen();
-
-        mBouncer.hide(false);
-        mBouncer.show(true);
-        verify(mKeyguardHostViewController).showPrimarySecurityScreen();
-    }
-
-    @Test
-    public void testShow_animatesKeyguardView() {
-        mBouncer.show(true);
-        verify(mKeyguardHostViewController).appear(anyInt());
-    }
-
-    @Test
-    public void testShow_showsErrorMessage() {
-        final String errorMessage = "an error message";
-        when(mViewMediatorCallback.consumeCustomMessage()).thenReturn(errorMessage);
-        mBouncer.show(true);
-        verify(mKeyguardHostViewController).showErrorMessage(eq(errorMessage));
-    }
-
-    @Test
-    public void testSetExpansion_notifiesFalsingManager() {
-        mBouncer.ensureView();
-        mBouncer.setExpansion(0.5f);
-
-        mBouncer.setExpansion(EXPANSION_HIDDEN);
-        verify(mFalsingCollector).onBouncerHidden();
-        verify(mExpansionCallback).onFullyHidden();
-
-        mBouncer.setExpansion(EXPANSION_VISIBLE);
-        verify(mFalsingCollector).onBouncerShown();
-        verify(mExpansionCallback).onFullyShown();
-
-        verify(mExpansionCallback, never()).onStartingToHide();
-        verify(mKeyguardHostViewController, never()).onStartingToHide();
-        mBouncer.setExpansion(0.9f);
-        verify(mExpansionCallback).onStartingToHide();
-        verify(mKeyguardHostViewController).onStartingToHide();
-    }
-
-    @Test
-    public void testSetExpansion_notifiesKeyguardView() {
-        mBouncer.ensureView();
-        mBouncer.setExpansion(0.1f);
-
-        mBouncer.setExpansion(0);
-        verify(mKeyguardHostViewController).onResume();
-        verify(mContainer).announceForAccessibility(any());
-    }
-
-    @Test
-    public void show_notifiesKeyguardViewController() {
-        mBouncer.ensureView();
-
-        mBouncer.show(/* resetSecuritySelection= */ false);
-
-        verify(mKeyguardHostViewController).onBouncerVisibilityChanged(View.VISIBLE);
-    }
-
-    @Test
-    public void testHide_notifiesFalsingManager() {
-        mBouncer.hide(false);
-        verify(mFalsingCollector).onBouncerHidden();
-    }
-
-    @Test
-    public void testHide_notifiesVisibility() {
-        mBouncer.hide(false);
-        verify(mKeyguardStateController).notifyBouncerShowing(eq(false));
-    }
-
-    @Test
-    public void testHide_notifiesDismissCallbackIfVisible() {
-        mBouncer.hide(false);
-        verifyZeroInteractions(mDismissCallbackRegistry);
-        mBouncer.show(false);
-        mBouncer.hide(false);
-        verify(mDismissCallbackRegistry).notifyDismissCancelled();
-    }
-
-    @Test
-    public void testHide_notShowingAnymore() {
-        mBouncer.ensureView();
-        mBouncer.show(false /* resetSecuritySelection */);
-        mBouncer.hide(false /* destroyViews */);
-        Assert.assertFalse("Not showing", mBouncer.isShowing());
-    }
-
-    @Test
-    public void testShowPromptReason_propagates() {
-        mBouncer.ensureView();
-        mBouncer.showPromptReason(1);
-        verify(mKeyguardHostViewController).showPromptReason(eq(1));
-    }
-
-    @Test
-    public void testShowMessage_propagates() {
-        final String message = "a message";
-        mBouncer.ensureView();
-        mBouncer.showMessage(message, ColorStateList.valueOf(Color.GREEN));
-        verify(mKeyguardHostViewController).showMessage(
-                eq(message), eq(ColorStateList.valueOf(Color.GREEN)));
-    }
-
-    @Test
-    public void testShowOnDismissAction_showsBouncer() {
-        final OnDismissAction dismissAction = () -> false;
-        final Runnable cancelAction = () -> {};
-        mBouncer.showWithDismissAction(dismissAction, cancelAction);
-        verify(mKeyguardHostViewController).setOnDismissAction(dismissAction, cancelAction);
-        Assert.assertTrue("Should be showing", mBouncer.isShowing());
-    }
-
-    @Test
-    public void testStartPreHideAnimation_notifiesView() {
-        final boolean[] ran = {false};
-        final Runnable r = () -> ran[0] = true;
-        mBouncer.startPreHideAnimation(r);
-        Assert.assertTrue("Callback should have been invoked", ran[0]);
-
-        ran[0] = false;
-        mBouncer.ensureView();
-        mBouncer.startPreHideAnimation(r);
-        verify(mKeyguardHostViewController).startDisappearAnimation(r);
-        Assert.assertFalse("Callback should have been deferred", ran[0]);
-    }
-
-    @Test
-    public void testIsShowing_animated() {
-        Assert.assertFalse("Show wasn't invoked yet", mBouncer.isShowing());
-        mBouncer.show(true /* reset */);
-        Assert.assertTrue("Should be showing", mBouncer.isShowing());
-    }
-
-    @Test
-    public void testIsShowing_forSwipeUp() {
-        mBouncer.setExpansion(1f);
-        mBouncer.show(true /* reset */, false /* animated */);
-        Assert.assertFalse("Should only be showing after collapsing notification panel",
-                mBouncer.isShowing());
-        mBouncer.setExpansion(0f);
-        Assert.assertTrue("Should be showing", mBouncer.isShowing());
-    }
-
-    @Test
-    public void testSetExpansion() {
-        mBouncer.ensureView();
-        mBouncer.setExpansion(0.5f);
-        verify(mKeyguardHostViewController).setExpansion(0.5f);
-    }
-
-    @Test
-    public void testIsFullscreenBouncer_asksKeyguardView() {
-        mBouncer.ensureView();
-        mBouncer.isFullscreenBouncer();
-        verify(mKeyguardHostViewController).getCurrentSecurityMode();
-    }
-
-    @Test
-    public void testIsHiding_preHideOrHide() {
-        Assert.assertFalse("Should not be hiding on initial state", mBouncer.isAnimatingAway());
-        mBouncer.startPreHideAnimation(null /* runnable */);
-        Assert.assertTrue("Should be hiding during pre-hide", mBouncer.isAnimatingAway());
-        mBouncer.hide(false /* destroyView */);
-        Assert.assertFalse("Should be hidden after hide()", mBouncer.isAnimatingAway());
-    }
-
-    @Test
-    public void testIsHiding_skipsTranslation() {
-        mBouncer.show(false /* reset */);
-        reset(mKeyguardHostViewController);
-        mBouncer.startPreHideAnimation(null /* runnable */);
-        mBouncer.setExpansion(0.5f);
-        verify(mKeyguardHostViewController, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void testIsSecure() {
-        mBouncer.ensureView();
-        for (KeyguardSecurityModel.SecurityMode mode : KeyguardSecurityModel.SecurityMode.values()){
-            reset(mKeyguardSecurityModel);
-            when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(mode);
-            Assert.assertEquals("Security doesn't match for mode: " + mode,
-                    mBouncer.isSecure(), mode != KeyguardSecurityModel.SecurityMode.None);
-        }
-    }
-
-    @Test
-    public void testIsShowingScrimmed_true() {
-        doAnswer(invocation -> {
-            assertThat(mBouncer.isScrimmed()).isTrue();
-            return null;
-        }).when(mExpansionCallback).onFullyShown();
-        mBouncer.show(false /* resetSecuritySelection */, true /* animate */);
-        assertThat(mBouncer.isScrimmed()).isTrue();
-        mBouncer.hide(false /* destroyView */);
-        assertThat(mBouncer.isScrimmed()).isFalse();
-    }
-
-    @Test
-    public void testIsShowingScrimmed_false() {
-        doAnswer(invocation -> {
-            assertThat(mBouncer.isScrimmed()).isFalse();
-            return null;
-        }).when(mExpansionCallback).onFullyShown();
-        mBouncer.show(false /* resetSecuritySelection */, false /* animate */);
-        assertThat(mBouncer.isScrimmed()).isFalse();
-    }
-
-    @Test
-    public void testWillDismissWithAction() {
-        mBouncer.ensureView();
-        Assert.assertFalse("Action not set yet", mBouncer.willDismissWithAction());
-        when(mKeyguardHostViewController.hasDismissActions()).thenReturn(true);
-        Assert.assertTrue("Action should exist", mBouncer.willDismissWithAction());
-    }
-
-    @Test
-    public void testShow_delaysIfFaceAuthIsRunning() {
-        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
-                .thenReturn(true);
-        when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
-        mBouncer.show(true /* reset */);
-
-        ArgumentCaptor<Runnable> showRunnable = ArgumentCaptor.forClass(Runnable.class);
-        verify(mHandler).postDelayed(showRunnable.capture(),
-                eq(KeyguardBouncer.BOUNCER_FACE_DELAY));
-
-        mBouncer.hide(false /* destroyView */);
-        verify(mHandler).removeCallbacks(eq(showRunnable.getValue()));
-    }
-
-    @Test
-    public void testShow_doesNotDelaysIfFaceAuthIsNotAllowed() {
-        when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
-        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
-                .thenReturn(false);
-        mBouncer.show(true /* reset */);
-
-        verify(mHandler, never()).postDelayed(any(), anyLong());
-    }
-
-    @Test
-    public void testShow_delaysIfFaceAuthIsRunning_unlessBypassEnabled() {
-        when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
-        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
-        mBouncer.show(true /* reset */);
-
-        verify(mHandler, never()).postDelayed(any(), anyLong());
-    }
-
-    @Test
-    public void testShow_delaysIfFaceAuthIsRunning_unlessFingerprintEnrolled() {
-        when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
-        when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0))
-                .thenReturn(true);
-        mBouncer.show(true /* reset */);
-
-        verify(mHandler, never()).postDelayed(any(), anyLong());
-    }
-
-    @Test
-    public void testRegisterUpdateMonitorCallback() {
-        verify(mKeyguardUpdateMonitor).registerCallback(any());
-    }
-
-    @Test
-    public void testInTransit_whenTranslation() {
-        mBouncer.show(true);
-        mBouncer.setExpansion(EXPANSION_HIDDEN);
-        assertThat(mBouncer.inTransit()).isFalse();
-        mBouncer.setExpansion(0.5f);
-        assertThat(mBouncer.inTransit()).isTrue();
-        mBouncer.setExpansion(EXPANSION_VISIBLE);
-        assertThat(mBouncer.inTransit()).isFalse();
-    }
-
-    @Test
-    public void testUpdateResources_delegatesToRootView() {
-        mBouncer.ensureView();
-        mBouncer.updateResources();
-
-        // This is mocked, so won't pick up on the call to updateResources via
-        // mKeyguardViewController.init(), only updateResources above.
-        verify(mKeyguardHostViewController).updateResources();
-    }
-
-    @Test
-    public void testUpdateKeyguardPosition_delegatesToRootView() {
-        mBouncer.ensureView();
-        mBouncer.updateKeyguardPosition(1.0f);
-
-        verify(mKeyguardHostViewController).updateKeyguardPosition(1.0f);
-    }
-
-    @Test
-    public void testExpansion_notifiesCallback() {
-        mBouncer.ensureView();
-        mBouncer.setExpansion(0.5f);
-
-        final PrimaryBouncerExpansionCallback callback =
-                mock(PrimaryBouncerExpansionCallback.class);
-        mBouncer.addBouncerExpansionCallback(callback);
-
-        mBouncer.setExpansion(EXPANSION_HIDDEN);
-        verify(callback).onFullyHidden();
-        verify(callback).onExpansionChanged(EXPANSION_HIDDEN);
-
-        Mockito.clearInvocations(callback);
-        mBouncer.setExpansion(EXPANSION_VISIBLE);
-        verify(callback).onFullyShown();
-        verify(callback).onExpansionChanged(EXPANSION_VISIBLE);
-
-        Mockito.clearInvocations(callback);
-        float bouncerHideAmount = 0.9f;
-        // Ensure the callback only triggers once despite multiple calls to setExpansion
-        // with the same value.
-        mBouncer.setExpansion(bouncerHideAmount);
-        mBouncer.setExpansion(bouncerHideAmount);
-        verify(callback, times(1)).onStartingToHide();
-        verify(callback, times(1)).onExpansionChanged(bouncerHideAmount);
-
-        Mockito.clearInvocations(callback);
-        mBouncer.removeBouncerExpansionCallback(callback);
-        bouncerHideAmount = 0.5f;
-        mBouncer.setExpansion(bouncerHideAmount);
-        verify(callback, never()).onExpansionChanged(bouncerHideAmount);
-    }
-
-    @Test
-    public void testOnResumeCalledForFullscreenBouncerOnSecondShow() {
-        // GIVEN a security mode which requires fullscreen bouncer
-        when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
-                .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
-        mBouncer.show(true);
-
-        // WHEN a second call to show occurs, the bouncer will already by visible
-        reset(mKeyguardHostViewController);
-        mBouncer.show(true);
-
-        // THEN ensure the ViewController is told to resume
-        verify(mKeyguardHostViewController).onResume();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
new file mode 100644
index 0000000..3e90ed9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.phone
+
+import android.content.pm.PackageManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_FLIPPED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.tuner.TunerService
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class KeyguardBypassControllerTest : SysuiTestCase() {
+
+    private lateinit var keyguardBypassController: KeyguardBypassController
+    private lateinit var postureControllerCallback: DevicePostureController.Callback
+    @Mock private lateinit var tunerService: TunerService
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock private lateinit var devicePostureController: DevicePostureController
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var packageManager: PackageManager
+    @Captor
+    private val postureCallbackCaptor =
+        ArgumentCaptor.forClass(DevicePostureController.Callback::class.java)
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        context.setMockPackageManager(packageManager)
+        whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true)
+        whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
+    }
+
+    @After
+    fun tearDown() {
+        reset(devicePostureController)
+        reset(keyguardStateController)
+    }
+
+    private fun defaultConfigPostureClosed() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_auth_supported_posture,
+            DEVICE_POSTURE_CLOSED
+        )
+        initKeyguardBypassController()
+        verify(devicePostureController).addCallback(postureCallbackCaptor.capture())
+        postureControllerCallback = postureCallbackCaptor.value
+    }
+
+    private fun defaultConfigPostureOpened() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_auth_supported_posture,
+            DEVICE_POSTURE_OPENED
+        )
+        initKeyguardBypassController()
+        verify(devicePostureController).addCallback(postureCallbackCaptor.capture())
+        postureControllerCallback = postureCallbackCaptor.value
+    }
+
+    private fun defaultConfigPostureFlipped() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_auth_supported_posture,
+            DEVICE_POSTURE_FLIPPED
+        )
+        initKeyguardBypassController()
+        verify(devicePostureController).addCallback(postureCallbackCaptor.capture())
+        postureControllerCallback = postureCallbackCaptor.value
+    }
+
+    private fun defaultConfigPostureUnknown() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_auth_supported_posture,
+            DEVICE_POSTURE_UNKNOWN
+        )
+        initKeyguardBypassController()
+        verify(devicePostureController, never()).addCallback(postureCallbackCaptor.capture())
+    }
+
+    private fun initKeyguardBypassController() {
+        keyguardBypassController =
+            KeyguardBypassController(
+                context,
+                tunerService,
+                statusBarStateController,
+                lockscreenUserManager,
+                keyguardStateController,
+                shadeExpansionStateManager,
+                devicePostureController,
+                dumpManager
+            )
+    }
+
+    @Test
+    fun configDevicePostureClosed_matchState_isPostureAllowedForFaceAuth_returnTrue() {
+        defaultConfigPostureClosed()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue()
+    }
+
+    @Test
+    fun configDevicePostureOpen_matchState_isPostureAllowedForFaceAuth_returnTrue() {
+        defaultConfigPostureOpened()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue()
+    }
+
+    @Test
+    fun configDevicePostureFlipped_matchState_isPostureAllowedForFaceAuth_returnTrue() {
+        defaultConfigPostureFlipped()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue()
+    }
+
+    @Test
+    fun configDevicePostureClosed_changeOpened_isPostureAllowedForFaceAuth_returnFalse() {
+        defaultConfigPostureClosed()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+    }
+
+    @Test
+    fun configDevicePostureClosed_changeFlipped_isPostureAllowedForFaceAuth_returnFalse() {
+        defaultConfigPostureClosed()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+    }
+
+    @Test
+    fun configDevicePostureOpened_changeClosed_isPostureAllowedForFaceAuth_returnFalse() {
+        defaultConfigPostureOpened()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+    }
+
+    @Test
+    fun configDevicePostureOpened_changeFlipped_isPostureAllowedForFaceAuth_returnFalse() {
+        defaultConfigPostureOpened()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+    }
+
+    @Test
+    fun configDevicePostureFlipped_changeClosed_isPostureAllowedForFaceAuth_returnFalse() {
+        defaultConfigPostureFlipped()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+    }
+
+    @Test
+    fun configDevicePostureFlipped_changeOpened_isPostureAllowedForFaceAuth_returnFalse() {
+        defaultConfigPostureFlipped()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+        assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+    }
+
+    @Test
+    fun defaultConfigPostureClosed_canOverrideByPassAlways_shouldReturnFalse() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_unlock_bypass_override,
+            1 /* FACE_UNLOCK_BYPASS_ALWAYS */
+        )
+
+        defaultConfigPostureClosed()
+
+        postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+        assertThat(keyguardBypassController.bypassEnabled).isFalse()
+    }
+
+    @Test
+    fun defaultConfigPostureUnknown_canNotOverrideByPassAlways_shouldReturnTrue() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_unlock_bypass_override,
+            1 /* FACE_UNLOCK_BYPASS_ALWAYS */
+        )
+
+        defaultConfigPostureUnknown()
+
+        assertThat(keyguardBypassController.bypassEnabled).isTrue()
+    }
+
+    @Test
+    fun defaultConfigPostureUnknown_canNotOverrideByPassNever_shouldReturnFalse() {
+        context.orCreateTestableResources.addOverride(
+            R.integer.config_face_unlock_bypass_override,
+            2 /* FACE_UNLOCK_BYPASS_NEVER */
+        )
+
+        defaultConfigPostureUnknown()
+
+        assertThat(keyguardBypassController.bypassEnabled).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index ed3f710..7e69efa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -264,6 +264,19 @@
     }
 
     @Test
+    public void notifPositionAlignedWithClockAndBurnInOffsetInSplitShadeMode() {
+        setSplitShadeTopMargin(100); // this makes clock to be at 100
+        givenAOD();
+        mIsSplitShade = true;
+        givenMaxBurnInOffset(100);
+        givenHighestBurnInOffset(); // this makes clock to be at 200
+        // WHEN the position algorithm is run
+        positionClock();
+        // THEN the notif padding adjusts for burn-in offset: clock position - burn-in offset
+        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(100);
+    }
+
+    @Test
     public void clockPositionedDependingOnMarginInSplitShade() {
         setSplitShadeTopMargin(400);
         givenLockScreen();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index cc4abfc..529519a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -38,6 +38,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.policy.BatteryController;
 
 import org.junit.Before;
@@ -67,7 +68,8 @@
                 mStatusBarIconController,
                 mock(BatteryController.class),
                 mock(NavigationModeController.class),
-                mock(DumpManager.class));
+                mock(DumpManager.class),
+                new FakeDisplayTracker(mContext));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
index 189aa0f..0a68406 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
@@ -28,6 +28,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController.DarkIntensityApplier;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -56,7 +57,8 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mLightBarTransitionsController = new LightBarTransitionsController(mContext, mApplier,
-                new CommandQueue(mContext), mKeyguardStateController, mStatusBarStateController);
+                new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
+                mKeyguardStateController, mStatusBarStateController);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index 64dee95..6b18169 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.phone
 
 import android.app.AlarmManager
-import android.app.IActivityManager
 import android.app.admin.DevicePolicyManager
 import android.content.SharedPreferences
 import android.os.UserManager
@@ -31,6 +30,7 @@
 import com.android.systemui.privacy.PrivacyItemController
 import com.android.systemui.privacy.logging.PrivacyLogger
 import com.android.systemui.screenrecord.RecordingController
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.BluetoothController
 import com.android.systemui.statusbar.policy.CastController
@@ -71,61 +71,36 @@
         private const val ALARM_SLOT = "alarm"
     }
 
-    @Mock
-    private lateinit var iconController: StatusBarIconController
-    @Mock
-    private lateinit var commandQueue: CommandQueue
-    @Mock
-    private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock
-    private lateinit var castController: CastController
-    @Mock
-    private lateinit var hotspotController: HotspotController
-    @Mock
-    private lateinit var bluetoothController: BluetoothController
-    @Mock
-    private lateinit var nextAlarmController: NextAlarmController
-    @Mock
-    private lateinit var userInfoController: UserInfoController
-    @Mock
-    private lateinit var rotationLockController: RotationLockController
-    @Mock
-    private lateinit var dataSaverController: DataSaverController
-    @Mock
-    private lateinit var zenModeController: ZenModeController
-    @Mock
-    private lateinit var deviceProvisionedController: DeviceProvisionedController
-    @Mock
-    private lateinit var keyguardStateController: KeyguardStateController
-    @Mock
-    private lateinit var locationController: LocationController
-    @Mock
-    private lateinit var sensorPrivacyController: SensorPrivacyController
-    @Mock
-    private lateinit var iActivityManager: IActivityManager
-    @Mock
-    private lateinit var alarmManager: AlarmManager
-    @Mock
-    private lateinit var userManager: UserManager
-    @Mock
-    private lateinit var devicePolicyManager: DevicePolicyManager
-    @Mock
-    private lateinit var recordingController: RecordingController
-    @Mock
-    private lateinit var telecomManager: TelecomManager
-    @Mock
-    private lateinit var sharedPreferences: SharedPreferences
-    @Mock
-    private lateinit var dateFormatUtil: DateFormatUtil
+    @Mock private lateinit var iconController: StatusBarIconController
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var castController: CastController
+    @Mock private lateinit var hotspotController: HotspotController
+    @Mock private lateinit var bluetoothController: BluetoothController
+    @Mock private lateinit var nextAlarmController: NextAlarmController
+    @Mock private lateinit var userInfoController: UserInfoController
+    @Mock private lateinit var rotationLockController: RotationLockController
+    @Mock private lateinit var dataSaverController: DataSaverController
+    @Mock private lateinit var zenModeController: ZenModeController
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var locationController: LocationController
+    @Mock private lateinit var sensorPrivacyController: SensorPrivacyController
+    @Mock private lateinit var alarmManager: AlarmManager
+    @Mock private lateinit var userManager: UserManager
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var recordingController: RecordingController
+    @Mock private lateinit var telecomManager: TelecomManager
+    @Mock private lateinit var sharedPreferences: SharedPreferences
+    @Mock private lateinit var dateFormatUtil: DateFormatUtil
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private lateinit var ringerModeTracker: RingerModeTracker
-    @Mock
-    private lateinit var privacyItemController: PrivacyItemController
-    @Mock
-    private lateinit var privacyLogger: PrivacyLogger
+    @Mock private lateinit var privacyItemController: PrivacyItemController
+    @Mock private lateinit var privacyLogger: PrivacyLogger
     @Captor
     private lateinit var alarmCallbackCaptor:
-            ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>
+        ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>
 
     private lateinit var executor: FakeExecutor
     private lateinit var statusBarPolicy: PhoneStatusBarPolicy
@@ -137,8 +112,8 @@
         executor = FakeExecutor(FakeSystemClock())
         testableLooper = TestableLooper.get(this)
         context.orCreateTestableResources.addOverride(
-                com.android.internal.R.string.status_bar_alarm_clock,
-                ALARM_SLOT
+            com.android.internal.R.string.status_bar_alarm_clock,
+            ALARM_SLOT
         )
         statusBarPolicy = createStatusBarPolicy()
     }
@@ -195,36 +170,37 @@
 
     private fun createStatusBarPolicy(): PhoneStatusBarPolicy {
         return PhoneStatusBarPolicy(
-                iconController,
-                commandQueue,
-                broadcastDispatcher,
-                executor,
-                testableLooper.looper,
-                context.resources,
-                castController,
-                hotspotController,
-                bluetoothController,
-                nextAlarmController,
-                userInfoController,
-                rotationLockController,
-                dataSaverController,
-                zenModeController,
-                deviceProvisionedController,
-                keyguardStateController,
-                locationController,
-                sensorPrivacyController,
-                iActivityManager,
-                alarmManager,
-                userManager,
-                devicePolicyManager,
-                recordingController,
-                telecomManager,
-                /* displayId = */ 0,
-                sharedPreferences,
-                dateFormatUtil,
-                ringerModeTracker,
-                privacyItemController,
-                privacyLogger
+            iconController,
+            commandQueue,
+            broadcastDispatcher,
+            executor,
+            executor,
+            testableLooper.looper,
+            context.resources,
+            castController,
+            hotspotController,
+            bluetoothController,
+            nextAlarmController,
+            userInfoController,
+            rotationLockController,
+            dataSaverController,
+            zenModeController,
+            deviceProvisionedController,
+            keyguardStateController,
+            locationController,
+            sensorPrivacyController,
+            alarmManager,
+            userManager,
+            userTracker,
+            devicePolicyManager,
+            recordingController,
+            telecomManager,
+            /* displayId = */ 0,
+            sharedPreferences,
+            dateFormatUtil,
+            ringerModeTracker,
+            privacyItemController,
+            privacyLogger
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index e475905..180d9f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -24,6 +24,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
@@ -58,6 +60,12 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
+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.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.statusbar.policy.FakeConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -84,8 +92,10 @@
 import java.util.HashSet;
 import java.util.Map;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+
 @RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class ScrimControllerTest extends SysuiTestCase {
 
@@ -114,6 +124,10 @@
     @Mock private DockManager mDockManager;
     @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
+    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    @Mock private CoroutineDispatcher mMainDispatcher;
+
     // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
     //   event-dispatch-on-registration pattern caused some of these unit tests to fail.)
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -224,13 +238,20 @@
         when(mDelayedWakeLockBuilder.build()).thenReturn(mWakeLock);
         when(mDockManager.isDocked()).thenReturn(false);
 
+        when(mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition())
+                .thenReturn(emptyFlow());
+        when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha()).thenReturn(emptyFlow());
+
         mScrimController = new ScrimController(mLightBarController,
                 mDozeParameters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder,
                 new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor,
                 mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
                 mScreenOffAnimationController,
                 mKeyguardUnlockAnimationController,
-                mStatusBarKeyguardViewManager);
+                mStatusBarKeyguardViewManager,
+                mPrimaryBouncerToGoneTransitionViewModel,
+                mKeyguardTransitionInteractor,
+                mMainDispatcher);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
@@ -629,6 +650,7 @@
 
     @Test
     public void transitionToUnlocked() {
+        mScrimController.setClipsQsScrim(false);
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -644,14 +666,21 @@
         ));
 
         // Back scrim should be visible after start dragging
-        mScrimController.setRawPanelExpansionFraction(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.29f);
         assertScrimAlpha(Map.of(
                 mScrimInFront, TRANSPARENT,
                 mNotificationsScrim, TRANSPARENT,
                 mScrimBehind, SEMI_TRANSPARENT));
 
+        // Back scrim should be opaque at 30%
+        mScrimController.setRawPanelExpansionFraction(0.3f);
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, OPAQUE));
+
         // Then, notification scrim should fade in
-        mScrimController.setRawPanelExpansionFraction(0.7f);
+        mScrimController.setRawPanelExpansionFraction(0.31f);
         assertScrimAlpha(Map.of(
                 mScrimInFront, TRANSPARENT,
                 mNotificationsScrim, SEMI_TRANSPARENT,
@@ -852,7 +881,10 @@
                 mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
                 mScreenOffAnimationController,
                 mKeyguardUnlockAnimationController,
-                mStatusBarKeyguardViewManager);
+                mStatusBarKeyguardViewManager,
+                mPrimaryBouncerToGoneTransitionViewModel,
+                mKeyguardTransitionInteractor,
+                mMainDispatcher);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
@@ -1347,33 +1379,10 @@
     }
 
     @Test
-    public void notificationAlpha_unnocclusionAnimating_bouncerActive_usesKeyguardNotifAlpha() {
-        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
-        mScrimController.setClipsQsScrim(true);
-
-        mScrimController.transitionTo(ScrimState.KEYGUARD);
-        mScrimController.setUnocclusionAnimationRunning(true);
-
-        assertAlphaAfterExpansion(
-                mNotificationsScrim, ScrimState.KEYGUARD.getNotifAlpha(), /* expansion */ 0f);
-        assertAlphaAfterExpansion(
-                mNotificationsScrim, ScrimState.KEYGUARD.getNotifAlpha(), /* expansion */ 0.4f);
-        assertAlphaAfterExpansion(
-                mNotificationsScrim, ScrimState.KEYGUARD.getNotifAlpha(), /* expansion */ 1.0f);
-
-        // Verify normal behavior after
-        mScrimController.setUnocclusionAnimationRunning(false);
-        float expansion = 0.4f;
-        float alpha = 1 - BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion);
-        assertAlphaAfterExpansion(mNotificationsScrim, alpha, expansion);
-    }
-
-    @Test
     public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
         mScrimController.transitionTo(ScrimState.KEYGUARD);
-        mScrimController.setUnocclusionAnimationRunning(true);
 
         assertAlphaAfterExpansion(
                 mNotificationsScrim, ScrimState.KEYGUARD.getNotifAlpha(), /* expansion */ 0f);
@@ -1383,7 +1392,6 @@
                 mNotificationsScrim, ScrimState.KEYGUARD.getNotifAlpha(), /* expansion */ 1.0f);
 
         // Verify normal behavior after
-        mScrimController.setUnocclusionAnimationRunning(false);
         float expansion = 0.4f;
         float alpha = 1 - ShadeInterpolation.getNotificationScrimAlpha(expansion);
         assertAlphaAfterExpansion(mNotificationsScrim, alpha, expansion);
@@ -1562,7 +1570,7 @@
     @Test
     public void transitionToDreaming() {
         mScrimController.setRawPanelExpansionFraction(0f);
-        mScrimController.setBouncerHiddenFraction(KeyguardBouncer.EXPANSION_HIDDEN);
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
         mScrimController.transitionTo(ScrimState.DREAMING);
         finishAnimationsImmediately();
 
@@ -1589,7 +1597,6 @@
 
     @Test
     public void setUnOccludingAnimationKeyguard() {
-        mScrimController.setUnocclusionAnimationRunning(true);
         mScrimController.transitionTo(ScrimState.KEYGUARD);
         finishAnimationsImmediately();
         assertThat(mNotificationsScrim.getViewAlpha())
@@ -1645,6 +1652,18 @@
         assertScrimAlpha(mScrimBehind, 0);
     }
 
+    @Test
+    public void ignoreTransitionRequestWhileKeyguardTransitionRunning() {
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        mScrimController.mPrimaryBouncerToGoneTransition.accept(
+                new TransitionStep(KeyguardState.PRIMARY_BOUNCER, KeyguardState.GONE, 0f,
+                        TransitionState.RUNNING, "ScrimControllerTest"));
+
+        // This request should not happen
+        mScrimController.transitionTo(ScrimState.BOUNCER);
+        assertThat(mScrimController.getState()).isEqualTo(ScrimState.UNLOCKED);
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
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/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 14a319b..158e9ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -16,7 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.flags.Flags.MODERN_BOUNCER;
+import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
+import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
 
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +38,10 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
 import android.window.OnBackInvokedCallback;
 import android.window.OnBackInvokedDispatcher;
 import android.window.WindowOnBackInvokedDispatcher;
@@ -54,13 +59,18 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.BouncerView;
 import com.android.systemui.keyguard.data.BouncerViewDelegate;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.navigationbar.TaskbarDelegate;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -102,10 +112,8 @@
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private View mNotificationContainer;
     @Mock private KeyguardBypassController mBypassController;
-    @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
     @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
     @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
-    @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
     @Mock private KeyguardMessageArea mKeyguardMessageArea;
     @Mock private ShadeController mShadeController;
     @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
@@ -115,18 +123,26 @@
     @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
     @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
     @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
     @Mock private BouncerView mBouncerView;
     @Mock private BouncerViewDelegate mBouncerViewDelegate;
+    @Mock private OnBackAnimationCallback mBouncerViewDelegateBackCallback;
+    @Mock private NotificationShadeWindowView mNotificationShadeWindowView;
+    @Mock private WindowInsetsController mWindowInsetsController;
+    @Mock private TaskbarDelegate mTaskbarDelegate;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
+    private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
+            mBouncerExpansionCallback;
     private FakeKeyguardStateController mKeyguardStateController =
             spy(new FakeKeyguardStateController());
 
-    @Mock private ViewRootImpl mViewRootImpl;
-    @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
+    @Mock
+    private ViewRootImpl mViewRootImpl;
+    @Mock
+    private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
     @Captor
-    private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
+    private ArgumentCaptor<OnBackInvokedCallback> mBackCallbackCaptor;
 
 
     @Before
@@ -137,8 +153,15 @@
         when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
                 .thenReturn(mKeyguardMessageAreaController);
         when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
+        when(mBouncerViewDelegate.getBackCallback()).thenReturn(mBouncerViewDelegateBackCallback);
+        when(mFeatureFlags
+                .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM))
+                .thenReturn(true);
 
-        when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(true);
+        when(mCentralSurfaces.getNotificationShadeWindowView())
+                .thenReturn(mNotificationShadeWindowView);
+        when(mNotificationShadeWindowView.getWindowInsetsController())
+                .thenReturn(mWindowInsetsController);
 
         mStatusBarKeyguardViewManager =
                 new StatusBarKeyguardViewManager(
@@ -154,7 +177,6 @@
                         mock(NotificationShadeWindowController.class),
                         mKeyguardStateController,
                         mock(NotificationMediaManager.class),
-                        mKeyguardBouncerFactory,
                         mKeyguardMessageAreaFactory,
                         Optional.of(mSysUiUnfoldComponent),
                         () -> mShadeController,
@@ -163,7 +185,8 @@
                         mFeatureFlags,
                         mPrimaryBouncerCallbackInteractor,
                         mPrimaryBouncerInteractor,
-                        mBouncerView) {
+                        mBouncerView,
+                        mAlternateBouncerInteractor) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
@@ -179,8 +202,8 @@
                 mNotificationContainer,
                 mBypassController);
         mStatusBarKeyguardViewManager.show(null);
-        ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
-                ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
+        ArgumentCaptor<PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
+                ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
         verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
                 callbackArgumentCaptor.capture());
         mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
@@ -189,7 +212,8 @@
     @Test
     public void dismissWithAction_AfterKeyguardGoneSetToFalse() {
         OnDismissAction action = () -> false;
-        Runnable cancelAction = () -> {};
+        Runnable cancelAction = () -> {
+        };
         mStatusBarKeyguardViewManager.dismissWithAction(
                 action, cancelAction, false /* afterKeyguardGone */);
         verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction));
@@ -253,7 +277,7 @@
         when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true);
 
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
+        verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(EXPANSION_HIDDEN));
     }
 
     @Test
@@ -281,7 +305,7 @@
                 .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+                        /* fraction= */ EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
         verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -298,7 +322,7 @@
                 .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+                        /* fraction= */ EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
         verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -309,7 +333,7 @@
         when(mKeyguardStateController.isOccluded()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+                        /* fraction= */ EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
         verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -326,7 +350,7 @@
                 .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+                        /* fraction= */ EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
         verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -337,24 +361,13 @@
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+                        /* fraction= */ EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
         verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
     }
 
     @Test
-    public void setOccluded_animatesPanelExpansion_onlyIfBouncerHidden() {
-        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
-        verify(mCentralSurfaces).animateKeyguardUnoccluding();
-
-        when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        clearInvocations(mCentralSurfaces);
-        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
-        verify(mCentralSurfaces, never()).animateKeyguardUnoccluding();
-    }
-
-    @Test
     public void setOccluded_onKeyguardOccludedChangedCalled() {
         clearInvocations(mKeyguardStateController);
         clearInvocations(mKeyguardUpdateMonitor);
@@ -434,37 +447,35 @@
 
     @Test
     public void testShowing_whenAlternateAuthShowing() {
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
-        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
         assertTrue(
-                "Is showing not accurate when alternative auth showing",
+                "Is showing not accurate when alternative bouncer is visible",
                 mStatusBarKeyguardViewManager.isBouncerShowing());
     }
 
     @Test
     public void testWillBeShowing_whenAlternateAuthShowing() {
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
-        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
         assertTrue(
-                "Is or will be showing not accurate when alternative auth showing",
+                "Is or will be showing not accurate when alternate bouncer is visible",
                 mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
     }
 
     @Test
-    public void testHideAlternateBouncer_onShowBouncer() {
-        // GIVEN alt auth is showing
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+    public void testHideAlternateBouncer_onShowPrimaryBouncer() {
+        reset(mAlternateBouncerInteractor);
+
+        // GIVEN alt bouncer is showing
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
-        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
-        reset(mAlternateBouncer);
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
 
         // WHEN showBouncer is called
         mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
 
         // THEN alt bouncer should be hidden
-        verify(mAlternateBouncer).hideAlternateBouncer();
+        verify(mAlternateBouncerInteractor).hide();
     }
 
     @Test
@@ -479,11 +490,9 @@
 
     @Test
     public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
-        // GIVEN alt auth exists, unlocking with biometric isn't allowed
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        // GIVEN cannot use alternate bouncer
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
-        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
-                .thenReturn(false);
+        when(mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()).thenReturn(false);
 
         // WHEN showGenericBouncer is called
         final boolean scrimmed = true;
@@ -491,21 +500,19 @@
 
         // THEN regular bouncer is shown
         verify(mPrimaryBouncerInteractor).show(eq(scrimmed));
-        verify(mAlternateBouncer, never()).showAlternateBouncer();
     }
 
     @Test
     public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
-        // GIVEN alt auth exists, unlocking with biometric is allowed
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        // GIVEN will show alternate bouncer
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
-        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+        when(mAlternateBouncerInteractor.show()).thenReturn(true);
 
         // WHEN showGenericBouncer is called
         mStatusBarKeyguardViewManager.showBouncer(true);
 
         // THEN alt auth bouncer is shown
-        verify(mAlternateBouncer).showAlternateBouncer();
+        verify(mAlternateBouncerInteractor).show();
         verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
     }
 
@@ -543,12 +550,12 @@
         mBouncerExpansionCallback.onVisibilityChanged(true);
         verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
                 eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
-                mOnBackInvokedCallback.capture());
+                mBackCallbackCaptor.capture());
 
         /* verify that the same callback is unregistered when the bouncer becomes invisible */
         mBouncerExpansionCallback.onVisibilityChanged(false);
         verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(
-                eq(mOnBackInvokedCallback.getValue()));
+                eq(mBackCallbackCaptor.getValue()));
     }
 
     @Test
@@ -557,18 +564,63 @@
         /* capture the predictive back callback during registration */
         verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
                 eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
-                mOnBackInvokedCallback.capture());
+                mBackCallbackCaptor.capture());
 
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
         when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
         /* invoke the back callback directly */
-        mOnBackInvokedCallback.getValue().onBackInvoked();
+        mBackCallbackCaptor.getValue().onBackInvoked();
 
         /* verify that the bouncer will be hidden as a result of the invocation */
         verify(mCentralSurfaces).setBouncerShowing(eq(false));
     }
 
     @Test
+    public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() {
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
+                .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
+        mBouncerExpansionCallback.onVisibilityChanged(true);
+        /* capture the predictive back callback during registration */
+        verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                mBackCallbackCaptor.capture());
+        assertTrue(mBackCallbackCaptor.getValue() instanceof OnBackAnimationCallback);
+
+        OnBackAnimationCallback backCallback =
+                (OnBackAnimationCallback) mBackCallbackCaptor.getValue();
+
+        BackEvent event = new BackEvent(0, 0, 0, BackEvent.EDGE_LEFT);
+        backCallback.onBackStarted(event);
+        verify(mBouncerViewDelegateBackCallback, never()).onBackStarted(any());
+    }
+
+    @Test
+    public void testPredictiveBackCallback_forwardsBackDispatches() {
+        mBouncerExpansionCallback.onVisibilityChanged(true);
+        /* capture the predictive back callback during registration */
+        verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                mBackCallbackCaptor.capture());
+        assertTrue(mBackCallbackCaptor.getValue() instanceof OnBackAnimationCallback);
+
+        OnBackAnimationCallback backCallback =
+                (OnBackAnimationCallback) mBackCallbackCaptor.getValue();
+
+        BackEvent event = new BackEvent(0, 0, 0, BackEvent.EDGE_LEFT);
+        backCallback.onBackStarted(event);
+        verify(mBouncerViewDelegateBackCallback).onBackStarted(eq(event));
+
+        backCallback.onBackProgressed(event);
+        verify(mBouncerViewDelegateBackCallback).onBackProgressed(eq(event));
+
+        backCallback.onBackInvoked();
+        verify(mBouncerViewDelegateBackCallback).onBackInvoked();
+
+        backCallback.onBackCancelled();
+        verify(mBouncerViewDelegateBackCallback).onBackCancelled();
+    }
+
+    @Test
     public void testReportBouncerOnDreamWhenVisible() {
         mBouncerExpansionCallback.onVisibilityChanged(true);
         verify(mCentralSurfaces).setBouncerShowingOverDream(false);
@@ -589,6 +641,14 @@
     }
 
     @Test
+    public void testHideTaskbar() {
+        when(mTaskbarDelegate.isInitialized()).thenReturn(true);
+        mStatusBarKeyguardViewManager.setTaskbarDelegate(mTaskbarDelegate);
+        mStatusBarKeyguardViewManager.updateNavigationBarVisibility(false);
+        verify(mWindowInsetsController).hide(WindowInsets.Type.navigationBars());
+    }
+
+    @Test
     public void hideAlternateBouncer_beforeCentralSurfacesRegistered() {
         mStatusBarKeyguardViewManager =
                 new StatusBarKeyguardViewManager(
@@ -604,7 +664,6 @@
                         mock(NotificationShadeWindowController.class),
                         mKeyguardStateController,
                         mock(NotificationMediaManager.class),
-                        mKeyguardBouncerFactory,
                         mKeyguardMessageAreaFactory,
                         Optional.of(mSysUiUnfoldComponent),
                         () -> mShadeController,
@@ -613,7 +672,8 @@
                         mFeatureFlags,
                         mPrimaryBouncerCallbackInteractor,
                         mPrimaryBouncerInteractor,
-                        mBouncerView) {
+                        mBouncerView,
+                        mAlternateBouncerInteractor) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java
deleted file mode 100644
index 96fba39..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java
+++ /dev/null
@@ -1,641 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone;
-
-import static com.android.systemui.flags.Flags.MODERN_BOUNCER;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewRootImpl;
-import android.window.OnBackInvokedCallback;
-import android.window.OnBackInvokedDispatcher;
-import android.window.WindowOnBackInvokedDispatcher;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.util.LatencyTracker;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.keyguard.KeyguardMessageArea;
-import com.android.keyguard.KeyguardMessageAreaController;
-import com.android.keyguard.KeyguardSecurityModel;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.ViewMediatorCallback;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dock.DockManager;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.keyguard.data.BouncerView;
-import com.android.systemui.keyguard.data.BouncerViewDelegate;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
-import com.android.systemui.navigationbar.NavigationModeController;
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.shade.NotificationPanelViewController;
-import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeExpansionChangeEvent;
-import com.android.systemui.shade.ShadeExpansionStateManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.unfold.SysUIUnfoldComponent;
-
-import com.google.common.truth.Truth;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-/**
- * StatusBarKeyguardViewManager Test with deprecated KeyguardBouncer.java.
- * TODO: Delete when deleting {@link KeyguardBouncer}
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase {
-    private static final ShadeExpansionChangeEvent EXPANSION_EVENT =
-            expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true);
-
-    @Mock private ViewMediatorCallback mViewMediatorCallback;
-    @Mock private LockPatternUtils mLockPatternUtils;
-    @Mock private CentralSurfaces mCentralSurfaces;
-    @Mock private ViewGroup mContainer;
-    @Mock private NotificationPanelViewController mNotificationPanelView;
-    @Mock private BiometricUnlockController mBiometricUnlockController;
-    @Mock private SysuiStatusBarStateController mStatusBarStateController;
-    @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock private View mNotificationContainer;
-    @Mock private KeyguardBypassController mBypassController;
-    @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
-    @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
-    @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
-    @Mock private KeyguardBouncer mPrimaryBouncer;
-    @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
-    @Mock private KeyguardMessageArea mKeyguardMessageArea;
-    @Mock private ShadeController mShadeController;
-    @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
-    @Mock private DreamOverlayStateController mDreamOverlayStateController;
-    @Mock private LatencyTracker mLatencyTracker;
-    @Mock private FeatureFlags mFeatureFlags;
-    @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
-    @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
-    @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
-    @Mock private BouncerView mBouncerView;
-    @Mock private BouncerViewDelegate mBouncerViewDelegate;
-
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
-    private FakeKeyguardStateController mKeyguardStateController =
-            spy(new FakeKeyguardStateController());
-
-    @Mock private ViewRootImpl mViewRootImpl;
-    @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
-    @Captor
-    private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
-
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mKeyguardBouncerFactory.create(
-                any(ViewGroup.class),
-                any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class)))
-                .thenReturn(mPrimaryBouncer);
-        when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer);
-        when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
-        when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
-                .thenReturn(mKeyguardMessageAreaController);
-        when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
-
-        mStatusBarKeyguardViewManager =
-                new StatusBarKeyguardViewManager(
-                        getContext(),
-                        mViewMediatorCallback,
-                        mLockPatternUtils,
-                        mStatusBarStateController,
-                        mock(ConfigurationController.class),
-                        mKeyguardUpdateMonitor,
-                        mDreamOverlayStateController,
-                        mock(NavigationModeController.class),
-                        mock(DockManager.class),
-                        mock(NotificationShadeWindowController.class),
-                        mKeyguardStateController,
-                        mock(NotificationMediaManager.class),
-                        mKeyguardBouncerFactory,
-                        mKeyguardMessageAreaFactory,
-                        Optional.of(mSysUiUnfoldComponent),
-                        () -> mShadeController,
-                        mLatencyTracker,
-                        mKeyguardSecurityModel,
-                        mFeatureFlags,
-                        mPrimaryBouncerCallbackInteractor,
-                        mPrimaryBouncerInteractor,
-                        mBouncerView) {
-                    @Override
-                    public ViewRootImpl getViewRootImpl() {
-                        return mViewRootImpl;
-                    }
-                };
-        when(mViewRootImpl.getOnBackInvokedDispatcher())
-                .thenReturn(mOnBackInvokedDispatcher);
-        mStatusBarKeyguardViewManager.registerCentralSurfaces(
-                mCentralSurfaces,
-                mNotificationPanelView,
-                new ShadeExpansionStateManager(),
-                mBiometricUnlockController,
-                mNotificationContainer,
-                mBypassController);
-        mStatusBarKeyguardViewManager.show(null);
-        ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
-                ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
-        verify(mKeyguardBouncerFactory).create(any(ViewGroup.class),
-                callbackArgumentCaptor.capture());
-        mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
-    }
-
-    @Test
-    public void dismissWithAction_AfterKeyguardGoneSetToFalse() {
-        OnDismissAction action = () -> false;
-        Runnable cancelAction = () -> {};
-        mStatusBarKeyguardViewManager.dismissWithAction(
-                action, cancelAction, false /* afterKeyguardGone */);
-        verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction));
-    }
-
-    @Test
-    public void showBouncer_onlyWhenShowing() {
-        mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
-        verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
-        verify(mPrimaryBouncer, never()).show(anyBoolean());
-    }
-
-    @Test
-    public void showBouncer_notWhenBouncerAlreadyShowing() {
-        mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
-        when(mPrimaryBouncer.isSecure()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
-        verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
-        verify(mPrimaryBouncer, never()).show(anyBoolean());
-    }
-
-    @Test
-    public void showBouncer_showsTheBouncer() {
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
-        verify(mPrimaryBouncer).show(anyBoolean(), eq(true));
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverShowsDuringHintAnimation() {
-        when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void onPanelExpansionChanged_propagatesToBouncerOnlyIfShowing() {
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
-
-        when(mPrimaryBouncer.isShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(/* fraction= */ 0.6f, /* expanded= */ false, /* tracking= */ true));
-        verify(mPrimaryBouncer).setExpansion(eq(0.6f));
-    }
-
-    @Test
-    public void onPanelExpansionChanged_duplicateEventsAreIgnored() {
-        when(mPrimaryBouncer.isShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer).setExpansion(eq(0.5f));
-
-        reset(mPrimaryBouncer);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
-    }
-
-    @Test
-    public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() {
-        mStatusBarKeyguardViewManager.hide(0, 0);
-        when(mPrimaryBouncer.inTransit()).thenReturn(true);
-
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
-    }
-
-    @Test
-    public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
-        mKeyguardStateController.setCanDismissLockScreen(false);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer).show(eq(false), eq(false));
-
-        // But not when it's already visible
-        reset(mPrimaryBouncer);
-        when(mPrimaryBouncer.isShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
-
-        // Or animating away
-        reset(mPrimaryBouncer);
-        when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() {
-        when(mBiometricUnlockController.getMode())
-                .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
-                        /* expanded= */ true,
-                        /* tracking= */ false));
-        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenDismissBouncer() {
-        // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing
-        // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel
-        // which would mistakenly cause the bouncer to show briefly before its visibility
-        // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the
-        // bouncer if the bouncer is dismissing as a result of a biometric unlock.
-        when(mBiometricUnlockController.getMode())
-                .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
-                        /* expanded= */ true,
-                        /* tracking= */ false));
-        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() {
-        when(mKeyguardStateController.isOccluded()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
-                        /* expanded= */ true,
-                        /* tracking= */ false));
-        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenShowBouncer() {
-        // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing
-        // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel
-        // which would mistakenly cause the bouncer to show briefly before its visibility
-        // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the
-        // bouncer if the bouncer is dismissing as a result of a biometric unlock.
-        when(mBiometricUnlockController.getMode())
-                .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
-                        /* expanded= */ true,
-                        /* tracking= */ false));
-        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenShadeLocked() {
-        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
-                        /* expanded= */ true,
-                        /* tracking= */ false));
-        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void setOccluded_animatesPanelExpansion_onlyIfBouncerHidden() {
-        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
-        verify(mCentralSurfaces).animateKeyguardUnoccluding();
-
-        when(mPrimaryBouncer.isShowing()).thenReturn(true);
-        clearInvocations(mCentralSurfaces);
-        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
-        verify(mCentralSurfaces, never()).animateKeyguardUnoccluding();
-    }
-
-    @Test
-    public void setOccluded_onKeyguardOccludedChangedCalled() {
-        clearInvocations(mKeyguardStateController);
-        clearInvocations(mKeyguardUpdateMonitor);
-
-        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
-        verify(mKeyguardStateController).notifyKeyguardState(true, false);
-
-        clearInvocations(mKeyguardUpdateMonitor);
-        clearInvocations(mKeyguardStateController);
-
-        mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
-        verify(mKeyguardStateController).notifyKeyguardState(true, true);
-
-        clearInvocations(mKeyguardUpdateMonitor);
-        clearInvocations(mKeyguardStateController);
-
-        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
-        verify(mKeyguardStateController).notifyKeyguardState(true, false);
-    }
-
-    @Test
-    public void setOccluded_isInLaunchTransition_onKeyguardOccludedChangedCalled() {
-        mStatusBarKeyguardViewManager.show(null);
-
-        mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
-        verify(mKeyguardStateController).notifyKeyguardState(true, true);
-    }
-
-    @Test
-    public void setOccluded_isLaunchingActivityOverLockscreen_onKeyguardOccludedChangedCalled() {
-        when(mCentralSurfaces.isLaunchingActivityOverLockscreen()).thenReturn(true);
-        mStatusBarKeyguardViewManager.show(null);
-
-        mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
-        verify(mKeyguardStateController).notifyKeyguardState(true, true);
-    }
-
-    @Test
-    public void testHiding_cancelsGoneRunnable() {
-        OnDismissAction action = mock(OnDismissAction.class);
-        Runnable cancelAction = mock(Runnable.class);
-        mStatusBarKeyguardViewManager.dismissWithAction(
-                action, cancelAction, true /* afterKeyguardGone */);
-
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        mStatusBarKeyguardViewManager.hideBouncer(true);
-        mStatusBarKeyguardViewManager.hide(0, 30);
-        verify(action, never()).onDismiss();
-        verify(cancelAction).run();
-    }
-
-    @Test
-    public void testHidingBouncer_cancelsGoneRunnable() {
-        OnDismissAction action = mock(OnDismissAction.class);
-        Runnable cancelAction = mock(Runnable.class);
-        mStatusBarKeyguardViewManager.dismissWithAction(
-                action, cancelAction, true /* afterKeyguardGone */);
-
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        mStatusBarKeyguardViewManager.hideBouncer(true);
-
-        verify(action, never()).onDismiss();
-        verify(cancelAction).run();
-    }
-
-    @Test
-    public void testHiding_doesntCancelWhenShowing() {
-        OnDismissAction action = mock(OnDismissAction.class);
-        Runnable cancelAction = mock(Runnable.class);
-        mStatusBarKeyguardViewManager.dismissWithAction(
-                action, cancelAction, true /* afterKeyguardGone */);
-
-        mStatusBarKeyguardViewManager.hide(0, 30);
-        verify(action).onDismiss();
-        verify(cancelAction, never()).run();
-    }
-
-    @Test
-    public void testShowing_whenAlternateAuthShowing() {
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
-        assertTrue(
-                "Is showing not accurate when alternative auth showing",
-                mStatusBarKeyguardViewManager.isBouncerShowing());
-    }
-
-    @Test
-    public void testWillBeShowing_whenAlternateAuthShowing() {
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
-        assertTrue(
-                "Is or will be showing not accurate when alternative auth showing",
-                mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
-    }
-
-    @Test
-    public void testHideAlternateBouncer_onShowBouncer() {
-        // GIVEN alt auth is showing
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
-        reset(mAlternateBouncer);
-
-        // WHEN showBouncer is called
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
-
-        // THEN alt bouncer should be hidden
-        verify(mAlternateBouncer).hideAlternateBouncer();
-    }
-
-    @Test
-    public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() {
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        when(mPrimaryBouncer.inTransit()).thenReturn(true);
-
-        assertTrue(
-                "Is or will be showing should be true when bouncer is in transit",
-                mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
-    }
-
-    @Test
-    public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
-        // GIVEN alt auth exists, unlocking with biometric isn't allowed
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
-                .thenReturn(false);
-
-        // WHEN showGenericBouncer is called
-        final boolean scrimmed = true;
-        mStatusBarKeyguardViewManager.showBouncer(scrimmed);
-
-        // THEN regular bouncer is shown
-        verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed));
-        verify(mAlternateBouncer, never()).showAlternateBouncer();
-    }
-
-    @Test
-    public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
-        // GIVEN alt auth exists, unlocking with biometric is allowed
-        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
-        when(mPrimaryBouncer.isShowing()).thenReturn(false);
-        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
-
-        // WHEN showGenericBouncer is called
-        mStatusBarKeyguardViewManager.showBouncer(true);
-
-        // THEN alt auth bouncer is shown
-        verify(mAlternateBouncer).showAlternateBouncer();
-        verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
-    }
-
-    @Test
-    public void testUpdateResources_delegatesToBouncer() {
-        mStatusBarKeyguardViewManager.updateResources();
-
-        verify(mPrimaryBouncer).updateResources();
-    }
-
-    @Test
-    public void updateKeyguardPosition_delegatesToBouncer() {
-        mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f);
-
-        verify(mPrimaryBouncer).updateKeyguardPosition(1.0f);
-    }
-
-    @Test
-    public void testIsBouncerInTransit() {
-        when(mPrimaryBouncer.inTransit()).thenReturn(true);
-        Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue();
-        when(mPrimaryBouncer.inTransit()).thenReturn(false);
-        Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
-        mPrimaryBouncer = null;
-        Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
-    }
-
-    private static ShadeExpansionChangeEvent expansionEvent(
-            float fraction, boolean expanded, boolean tracking) {
-        return new ShadeExpansionChangeEvent(
-                fraction, expanded, tracking, /* dragDownPxAmount= */ 0f);
-    }
-
-    @Test
-    public void testPredictiveBackCallback_registration() {
-        /* verify that a predictive back callback is registered when the bouncer becomes visible */
-        mBouncerExpansionCallback.onVisibilityChanged(true);
-        verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
-                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
-                mOnBackInvokedCallback.capture());
-
-        /* verify that the same callback is unregistered when the bouncer becomes invisible */
-        mBouncerExpansionCallback.onVisibilityChanged(false);
-        verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(
-                eq(mOnBackInvokedCallback.getValue()));
-    }
-
-    @Test
-    public void testPredictiveBackCallback_invocationHidesBouncer() {
-        mBouncerExpansionCallback.onVisibilityChanged(true);
-        /* capture the predictive back callback during registration */
-        verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
-                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
-                mOnBackInvokedCallback.capture());
-
-        when(mPrimaryBouncer.isShowing()).thenReturn(true);
-        when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
-        /* invoke the back callback directly */
-        mOnBackInvokedCallback.getValue().onBackInvoked();
-
-        /* verify that the bouncer will be hidden as a result of the invocation */
-        verify(mCentralSurfaces).setBouncerShowing(eq(false));
-    }
-
-    @Test
-    public void testReportBouncerOnDreamWhenVisible() {
-        mBouncerExpansionCallback.onVisibilityChanged(true);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(false);
-        Mockito.clearInvocations(mCentralSurfaces);
-        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
-        mBouncerExpansionCallback.onVisibilityChanged(true);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(true);
-    }
-
-    @Test
-    public void testReportBouncerOnDreamWhenNotVisible() {
-        mBouncerExpansionCallback.onVisibilityChanged(false);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(false);
-        Mockito.clearInvocations(mCentralSurfaces);
-        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
-        mBouncerExpansionCallback.onVisibilityChanged(false);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(false);
-    }
-
-    @Test
-    public void flag_off_DoesNotCallBouncerInteractor() {
-        when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false);
-        mStatusBarKeyguardViewManager.hideBouncer(false);
-        verify(mPrimaryBouncerInteractor, never()).hide();
-    }
-
-    @Test
-    public void hideAlternateBouncer_beforeCentralSurfacesRegistered() {
-        mStatusBarKeyguardViewManager =
-                new StatusBarKeyguardViewManager(
-                        getContext(),
-                        mViewMediatorCallback,
-                        mLockPatternUtils,
-                        mStatusBarStateController,
-                        mock(ConfigurationController.class),
-                        mKeyguardUpdateMonitor,
-                        mDreamOverlayStateController,
-                        mock(NavigationModeController.class),
-                        mock(DockManager.class),
-                        mock(NotificationShadeWindowController.class),
-                        mKeyguardStateController,
-                        mock(NotificationMediaManager.class),
-                        mKeyguardBouncerFactory,
-                        mKeyguardMessageAreaFactory,
-                        Optional.of(mSysUiUnfoldComponent),
-                        () -> mShadeController,
-                        mLatencyTracker,
-                        mKeyguardSecurityModel,
-                        mFeatureFlags,
-                        mPrimaryBouncerCallbackInteractor,
-                        mPrimaryBouncerInteractor,
-                        mBouncerView) {
-                    @Override
-                    public ViewRootImpl getViewRootImpl() {
-                        return mViewRootImpl;
-                    }
-                };
-
-        // the following call before registering centralSurfaces should NOT throw a NPE:
-        mStatusBarKeyguardViewManager.hideAlternateBouncer(true);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 19658e6..ccc57ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -33,6 +33,7 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -59,6 +60,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.ShadeControllerImpl;
@@ -139,6 +141,8 @@
     private ActivityLaunchAnimator mActivityLaunchAnimator;
     @Mock
     private InteractionJankMonitor mJankMonitor;
+    @Mock
+    private UserTracker mUserTracker;
     private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
     private ExpandableNotificationRow mNotificationRow;
     private ExpandableNotificationRow mBubbleNotificationRow;
@@ -183,6 +187,8 @@
         when(mVisibilityProvider.obtain(any(NotificationEntry.class), anyBoolean()))
                 .thenAnswer(invocation -> NotificationVisibility.obtain(
                         invocation.<NotificationEntry>getArgument(0).getKey(), 0, 1, false));
+        when(mUserTracker.getUserHandle()).thenReturn(
+                UserHandle.of(ActivityManager.getCurrentUser()));
 
         HeadsUpManagerPhone headsUpManager = mock(HeadsUpManagerPhone.class);
         NotificationLaunchAnimatorControllerProvider notificationAnimationProvider =
@@ -222,7 +228,8 @@
                         mActivityLaunchAnimator,
                         notificationAnimationProvider,
                         mock(LaunchFullScreenIntentProvider.class),
-                        mock(FeatureFlags.class)
+                        mock(FeatureFlags.class),
+                        mUserTracker
                 );
 
         // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index eef43bd..5bb25f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -40,8 +40,10 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
+import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -92,7 +94,7 @@
         mMetricsLogger = new FakeMetricsLogger();
         LockscreenGestureLogger lockscreenGestureLogger = new LockscreenGestureLogger(
                 mMetricsLogger);
-        mCommandQueue = new CommandQueue(mContext);
+        mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext));
         mDependency.injectTestDependency(StatusBarStateController.class,
                 mock(SysuiStatusBarStateController.class));
         mDependency.injectTestDependency(ShadeController.class, mShadeController);
@@ -111,6 +113,7 @@
         mStatusBarNotificationPresenter = new StatusBarNotificationPresenter(
                 mContext,
                 mock(NotificationPanelViewController.class),
+                mock(QuickSettingsController.class),
                 mock(HeadsUpManagerPhone.class),
                 notificationShadeWindowView,
                 mock(ActivityStarter.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index 613238f..929099a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -32,6 +32,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
@@ -78,7 +79,8 @@
         mRemoteInputCallback = spy(new StatusBarRemoteInputCallback(mContext,
                 mock(GroupExpansionManager.class), mNotificationLockscreenUserManager,
                 mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager,
-                mActivityStarter, mShadeController, new CommandQueue(mContext),
+                mActivityStarter, mShadeController,
+                new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
                 mock(ActionClickLogger.class), mFakeExecutor));
         mRemoteInputCallback.mChallengeReceiver = mRemoteInputCallback.new ChallengeReceiver();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 6c83e9f..14a925a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -19,8 +19,10 @@
 
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
 import android.content.Intent;
@@ -32,6 +34,8 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,12 +50,15 @@
 public class SystemUIDialogTest extends SysuiTestCase {
 
     @Mock
+    private FeatureFlags mFeatureFlags;
+    @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
+        mDependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
         mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher);
     }
 
@@ -86,4 +93,20 @@
         verify(mBroadcastDispatcher, never()).unregisterReceiver(any());
         assertFalse(dialog.isShowing());
     }
+
+    @Test
+    public void usePredictiveBackAnimFlag() {
+        when(mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM))
+                .thenReturn(true);
+        final SystemUIDialog dialog = new SystemUIDialog(mContext);
+
+        dialog.show();
+
+        assertTrue(dialog.isShowing());
+        verify(mFeatureFlags, atLeast(1))
+                .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM);
+
+        dialog.dismiss();
+        assertFalse(dialog.isShowing());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 438271c..1e5782b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -23,14 +23,15 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import android.animation.Animator;
 import android.app.Fragment;
 import android.app.StatusBarManager;
 import android.content.Context;
@@ -43,8 +44,10 @@
 import android.view.ViewPropertyAnimator;
 import android.widget.FrameLayout;
 
+import androidx.core.animation.Animator;
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.R;
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.dump.DumpManager;
@@ -58,7 +61,6 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger;
 import com.android.systemui.statusbar.OperatorNameViewController;
-import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -68,6 +70,8 @@
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
@@ -80,6 +84,9 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -92,7 +99,6 @@
     private StatusBarLocationPublisher mLocationPublisher;
     // Set in instantiate()
     private StatusBarIconController mStatusBarIconController;
-    private NetworkController mNetworkController;
     private KeyguardStateController mKeyguardStateController;
 
     private final CommandQueue mCommandQueue = mock(CommandQueue.class);
@@ -120,6 +126,12 @@
     private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock
     private DumpManager mDumpManager;
+    @Mock
+    private StatusBarWindowStateController mStatusBarWindowStateController;
+    @Mock
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+    private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
 
     public CollapsedStatusBarFragmentTest() {
         super(CollapsedStatusBarFragment.class);
@@ -129,6 +141,14 @@
     public void setup() {
         injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
         mDependency.injectMockDependency(DarkIconDispatcher.class);
+
+        // Keep the window state listeners so we can dispatch to them to test the status bar
+        // fragment's response.
+        doAnswer(invocation -> {
+            mStatusBarWindowStateListeners.add(invocation.getArgument(0));
+            return null;
+        }).when(mStatusBarWindowStateController).addListener(
+                any(StatusBarWindowStateListener.class));
     }
 
     @Test
@@ -416,6 +436,27 @@
         assertFalse(contains);
     }
 
+    @Test
+    public void testStatusBarIcons_hiddenThroughoutCameraLaunch() {
+        final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+        mockSecureCameraLaunch(fragment, true /* launched */);
+
+        // Status icons should be invisible or gone, but certainly not VISIBLE.
+        assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+        mockSecureCameraLaunchFinished();
+
+        assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+        mockSecureCameraLaunch(fragment, false /* launched */);
+
+        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        assertEquals(View.VISIBLE, getClockView().getVisibility());
+    }
+
     @Override
     protected Fragment instantiate(Context context, String className, Bundle arguments) {
         MockitoAnnotations.initMocks(this);
@@ -424,7 +465,6 @@
         mAnimationScheduler = mock(SystemStatusAnimationScheduler.class);
         mLocationPublisher = mock(StatusBarLocationPublisher.class);
         mStatusBarIconController = mock(StatusBarIconController.class);
-        mNetworkController = mock(NetworkController.class);
         mStatusBarStateController = mock(StatusBarStateController.class);
         mKeyguardStateController = mock(KeyguardStateController.class);
         mOperatorNameViewController = mock(OperatorNameViewController.class);
@@ -448,7 +488,6 @@
                 mStatusBarHideIconsForBouncerManager,
                 mKeyguardStateController,
                 mNotificationPanelViewController,
-                mNetworkController,
                 mStatusBarStateController,
                 mCommandQueue,
                 mCarrierConfigTracker,
@@ -459,7 +498,9 @@
                 mOperatorNameViewControllerFactory,
                 mSecureSettings,
                 mExecutor,
-                mDumpManager);
+                mDumpManager,
+                mStatusBarWindowStateController,
+                mKeyguardUpdateMonitor);
     }
 
     private void setUpDaggerComponent() {
@@ -482,6 +523,35 @@
                 mNotificationAreaInner);
     }
 
+    /**
+     * Configure mocks to return values consistent with the secure camera animating itself launched
+     * over the keyguard.
+     */
+    private void mockSecureCameraLaunch(CollapsedStatusBarFragment fragment, boolean launched) {
+        when(mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard()).thenReturn(launched);
+        when(mKeyguardStateController.isOccluded()).thenReturn(launched);
+
+        if (launched) {
+            fragment.onCameraLaunchGestureDetected(0 /* source */);
+        } else {
+            for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+                listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_SHOWING);
+            }
+        }
+
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+    }
+
+    /**
+     * Configure mocks to return values consistent with the secure camera showing over the keyguard
+     * with its launch animation finished.
+     */
+    private void mockSecureCameraLaunchFinished() {
+        for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+            listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_HIDDEN);
+        }
+    }
+
     private CollapsedStatusBarFragment resumeAndGetFragment() {
         mFragments.dispatchResume();
         processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index d30222f..711e4ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -50,7 +50,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.*
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.nullable
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.eq
@@ -83,7 +85,8 @@
     private lateinit var notifCollectionListener: NotifCollectionListener
 
     @Mock private lateinit var mockOngoingCallFlags: OngoingCallFlags
-    @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
+    @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler:
+        SwipeStatusBarAwayGestureHandler
     @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
     @Mock private lateinit var mockActivityStarter: ActivityStarter
     @Mock private lateinit var mockIActivityManager: IActivityManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt
index 5a6bb30..ab888f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt
@@ -18,9 +18,9 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+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.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.google.common.truth.Truth.assertThat
@@ -42,7 +42,7 @@
 
     private lateinit var underTest: AirplaneModeViewModelImpl
 
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var logger: TableLogBuffer
     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var interactor: AirplaneModeInteractor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
index f822ba0..45189cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
@@ -19,7 +19,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableRowLogger
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_IN
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_OUT
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE
@@ -54,7 +55,19 @@
         assertThat(logger.changes)
             .contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString()))
         assertThat(logger.changes)
-            .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString()))
+            .contains(
+                Pair(
+                    COL_ACTIVITY_DIRECTION_IN,
+                    connection.dataActivityDirection.hasActivityIn.toString(),
+                )
+            )
+        assertThat(logger.changes)
+            .contains(
+                Pair(
+                    COL_ACTIVITY_DIRECTION_OUT,
+                    connection.dataActivityDirection.hasActivityOut.toString(),
+                )
+            )
         assertThat(logger.changes)
             .contains(
                 Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString())
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..0145103
--- /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.mobile.shared.MobileInputLogger
+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: MobileInputLogger
+    @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 49d4bdc..f483e42 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
@@ -26,6 +26,7 @@
 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.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository
@@ -54,21 +55,27 @@
     private val _subscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
     override val subscriptions = _subscriptions
 
-    private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+    private val _activeMobileDataSubscriptionId = MutableStateFlow<Int?>(null)
     override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
 
+    private val _activeMobileRepository = MutableStateFlow<MobileConnectionRepository?>(null)
+    override val activeMobileDataRepository = _activeMobileRepository
+
+    override val activeSubChangedInGroupEvent: MutableSharedFlow<Unit> = MutableSharedFlow()
+
+    private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+    override val defaultDataSubId = _defaultDataSubId
+
     private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
     override val defaultMobileNetworkConnectivity = _mobileConnectivity
 
     private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
+
     override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
         return subIdRepos[subId]
             ?: 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)
@@ -81,16 +88,23 @@
         _subscriptions.value = subs
     }
 
+    fun setDefaultDataSubId(id: Int) {
+        _defaultDataSubId.value = id
+    }
+
     fun setMobileConnectivity(model: MobileConnectivityModel) {
         _mobileConnectivity.value = model
     }
 
-    suspend fun triggerGlobalMobileDataSettingChangedEvent() {
-        _globalMobileDataSettingChangedEvent.emit(Unit)
-    }
-
     fun setActiveMobileDataSubscriptionId(subId: Int) {
-        _activeMobileDataSubscriptionId.value = subId
+        // Simulate the filtering that the repo does
+        if (subId == INVALID_SUBSCRIPTION_ID) {
+            _activeMobileDataSubscriptionId.value = null
+            _activeMobileRepository.value = null
+        } else {
+            _activeMobileDataSubscriptionId.value = subId
+            _activeMobileRepository.value = getRepoForSubId(subId)
+        }
     }
 
     fun setMobileConnectionRepositoryMap(connections: Map<Int, MobileConnectionRepository>) {
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 5d377a8..17502f2 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
@@ -32,13 +33,14 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
 import com.android.systemui.util.mockito.any
 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
@@ -71,17 +73,19 @@
     private lateinit var underTest: MobileRepositorySwitcher
     private lateinit var realRepo: MobileConnectionsRepositoryImpl
     private lateinit var demoRepo: DemoMobileConnectionsRepository
-    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var wifiDataSource: DemoModeWifiDataSource
     private lateinit var logFactory: TableLogBufferFactory
+    private lateinit var wifiRepository: FakeWifiRepository
 
     @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 logger: MobileInputLogger
+    @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()
 
@@ -96,10 +100,15 @@
         // Never start in demo mode
         whenever(demoModeController.isInDemoMode).thenReturn(false)
 
-        mockDataSource =
+        mobileDataSource =
             mock<DemoModeMobileConnectionDataSource>().also {
                 whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
             }
+        wifiDataSource =
+            mock<DemoModeWifiDataSource>().also {
+                whenever(it.wifiEvents).thenReturn(MutableStateFlow(null))
+            }
+        wifiRepository = FakeWifiRepository()
 
         realRepo =
             MobileConnectionsRepositoryImpl(
@@ -107,18 +116,20 @@
                 subscriptionManager,
                 telephonyManager,
                 logger,
+                summaryLogger,
                 mobileMappings,
                 fakeBroadcastDispatcher,
-                globalSettings,
                 context,
                 IMMEDIATE,
                 scope,
+                wifiRepository,
                 mock(),
             )
 
         demoRepo =
             DemoMobileConnectionsRepository(
-                dataSource = mockDataSource,
+                mobileDataSource = mobileDataSource,
+                wifiDataSource = wifiDataSource,
                 scope = scope,
                 context = context,
                 logFactory = logFactory,
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 2102085..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
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
@@ -63,10 +65,12 @@
     private val testScope = TestScope(testDispatcher)
 
     private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+    private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
 
     private lateinit var connectionsRepo: DemoMobileConnectionsRepository
     private lateinit var underTest: DemoMobileConnectionRepository
     private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var mockWifiDataSource: DemoModeWifiDataSource
 
     @Before
     fun setUp() {
@@ -75,10 +79,15 @@
             mock<DemoModeMobileConnectionDataSource>().also {
                 whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
             }
+        mockWifiDataSource =
+            mock<DemoModeWifiDataSource>().also {
+                whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+            }
 
         connectionsRepo =
             DemoMobileConnectionsRepository(
-                dataSource = mockDataSource,
+                mobileDataSource = mockDataSource,
+                wifiDataSource = mockWifiDataSource,
                 scope = testScope.backgroundScope,
                 context = context,
                 logFactory = logFactory,
@@ -129,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 cdbe75e..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
@@ -32,6 +32,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
@@ -57,21 +59,28 @@
     private val testScope = TestScope(testDispatcher)
 
     private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+    private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
 
     private lateinit var underTest: DemoMobileConnectionsRepository
-    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var wifiDataSource: DemoModeWifiDataSource
 
     @Before
     fun setUp() {
         // The data source only provides one API, so we can mock it with a flow here for convenience
-        mockDataSource =
+        mobileDataSource =
             mock<DemoModeMobileConnectionDataSource>().also {
                 whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
             }
+        wifiDataSource =
+            mock<DemoModeWifiDataSource>().also {
+                whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+            }
 
         underTest =
             DemoMobileConnectionsRepository(
-                dataSource = mockDataSource,
+                mobileDataSource = mobileDataSource,
+                wifiDataSource = wifiDataSource,
                 scope = testScope.backgroundScope,
                 context = context,
                 logFactory = logFactory,
@@ -81,6 +90,14 @@
     }
 
     @Test
+    fun `connectivity - defaults to connected and validated`() =
+        testScope.runTest {
+            val connectivity = underTest.defaultMobileNetworkConnectivity.value
+            assertThat(connectivity.isConnected).isTrue()
+            assertThat(connectivity.isValidated).isTrue()
+        }
+
+    @Test
     fun `network event - create new subscription`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -97,6 +114,22 @@
         }
 
     @Test
+    fun `wifi carrier merged event - create new subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEmpty()
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+            job.cancel()
+        }
+
+    @Test
     fun `network event - reuses subscription when same Id`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -119,6 +152,28 @@
         }
 
     @Test
+    fun `wifi carrier merged event - reuses subscription when same Id`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEmpty()
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+            // Second network event comes in with the same subId, does not create a new subscription
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+            job.cancel()
+        }
+
+    @Test
     fun `multiple subscriptions`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -133,6 +188,35 @@
         }
 
     @Test
+    fun `mobile subscription and carrier merged subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+            assertThat(latest).hasSize(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `multiple mobile subscriptions and carrier merged subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3)
+
+            assertThat(latest).hasSize(3)
+
+            job.cancel()
+        }
+
+    @Test
     fun `mobile disabled event - disables connection - subId specified - single conn`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -194,6 +278,112 @@
             job.cancel()
         }
 
+    @Test
+    fun `wifi network updates to disabled - carrier merged connection removed`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+            assertThat(latest).hasSize(1)
+
+            fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled
+
+            assertThat(latest).isEmpty()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `wifi network updates to active - carrier merged connection removed`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+            assertThat(latest).hasSize(1)
+
+            fakeWifiEventFlow.value =
+                FakeWifiEventModel.Wifi(
+                    level = 1,
+                    activity = 0,
+                    ssid = null,
+                    validated = true,
+                )
+
+            assertThat(latest).isEmpty()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile sub updates to carrier merged - only one connection`() =
+        testScope.runTest {
+            var latestSubsList: List<SubscriptionModel>? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptions
+                    .onEach { latestSubsList = it }
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2)
+            assertThat(latestSubsList).hasSize(1)
+
+            val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+            fakeWifiEventFlow.value = carrierMergedEvent
+            assertThat(latestSubsList).hasSize(1)
+            val connection = connections!!.find { it.subId == 3 }!!
+            assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile sub updates to carrier merged then back - has old mobile data`() =
+        testScope.runTest {
+            var latestSubsList: List<SubscriptionModel>? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptions
+                    .onEach { latestSubsList = it }
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            val mobileEvent = validMobileEvent(subId = 3, level = 2)
+            fakeNetworkEventFlow.value = mobileEvent
+            assertThat(latestSubsList).hasSize(1)
+
+            val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+            fakeWifiEventFlow.value = carrierMergedEvent
+            assertThat(latestSubsList).hasSize(1)
+            var connection = connections!!.find { it.subId == 3 }!!
+            assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+            // WHEN the carrier merged is removed
+            fakeWifiEventFlow.value =
+                FakeWifiEventModel.Wifi(
+                    level = 4,
+                    activity = 0,
+                    ssid = null,
+                    validated = true,
+                )
+
+            // THEN the subId=3 connection goes back to the mobile information
+            connection = connections!!.find { it.subId == 3 }!!
+            assertConnection(connection, mobileEvent)
+
+            job.cancel()
+        }
+
     /** Regression test for b/261706421 */
     @Test
     fun `multiple connections - remove all - does not throw`() =
@@ -289,6 +479,51 @@
             job.cancel()
         }
 
+    @Test
+    fun `demo connection - two connections - update carrier merged - no affect on first`() =
+        testScope.runTest {
+            var currentEvent1 = validMobileEvent(subId = 1)
+            var connection1: DemoMobileConnectionRepository? = null
+            var currentEvent2 = validCarrierMergedEvent(subId = 2)
+            var connection2: DemoMobileConnectionRepository? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptions
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            fakeNetworkEventFlow.value = currentEvent1
+            fakeWifiEventFlow.value = currentEvent2
+            assertThat(connections).hasSize(2)
+            connections!!.forEach {
+                when (it.subId) {
+                    1 -> connection1 = it
+                    2 -> connection2 = it
+                    else -> Assert.fail("Unexpected subscription")
+                }
+            }
+
+            assertConnection(connection1!!, currentEvent1)
+            assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+            // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+            currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4)
+            fakeWifiEventFlow.value = currentEvent2
+            assertConnection(connection1!!, currentEvent1)
+            assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+            // and vice versa
+            currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+            fakeNetworkEventFlow.value = currentEvent1
+            assertConnection(connection1!!, currentEvent1)
+            assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+            job.cancel()
+        }
+
     private fun assertConnection(
         conn: DemoMobileConnectionRepository,
         model: FakeNetworkEventModel
@@ -304,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()
@@ -315,6 +551,21 @@
             else -> {}
         }
     }
+
+    private fun assertCarrierMergedConnection(
+        conn: DemoMobileConnectionRepository,
+        model: FakeWifiEventModel.CarrierMerged,
+    ) {
+        val connectionInfo: MobileConnectionModel = conn.connectionInfo.value
+        assertThat(conn.subId).isEqualTo(model.subscriptionId)
+        assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
+        assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
+        assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false)
+        assertThat(connectionInfo.isRoaming).isEqualTo(false)
+        assertThat(connectionInfo.isEmergencyOnly).isFalse()
+        assertThat(connectionInfo.isGsm).isFalse()
+        assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected)
+    }
 }
 
 /** Convenience to create a valid fake network event with minimal params */
@@ -339,3 +590,16 @@
         roaming = roaming,
         name = "demo name",
     )
+
+fun validCarrierMergedEvent(
+    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
new file mode 100644
index 0000000..f0f213b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.telephony.TelephonyManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+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
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+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.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class CarrierMergedConnectionRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: CarrierMergedConnectionRepository
+
+    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)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
+        whenever(telephonyManager.simOperatorName).thenReturn("")
+
+        wifiRepository = FakeWifiRepository()
+
+        underTest =
+            CarrierMergedConnectionRepository(
+                SUB_ID,
+                logger,
+                telephonyManager,
+                testScope.backgroundScope,
+                wifiRepository,
+            )
+    }
+
+    @Test
+    fun connectionInfo_inactiveWifi_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_activeWifi_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1))
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() =
+        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,
+                )
+            )
+
+            val expected =
+                MobileConnectionModel(
+                    primaryLevel = 3,
+                    cdmaLevel = 3,
+                    dataConnectionState = DataConnectionState.Connected,
+                    dataActivityDirection =
+                        DataActivityModel(
+                            hasActivityIn = false,
+                            hasActivityOut = false,
+                        ),
+                    resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+                    isRoaming = false,
+                    isEmergencyOnly = false,
+                    operatorAlphaShort = null,
+                    isInService = true,
+                    isGsm = false,
+                    carrierNetworkChangeActive = false,
+                )
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @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
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID + 10,
+                    level = 3,
+                )
+            )
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+            assertThat(latest!!.primaryLevel).isNotEqualTo(3)
+            assertThat(latest!!.resolvedNetworkType)
+                .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
+
+            job.cancel()
+        }
+
+    // This scenario likely isn't possible, but write a test for it anyway
+    @Test
+    fun connectionInfo_carrierMergedButNotEnabled_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+            wifiRepository.setIsWifiEnabled(false)
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    // This scenario likely isn't possible, but write a test for it anyway
+    @Test
+    fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+            wifiRepository.setIsWifiDefault(false)
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun numberOfLevels_comesFromCarrierMerged() =
+        testScope.runTest {
+            var latest: Int? = null
+            val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 1,
+                    numberOfLevels = 6,
+                )
+            )
+
+            assertThat(latest).isEqualTo(6)
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataEnabled_matchesWifiEnabled() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setIsWifiEnabled(true)
+            assertThat(latest).isTrue()
+
+            wifiRepository.setIsWifiEnabled(false)
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun cdmaRoaming_alwaysFalse() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            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
new file mode 100644
index 0000000..cd4d847
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.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.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+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.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the
+ * repository interface it's switching on. These tests just need to verify that the entire interface
+ * properly switches over when the value of `isCarrierMerged` changes.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class FullMobileConnectionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: FullMobileConnectionRepository
+
+    private val systemClock = FakeSystemClock()
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private val tableLogBuffer = TableLogBuffer(maxSize = 100, name = "TestName", systemClock)
+    private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
+    private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
+
+    private lateinit var mobileRepo: FakeMobileConnectionRepository
+    private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
+
+    @Before
+    fun setUp() {
+        mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+        carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+
+        whenever(
+                mobileFactory.build(
+                    eq(SUB_ID),
+                    any(),
+                    eq(DEFAULT_NAME),
+                    eq(SEP),
+                )
+            )
+            .thenReturn(mobileRepo)
+        whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(carrierMergedRepo)
+    }
+
+    @Test
+    fun startingIsCarrierMerged_usesCarrierMergedInitially() =
+        testScope.runTest {
+            val carrierMergedConnectionInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator",
+                )
+            carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo)
+
+            initializeRepo(startingIsCarrierMerged = true)
+
+            assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo)
+            assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo)
+            verify(mobileFactory, never())
+                .build(
+                    SUB_ID,
+                    tableLogBuffer,
+                    DEFAULT_NAME,
+                    SEP,
+                )
+        }
+
+    @Test
+    fun startingNotCarrierMerged_usesTypicalInitially() =
+        testScope.runTest {
+            val mobileConnectionInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Operator",
+                )
+            mobileRepo.setConnectionInfo(mobileConnectionInfo)
+
+            initializeRepo(startingIsCarrierMerged = false)
+
+            assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
+            assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo)
+            verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer)
+        }
+
+    @Test
+    fun activeRepo_matchesIsCarrierMerged() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+            var latest: MobileConnectionRepository? = null
+            val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+            underTest.setIsCarrierMerged(true)
+
+            assertThat(latest).isEqualTo(carrierMergedRepo)
+
+            underTest.setIsCarrierMerged(false)
+
+            assertThat(latest).isEqualTo(mobileRepo)
+
+            underTest.setIsCarrierMerged(true)
+
+            assertThat(latest).isEqualTo(carrierMergedRepo)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_getsUpdatesFromRepo_carrierMerged() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            underTest.setIsCarrierMerged(true)
+
+            val info1 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator",
+                    primaryLevel = 1,
+                )
+            carrierMergedRepo.setConnectionInfo(info1)
+
+            assertThat(latest).isEqualTo(info1)
+
+            val info2 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator #2",
+                    primaryLevel = 2,
+                )
+            carrierMergedRepo.setConnectionInfo(info2)
+
+            assertThat(latest).isEqualTo(info2)
+
+            val info3 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator #3",
+                    primaryLevel = 3,
+                )
+            carrierMergedRepo.setConnectionInfo(info3)
+
+            assertThat(latest).isEqualTo(info3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_getsUpdatesFromRepo_mobile() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            underTest.setIsCarrierMerged(false)
+
+            val info1 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Merged Operator",
+                    primaryLevel = 1,
+                )
+            mobileRepo.setConnectionInfo(info1)
+
+            assertThat(latest).isEqualTo(info1)
+
+            val info2 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Merged Operator #2",
+                    primaryLevel = 2,
+                )
+            mobileRepo.setConnectionInfo(info2)
+
+            assertThat(latest).isEqualTo(info2)
+
+            val info3 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Merged Operator #3",
+                    primaryLevel = 3,
+                )
+            mobileRepo.setConnectionInfo(info3)
+
+            assertThat(latest).isEqualTo(info3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_updatesWhenCarrierMergedUpdates() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            val carrierMergedInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator",
+                    primaryLevel = 4,
+                )
+            carrierMergedRepo.setConnectionInfo(carrierMergedInfo)
+
+            val mobileInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Operator",
+                    primaryLevel = 2,
+                )
+            mobileRepo.setConnectionInfo(mobileInfo)
+
+            // Start with the mobile info
+            assertThat(latest).isEqualTo(mobileInfo)
+
+            // WHEN isCarrierMerged is set to true
+            underTest.setIsCarrierMerged(true)
+
+            // THEN the carrier merged info is used
+            assertThat(latest).isEqualTo(carrierMergedInfo)
+
+            val newCarrierMergedInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "New CM Operator",
+                    primaryLevel = 0,
+                )
+            carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo)
+
+            assertThat(latest).isEqualTo(newCarrierMergedInfo)
+
+            // WHEN isCarrierMerged is set to false
+            underTest.setIsCarrierMerged(false)
+
+            // THEN the typical info is used
+            assertThat(latest).isEqualTo(mobileInfo)
+
+            val newMobileInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "New Mobile Operator",
+                    primaryLevel = 3,
+                )
+            mobileRepo.setConnectionInfo(newMobileInfo)
+
+            assertThat(latest).isEqualTo(newMobileInfo)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `factory - reuses log buffers for same connection`() =
+        testScope.runTest {
+            val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+            val factory =
+                FullMobileConnectionRepository.Factory(
+                    scope = testScope.backgroundScope,
+                    realLoggerFactory,
+                    mobileFactory,
+                    carrierMergedFactory,
+                )
+
+            // Create two connections for the same subId. Similar to if the connection appeared
+            // and disappeared from the connectionFactory's perspective
+            val connection1 =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = false,
+                    DEFAULT_NAME,
+                    SEP,
+                )
+
+            val connection1Repeat =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = false,
+                    DEFAULT_NAME,
+                    SEP,
+                )
+
+            assertThat(connection1.tableLogBuffer)
+                .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+        }
+
+    @Test
+    fun `factory - reuses log buffers for same sub ID even if carrier merged`() =
+        testScope.runTest {
+            val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+            val factory =
+                FullMobileConnectionRepository.Factory(
+                    scope = testScope.backgroundScope,
+                    realLoggerFactory,
+                    mobileFactory,
+                    carrierMergedFactory,
+                )
+
+            val connection1 =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = false,
+                    DEFAULT_NAME,
+                    SEP,
+                )
+
+            // WHEN a connection with the same sub ID but carrierMerged = true is created
+            val connection1Repeat =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = true,
+                    DEFAULT_NAME,
+                    SEP,
+                )
+
+            // THEN the same table is re-used
+            assertThat(connection1.tableLogBuffer)
+                .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+        }
+
+    @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 =
+            FullMobileConnectionRepository(
+                SUB_ID,
+                startingIsCarrierMerged,
+                tableLogBuffer,
+                DEFAULT_NAME,
+                SEP,
+                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 d6b8c0d..b2577e3 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
@@ -41,6 +39,8 @@
 import android.telephony.TelephonyManager.DATA_CONNECTING
 import android.telephony.TelephonyManager.DATA_DISCONNECTED
 import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS
+import android.telephony.TelephonyManager.DATA_SUSPENDED
 import android.telephony.TelephonyManager.DATA_UNKNOWN
 import android.telephony.TelephonyManager.ERI_OFF
 import android.telephony.TelephonyManager.ERI_ON
@@ -59,18 +59,19 @@
 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
+import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
-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.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
@@ -83,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")
@@ -94,17 +94,20 @@
     private lateinit var connectionsRepo: FakeMobileConnectionsRepository
 
     @Mock private lateinit var telephonyManager: TelephonyManager
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var logger: MobileInputLogger
     @Mock private lateinit var tableLogger: TableLogBuffer
 
     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)
@@ -116,9 +119,8 @@
                 DEFAULT_NAME,
                 SEP,
                 telephonyManager,
-                globalSettings,
+                systemUiCarrierConfig,
                 fakeBroadcastDispatcher,
-                connectionsRepo.globalMobileDataSettingChangedEvent,
                 mobileMappings,
                 IMMEDIATE,
                 logger,
@@ -255,6 +257,37 @@
         }
 
     @Test
+    fun testFlowForSubId_dataConnectionState_suspended() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Suspended)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState_handoverInProgress() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState)
+                .isEqualTo(DataConnectionState.HandoverInProgress)
+
+            job.cancel()
+        }
+
+    @Test
     fun testFlowForSubId_dataConnectionState_unknown() =
         runBlocking(IMMEDIATE) {
             var latest: MobileConnectionModel? = null
@@ -270,6 +303,21 @@
         }
 
     @Test
+    fun testFlowForSubId_dataConnectionState_invalid() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(45, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Invalid)
+
+            job.cancel()
+        }
+
+    @Test
     fun testFlowForSubId_dataActivity() =
         runBlocking(IMMEDIATE) {
             var latest: MobileConnectionModel? = null
@@ -352,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()
@@ -418,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>()
@@ -439,7 +459,6 @@
 
             assertThat(latest).isTrue()
 
-            telephonyJob.cancel()
             job.cancel()
         }
 
@@ -536,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)
 
@@ -553,7 +607,7 @@
         }
 
     @Test
-    fun `network name - operatorAlphaShort - tracked`() =
+    fun `operatorAlphaShort - tracked`() =
         runBlocking(IMMEDIATE) {
             var latest: String? = null
 
@@ -625,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 */
@@ -668,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 0da15e2..09b7a66 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
@@ -22,10 +22,11 @@
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
-import android.provider.Settings
+import android.os.ParcelUuid
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
 import android.telephony.TelephonyManager
@@ -38,19 +39,25 @@
 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.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName
+import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 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
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
@@ -72,20 +79,25 @@
     private lateinit var underTest: MobileConnectionsRepositoryImpl
 
     private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
+    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 logger: MobileInputLogger
+    @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 ->
@@ -94,21 +106,55 @@
             }
         }
 
-        whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ ->
+        whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ ->
             mock<TableLogBuffer>()
         }
 
+        // For convenience, set up the subscription info callbacks
+        whenever(subscriptionManager.getActiveSubscriptionInfo(anyInt())).thenAnswer { invocation ->
+            when (invocation.getArgument(0) as Int) {
+                1 -> SUB_1
+                2 -> SUB_2
+                3 -> SUB_3
+                4 -> SUB_4
+                else -> null
+            }
+        }
+
+        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,
+            )
+        fullConnectionFactory =
+            FullMobileConnectionRepository.Factory(
+                scope = scope,
                 logFactory = logBufferFactory,
+                mobileRepoFactory = connectionFactory,
+                carrierMergedRepoFactory = carrierMergedFactory,
             )
 
         underTest =
@@ -117,13 +163,14 @@
                 subscriptionManager,
                 telephonyManager,
                 logger,
+                summaryLogger,
                 mobileMappings,
                 fakeBroadcastDispatcher,
-                globalSettings,
                 context,
                 IMMEDIATE,
                 scope,
-                connectionFactory,
+                wifiRepository,
+                fullConnectionFactory,
             )
     }
 
@@ -178,10 +225,43 @@
         }
 
     @Test
-    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+    fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
         runBlocking(IMMEDIATE) {
-            assertThat(underTest.activeMobileDataSubscriptionId.value)
-                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+            var latest: List<SubscriptionModel>? = null
+
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(MODEL_CM))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionModel>? = null
+
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_initialValueIsNull() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.activeMobileDataSubscriptionId.value).isEqualTo(null)
         }
 
     @Test
@@ -200,6 +280,140 @@
         }
 
     @Test
+    fun activeSubId_nullIfInvalidSubIdIsReceived() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+
+            val job = underTest.activeMobileDataSubscriptionId.onEach { latest = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(latest).isNotNull()
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun activeRepo_initiallyNull() {
+        assertThat(underTest.activeMobileDataRepository.value).isNull()
+    }
+
+    @Test
+    fun activeRepo_updatesWithActiveDataId() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileConnectionRepository? = null
+            val job = underTest.activeMobileDataRepository.onEach { latest = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(latest?.subId).isEqualTo(SUB_2_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun activeRepo_nullIfActiveDataSubIdBecomesInvalid() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileConnectionRepository? = null
+            val job = underTest.activeMobileDataRepository.onEach { latest = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(latest).isNotNull()
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    /** Regression test for b/268146648. */
+    fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() =
+        runBlocking(IMMEDIATE) {
+            var activeRepo: MobileConnectionRepository? = null
+            var subscriptions: List<SubscriptionModel>? = null
+
+            val activeRepoJob =
+                underTest.activeMobileDataRepository.onEach { activeRepo = it }.launchIn(this)
+            val subscriptionsJob =
+                underTest.subscriptions.onEach { subscriptions = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(subscriptions).isEmpty()
+            assertThat(activeRepo).isNotNull()
+
+            activeRepoJob.cancel()
+            subscriptionsJob.cancel()
+        }
+
+    @Test
+    fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileConnectionRepository? = null
+            var subscriptions: List<SubscriptionModel>? = null
+            val activeSubIdJob =
+                underTest.activeMobileDataSubscriptionId
+                    .filterNotNull()
+                    .onEach { latest = underTest.getRepoForSubId(it) }
+                    .launchIn(this)
+            val subscriptionsJob =
+                underTest.subscriptions.onEach { subscriptions = it }.launchIn(this)
+
+            // Active data subscription id is sent, but no subscription change has been posted yet
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            // Subscriptions list is empty
+            assertThat(subscriptions).isEmpty()
+            // getRepoForSubId does not throw
+            assertThat(latest).isNotNull()
+
+            activeSubIdJob.cancel()
+            subscriptionsJob.cancel()
+        }
+
+    @Test
+    fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() =
+        runBlocking(IMMEDIATE) {
+            var activeRepo: MobileConnectionRepository? = null
+            val job = underTest.activeMobileDataRepository.onEach { activeRepo = it }.launchIn(this)
+            val subscriptionsJob = underTest.subscriptions.launchIn(this)
+
+            // GIVEN active repo is updated before the subscription list updates
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(activeRepo).isNotNull()
+
+            // GIVEN the subscription list is then updated which includes the active data sub id
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // WHEN requesting a connection repository for the subscription
+            val newRepo = underTest.getRepoForSubId(SUB_2_ID)
+
+            // THEN the newly request repo has been cached and reused
+            assertThat(activeRepo).isSameInstanceAs(newRepo)
+
+            job.cancel()
+            subscriptionsJob.cancel()
+        }
+
+    @Test
     fun testConnectionRepository_validSubId_isCached() =
         runBlocking(IMMEDIATE) {
             val job = underTest.subscriptions.launchIn(this)
@@ -217,6 +431,96 @@
         }
 
     @Test
+    fun testConnectionRepository_carrierMergedSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val repo1 = underTest.getRepoForSubId(SUB_CM_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_CM_ID)
+
+            assertThat(repo1).isSameInstanceAs(repo2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            val mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            // WHEN the wifi network updates to be not carrier merged
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1))
+
+            // THEN the repos update
+            val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            // WHEN the wifi network updates to be carrier merged
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+
+            // THEN the repos update
+            val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun testConnectionCache_clearsInvalidSubscriptions() =
         runBlocking(IMMEDIATE) {
             val job = underTest.subscriptions.launchIn(this)
@@ -242,6 +546,34 @@
             job.cancel()
         }
 
+    @Test
+    fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // Get repos to trigger caching
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+            val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID)
+
+            assertThat(underTest.getSubIdRepoCache())
+                .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged)
+
+            // SUB_2 and SUB_CM disappear
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+            job.cancel()
+        }
+
     /** Regression test for b/261706421 */
     @Test
     fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() =
@@ -281,7 +613,7 @@
         }
 
     @Test
-    fun `connection repository - log buffer contains sub id in its name`() =
+    fun connectionRepository_logBufferContainsSubIdInItsName() =
         runBlocking(IMMEDIATE) {
             val job = underTest.subscriptions.launchIn(this)
 
@@ -292,14 +624,14 @@
             // Get repos to trigger creation
             underTest.getRepoForSubId(SUB_1_ID)
             verify(logBufferFactory)
-                .create(
-                    eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)),
+                .getOrCreate(
+                    eq(tableBufferLogName(SUB_1_ID)),
                     anyInt(),
                 )
             underTest.getRepoForSubId(SUB_2_ID)
             verify(logBufferFactory)
-                .create(
-                    eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)),
+                .getOrCreate(
+                    eq(tableBufferLogName(SUB_2_ID)),
                     anyInt(),
                 )
 
@@ -307,6 +639,35 @@
         }
 
     @Test
+    fun testDefaultDataSubId_updatesOnBroadcast() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(
+                    context,
+                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
+                )
+            }
+
+            assertThat(latest).isEqualTo(SUB_2_ID)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(
+                    context,
+                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+                )
+            }
+
+            assertThat(latest).isEqualTo(SUB_1_ID)
+
+            job.cancel()
+        }
+
+    @Test
     fun mobileConnectivity_default() {
         assertThat(underTest.defaultMobileNetworkConnectivity.value)
             .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
@@ -330,24 +691,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)
@@ -413,13 +756,14 @@
                     subscriptionManager,
                     telephonyManager,
                     logger,
+                    summaryLogger,
                     mobileMappings,
                     fakeBroadcastDispatcher,
-                    globalSettings,
                     context,
                     IMMEDIATE,
                     scope,
-                    connectionFactory,
+                    wifiRepository,
+                    fullConnectionFactory,
                 )
 
             var latest: MobileMappings.Config? = null
@@ -484,6 +828,38 @@
             job.cancel()
         }
 
+    @Test
+    fun activeDataChange_inSameGroup_emitsUnit() =
+        runBlocking(IMMEDIATE) {
+            var latest: Unit? = null
+            val job = underTest.activeSubChangedInGroupEvent.onEach { latest = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_4_ID_GROUPED)
+
+            assertThat(latest).isEqualTo(Unit)
+
+            job.cancel()
+        }
+
+    @Test
+    fun activeDataChange_notInSameGroup_doesNotEmit() =
+        runBlocking(IMMEDIATE) {
+            var latest: Unit? = null
+            val job = underTest.activeSubChangedInGroupEvent.onEach { latest = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_1_ID)
+
+            assertThat(latest).isEqualTo(null)
+
+            job.cancel()
+        }
+
     private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities =
         mock<NetworkCapabilities>().also {
             whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected)
@@ -517,17 +893,69 @@
 
     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 MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
 
+        // Subscription 1
+        private const val SUB_1_ID = 1
+        private val GROUP_1 = ParcelUuid(UUID.randomUUID())
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_1_ID)
+                whenever(it.groupUuid).thenReturn(GROUP_1)
+            }
+        private val MODEL_1 =
+            SubscriptionModel(
+                subscriptionId = SUB_1_ID,
+                groupUuid = GROUP_1,
+            )
+
+        // Subscription 2
         private const val SUB_2_ID = 2
+        private val GROUP_2 = ParcelUuid(UUID.randomUUID())
         private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
-        private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_2_ID)
+                whenever(it.groupUuid).thenReturn(GROUP_2)
+            }
+        private val MODEL_2 =
+            SubscriptionModel(
+                subscriptionId = SUB_2_ID,
+                groupUuid = GROUP_2,
+            )
+
+        // Subs 3 and 4 are considered to be in the same group ------------------------------------
+        private val GROUP_ID_3_4 = ParcelUuid(UUID.randomUUID())
+
+        // Subscription 3
+        private const val SUB_3_ID_GROUPED = 3
+        private val SUB_3 =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_3_ID_GROUPED)
+                whenever(it.groupUuid).thenReturn(GROUP_ID_3_4)
+            }
+
+        // Subscription 4
+        private const val SUB_4_ID_GROUPED = 4
+        private val SUB_4 =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_4_ID_GROUPED)
+                whenever(it.groupUuid).thenReturn(GROUP_ID_3_4)
+            }
+
+        // Subs 3 and 4 are considered to be in the same group ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
         private const val NET_ID = 123
         private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
+
+        // Carrier merged subscription
+        private const val SUB_CM_ID = 5
+        private val SUB_CM =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) }
+        private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID)
+        private val WIFI_NETWORK_CM =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 3,
+                subscriptionId = SUB_CM_ID,
+                level = 1,
+            )
     }
 }
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 a29146b..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
@@ -40,10 +40,12 @@
             )
         )
 
+    override val isConnected = MutableStateFlow(true)
+
     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
@@ -69,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 1c00646..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
@@ -23,6 +23,7 @@
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.log.table.TableLogBuffer
+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.util.MobileMappingsProxy
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -59,6 +60,9 @@
     override val alwaysShowDataRatIcon = MutableStateFlow(false)
 
     override val alwaysUseCdmaLevel = MutableStateFlow(false)
+    override val defaultDataSubId = MutableStateFlow(DEFAULT_DATA_SUB_ID)
+
+    override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
 
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
     override val defaultMobileIconMapping = _defaultMobileIconMapping
@@ -69,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)
@@ -77,6 +83,8 @@
     companion object {
         val DEFAULT_ICON = TelephonyIcons.G
 
+        const val DEFAULT_DATA_SUB_ID = 1
+
         // Use [MobileMappings] to define some simple definitions
         const val THREE_G = NETWORK_TYPE_GSM
         const val LTE = NETWORK_TYPE_LTE
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 61e13b8..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
@@ -25,6 +25,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 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.CarrierMergedNetworkType
 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.repository.FakeMobileConnectionRepository
@@ -60,9 +61,12 @@
                 mobileIconsInteractor.activeDataConnectionHasDataEnabled,
                 mobileIconsInteractor.alwaysShowDataRatIcon,
                 mobileIconsInteractor.alwaysUseCdmaLevel,
+                mobileIconsInteractor.defaultMobileNetworkConnectivity,
                 mobileIconsInteractor.defaultMobileIconMapping,
                 mobileIconsInteractor.defaultMobileIconGroup,
+                mobileIconsInteractor.defaultDataSubId,
                 mobileIconsInteractor.isDefaultConnectionFailed,
+                mobileIconsInteractor.isForceHidden,
                 connectionRepository,
             )
     }
@@ -271,6 +275,47 @@
         }
 
     @Test
+    fun iconGroup_carrierMerged_usesOverride() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    resolvedNetworkType = CarrierMergedNetworkType,
+                ),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `icon group - checks default data`() =
+        runBlocking(IMMEDIATE) {
+            mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+                ),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
+
+            // Default data sub id changes to something else
+            mobileIconsInteractor.defaultDataSubId.value = 123
+            yield()
+
+            assertThat(latest).isEqualTo(TelephonyIcons.NOT_DEFAULT_DATA)
+
+            job.cancel()
+        }
+
+    @Test
     fun alwaysShowDataRatIcon_matchesParent() =
         runBlocking(IMMEDIATE) {
             var latest: Boolean? = null
@@ -486,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))
@@ -506,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
 
@@ -515,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 b82a584..c51dbf1 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
+import android.os.ParcelUuid
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
 import com.android.settingslib.mobile.MobileMappings
@@ -27,15 +28,21 @@
 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
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
+import java.util.UUID
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.yield
 import org.junit.After
 import org.junit.Before
@@ -43,13 +50,17 @@
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @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()
-    private val scope = CoroutineScope(IMMEDIATE)
+
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
 
     @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
 
@@ -57,6 +68,8 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        connectivityRepository = FakeConnectivityRepository()
+
         connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer)
         connectionsRepository.setMobileConnectionRepositoryMap(
             mapOf(
@@ -72,8 +85,10 @@
             MobileIconsInteractorImpl(
                 connectionsRepository,
                 carrierConfigTracker,
+                tableLogger = mock(),
+                connectivityRepository,
                 userSetupRepository,
-                scope
+                testScope.backgroundScope,
             )
     }
 
@@ -81,7 +96,7 @@
 
     @Test
     fun filteredSubscriptions_default() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: List<SubscriptionModel>? = null
             val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
 
@@ -90,9 +105,24 @@
             job.cancel()
         }
 
+    // Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
+    @Test
+    fun filteredSubscriptions_moreThanTwo_doesNotFilter() =
+        testScope.runTest {
+            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
+
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
+
+            job.cancel()
+        }
+
     @Test
     fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
 
             var latest: List<SubscriptionModel>? = null
@@ -104,10 +134,50 @@
         }
 
     @Test
-    fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() =
-        runBlocking(IMMEDIATE) {
+    fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() =
+        testScope.runTest {
             connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() =
+        testScope.runTest {
+            val (sub1, sub2) =
+                createSubscriptionPair(
+                    subscriptionIds = Pair(SUB_1_ID, SUB_2_ID),
+                    opportunistic = Pair(true, true),
+                    grouped = false,
+                )
+            connectionsRepository.setSubscriptions(listOf(sub1, sub2))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf(sub1, sub2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() =
+        testScope.runTest {
+            val (sub3, sub4) =
+                createSubscriptionPair(
+                    subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+                    opportunistic = Pair(true, true),
+                    grouped = true,
+                )
+            connectionsRepository.setSubscriptions(listOf(sub3, sub4))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
@@ -115,15 +185,21 @@
             val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
 
             // Filtered subscriptions should show the active one when the config is false
-            assertThat(latest).isEqualTo(listOf(SUB_3_OPP))
+            assertThat(latest).isEqualTo(listOf(sub3))
 
             job.cancel()
         }
 
     @Test
-    fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() =
-        runBlocking(IMMEDIATE) {
-            connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+    fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() =
+        testScope.runTest {
+            val (sub3, sub4) =
+                createSubscriptionPair(
+                    subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+                    opportunistic = Pair(true, true),
+                    grouped = true,
+                )
+            connectionsRepository.setSubscriptions(listOf(sub3, sub4))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
@@ -132,15 +208,21 @@
             val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
 
             // Filtered subscriptions should show the active one when the config is false
-            assertThat(latest).isEqualTo(listOf(SUB_4_OPP))
+            assertThat(latest).isEqualTo(listOf(sub4))
 
             job.cancel()
         }
 
     @Test
-    fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() =
-        runBlocking(IMMEDIATE) {
-            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+    fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_active_1() =
+        testScope.runTest {
+            val (sub1, sub3) =
+                createSubscriptionPair(
+                    subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
+                    opportunistic = Pair(false, true),
+                    grouped = true,
+                )
+            connectionsRepository.setSubscriptions(listOf(sub1, sub3))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(true)
@@ -150,15 +232,21 @@
 
             // Filtered subscriptions should show the primary (non-opportunistic) if the config is
             // true
-            assertThat(latest).isEqualTo(listOf(SUB_1))
+            assertThat(latest).isEqualTo(listOf(sub1))
 
             job.cancel()
         }
 
     @Test
-    fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() =
-        runBlocking(IMMEDIATE) {
-            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+    fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_nonActive_1() =
+        testScope.runTest {
+            val (sub1, sub3) =
+                createSubscriptionPair(
+                    subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
+                    opportunistic = Pair(false, true),
+                    grouped = true,
+                )
+            connectionsRepository.setSubscriptions(listOf(sub1, sub3))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(true)
@@ -168,14 +256,14 @@
 
             // Filtered subscriptions should show the primary (non-opportunistic) if the config is
             // true
-            assertThat(latest).isEqualTo(listOf(SUB_1))
+            assertThat(latest).isEqualTo(listOf(sub1))
 
             job.cancel()
         }
 
     @Test
     fun activeDataConnection_turnedOn() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             CONNECTION_1.setDataEnabled(true)
             var latest: Boolean? = null
             val job =
@@ -188,7 +276,7 @@
 
     @Test
     fun activeDataConnection_turnedOff() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             CONNECTION_1.setDataEnabled(true)
             var latest: Boolean? = null
             val job =
@@ -204,7 +292,7 @@
 
     @Test
     fun activeDataConnection_invalidSubId() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job =
                 underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
@@ -220,7 +308,7 @@
 
     @Test
     fun failedConnection_connected_validated_notFailed() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
             connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true))
@@ -233,7 +321,7 @@
 
     @Test
     fun failedConnection_notConnected_notValidated_notFailed() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
 
@@ -247,7 +335,7 @@
 
     @Test
     fun failedConnection_connected_notValidated_failed() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
 
@@ -261,7 +349,7 @@
 
     @Test
     fun alwaysShowDataRatIcon_configHasTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
 
@@ -277,7 +365,7 @@
 
     @Test
     fun alwaysShowDataRatIcon_configHasFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
 
@@ -293,7 +381,7 @@
 
     @Test
     fun alwaysUseCdmaLevel_configHasTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
 
@@ -309,7 +397,7 @@
 
     @Test
     fun alwaysUseCdmaLevel_configHasFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
 
@@ -323,8 +411,339 @@
             job.cancel()
         }
 
+    @Test
+    fun `default mobile connectivity - uses repo value`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            var expected = MobileConnectivityModel(isConnected = true, isValidated = true)
+            connectionsRepository.setMobileConnectivity(expected)
+            assertThat(latest).isEqualTo(expected)
+
+            expected = MobileConnectivityModel(isConnected = false, isValidated = true)
+            connectionsRepository.setMobileConnectivity(expected)
+            assertThat(latest).isEqualTo(expected)
+
+            expected = MobileConnectivityModel(isConnected = true, isValidated = false)
+            connectionsRepository.setMobileConnectivity(expected)
+            assertThat(latest).isEqualTo(expected)
+
+            expected = MobileConnectivityModel(isConnected = false, isValidated = false)
+            connectionsRepository.setMobileConnectivity(expected)
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `data switch - in same group - validated matches previous value`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = true,
+                    isValidated = true,
+                )
+            )
+            // Trigger a data change in the same subscription group
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = false,
+                    isValidated = false,
+                )
+            )
+
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = true,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `data switch - in same group - validated matches previous value - expires after 2s`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = true,
+                    isValidated = true,
+                )
+            )
+            // Trigger a data change in the same subscription group
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = false,
+                    isValidated = false,
+                )
+            )
+            // After 1s, the force validation bit is still present
+            advanceTimeBy(1000)
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = true,
+                    )
+                )
+
+            // After 2s, the force validation expires
+            advanceTimeBy(1001)
+
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = false,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `data switch - in same group - not validated - uses new value immediately`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = true,
+                    isValidated = false,
+                )
+            )
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = false,
+                    isValidated = false,
+                )
+            )
+
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = false,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `data switch - lose validation - then switch happens - clears forced bit`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            // GIVEN the network starts validated
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = true,
+                    isValidated = true,
+                )
+            )
+
+            // WHEN a data change happens in the same group
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+
+            // WHEN the validation bit is lost
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = false,
+                    isValidated = false,
+                )
+            )
+
+            // WHEN another data change happens in the same group
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+
+            // THEN the forced validation bit is still removed after 2s
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = true,
+                    )
+                )
+
+            advanceTimeBy(1000)
+
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = true,
+                    )
+                )
+
+            advanceTimeBy(1001)
+
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = false,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `data switch - while already forcing validation - resets clock`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = true,
+                    isValidated = true,
+                )
+            )
+
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+
+            advanceTimeBy(1000)
+
+            // WHEN another change in same group event happens
+            connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = false,
+                    isValidated = false,
+                )
+            )
+
+            // THEN the forced validation remains for exactly 2 more seconds from now
+
+            // 1.500s from second event
+            advanceTimeBy(1500)
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = true,
+                    )
+                )
+
+            // 2.001s from the second event
+            advanceTimeBy(501)
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = false,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `data switch - not in same group - uses new values`() =
+        testScope.runTest {
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = true,
+                    isValidated = true,
+                )
+            )
+            connectionsRepository.setMobileConnectivity(
+                MobileConnectivityModel(
+                    isConnected = false,
+                    isValidated = false,
+                )
+            )
+
+            assertThat(latest)
+                .isEqualTo(
+                    MobileConnectivityModel(
+                        isConnected = false,
+                        isValidated = false,
+                    )
+                )
+
+            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()
+        }
+
+    /**
+     * Convenience method for creating a pair of subscriptions to test the filteredSubscriptions
+     * flow.
+     */
+    private fun createSubscriptionPair(
+        subscriptionIds: Pair<Int, Int>,
+        opportunistic: Pair<Boolean, Boolean> = Pair(false, false),
+        grouped: Boolean = false,
+    ): Pair<SubscriptionModel, SubscriptionModel> {
+        val groupUuid = if (grouped) ParcelUuid(UUID.randomUUID()) else null
+        val sub1 =
+            SubscriptionModel(
+                subscriptionId = subscriptionIds.first,
+                isOpportunistic = opportunistic.first,
+                groupUuid = groupUuid,
+            )
+
+        val sub2 =
+            SubscriptionModel(
+                subscriptionId = subscriptionIds.second,
+                isOpportunistic = opportunistic.second,
+                groupUuid = groupUuid,
+            )
+
+        return Pair(sub1, sub2)
+    }
+
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
         private val tableLogBuffer =
             TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())
 
@@ -337,11 +756,21 @@
         private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, tableLogBuffer)
 
         private const val SUB_3_ID = 3
-        private val SUB_3_OPP = SubscriptionModel(subscriptionId = SUB_3_ID, isOpportunistic = true)
+        private val SUB_3_OPP =
+            SubscriptionModel(
+                subscriptionId = SUB_3_ID,
+                isOpportunistic = true,
+                groupUuid = ParcelUuid(UUID.randomUUID()),
+            )
         private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, tableLogBuffer)
 
         private const val SUB_4_ID = 4
-        private val SUB_4_OPP = SubscriptionModel(subscriptionId = SUB_4_ID, isOpportunistic = true)
+        private val SUB_4_OPP =
+            SubscriptionModel(
+                subscriptionId = SUB_4_ID,
+                isOpportunistic = true,
+                groupUuid = ParcelUuid(UUID.randomUUID()),
+            )
         private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, tableLogBuffer)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLoggerTest.kt
new file mode 100644
index 0000000..86529dc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLoggerTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.shared
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.plugins.log.LogcatEchoTracker
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+
+@SmallTest
+class MobileInputLoggerTest : SysuiTestCase() {
+    private val buffer =
+        LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java)).create("buffer", 10)
+    private val logger = MobileInputLogger(buffer)
+
+    @Test
+    fun testLogNetworkCapsChange_bufferHasInfo() {
+        logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS, isDefaultNetworkCallback = true)
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+
+        val expectedNetId = NET_1_ID.toString()
+        val expectedCaps = NET_1_CAPS.toString()
+
+        assertThat(actualString).contains("true")
+        assertThat(actualString).contains(expectedNetId)
+        assertThat(actualString).contains(expectedCaps)
+    }
+
+    @Test
+    fun testLogOnLost_bufferHasNetIdOfLostNetwork() {
+        logger.logOnLost(NET_1)
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+
+        val expectedNetId = NET_1_ID.toString()
+
+        assertThat(actualString).contains(expectedNetId)
+    }
+
+    companion object {
+        private const val NET_1_ID = 100
+        private val NET_1 =
+            com.android.systemui.util.mockito.mock<Network>().also {
+                Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID)
+            }
+        private val NET_1_CAPS =
+            NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .build()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/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 2a8d42f..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
@@ -17,16 +17,20 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
 import androidx.test.filters.SmallTest
-import com.android.settingslib.graph.SignalDrawable
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
 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
@@ -47,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
 
@@ -57,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)
@@ -67,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)
@@ -88,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)
@@ -100,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
 
@@ -122,6 +215,39 @@
         }
 
     @Test
+    fun contentDescription_notInService_usesNoPhone() =
+        testScope.runTest {
+            var latest: ContentDescription? = null
+            val job = underTest.contentDescription.onEach { latest = it }.launchIn(this)
+
+            interactor.isInService.value = false
+
+            assertThat((latest as ContentDescription.Resource).res)
+                .isEqualTo(PHONE_SIGNAL_STRENGTH_NONE)
+
+            job.cancel()
+        }
+
+    @Test
+    fun contentDescription_inService_usesLevel() =
+        testScope.runTest {
+            var latest: ContentDescription? = null
+            val job = underTest.contentDescription.onEach { latest = it }.launchIn(this)
+
+            interactor.isInService.value = true
+
+            interactor.level.value = 2
+            assertThat((latest as ContentDescription.Resource).res)
+                .isEqualTo(PHONE_SIGNAL_STRENGTH[2])
+
+            interactor.level.value = 0
+            assertThat((latest as ContentDescription.Resource).res)
+                .isEqualTo(PHONE_SIGNAL_STRENGTH[0])
+
+            job.cancel()
+        }
+
+    @Test
     fun networkType_dataEnabled_groupIsRepresented() =
         testScope.runTest {
             val expected =
@@ -274,6 +400,41 @@
         }
 
     @Test
+    fun `network type - alwaysShow - shown when not connected`() =
+        testScope.runTest {
+            interactor.setIconGroup(THREE_G)
+            interactor.isConnected.value = false
+            interactor.alwaysShowDataRatIcon.value = true
+
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            val expected =
+                Icon.Resource(
+                    THREE_G.dataType,
+                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                )
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network type - not shown when not connected`() =
+        testScope.runTest {
+            interactor.setIconGroup(THREE_G)
+            interactor.isDataConnected.value = true
+            interactor.isConnected.value = false
+
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
     fun roaming() =
         testScope.runTest {
             interactor.isRoaming.value = true
@@ -294,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)
@@ -333,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)
@@ -389,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
 
@@ -396,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..4628f84 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,13 @@
 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,8 +48,8 @@
     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
 
     private val testDispatcher = UnconfinedTestDispatcher()
@@ -57,6 +59,12 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        airplaneModeInteractor =
+            AirplaneModeInteractor(
+                FakeAirplaneModeRepository(),
+                FakeConnectivityRepository(),
+            )
+
         val subscriptionIdsFlow =
             interactor.filteredSubscriptions
                 .map { subs -> subs.map { it.subscriptionId } }
@@ -66,7 +74,7 @@
             MobileIconsViewModel(
                 subscriptionIdsFlow,
                 interactor,
-                logger,
+                airplaneModeInteractor,
                 constants,
                 testScope.backgroundScope,
                 statusBarPipelineFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
deleted file mode 100644
index b32058f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.shared
-
-import android.net.Network
-import android.net.NetworkCapabilities
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
-import com.google.common.truth.Truth.assertThat
-import java.io.PrintWriter
-import java.io.StringWriter
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.mockito.Mockito
-import org.mockito.Mockito.mock
-
-@SmallTest
-class ConnectivityPipelineLoggerTest : SysuiTestCase() {
-    private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
-        .create("buffer", 10)
-    private val logger = ConnectivityPipelineLogger(buffer)
-
-    @Test
-    fun testLogNetworkCapsChange_bufferHasInfo() {
-        logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS)
-
-        val stringWriter = StringWriter()
-        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
-        val expectedNetId = NET_1_ID.toString()
-        val expectedCaps = NET_1_CAPS.toString()
-
-        assertThat(actualString).contains(expectedNetId)
-        assertThat(actualString).contains(expectedCaps)
-    }
-
-    @Test
-    fun testLogOnLost_bufferHasNetIdOfLostNetwork() {
-        logger.logOnLost(NET_1)
-
-        val stringWriter = StringWriter()
-        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
-        val expectedNetId = NET_1_ID.toString()
-
-        assertThat(actualString).contains(expectedNetId)
-    }
-
-    @Test
-    fun logOutputChange_printsValuesAndNulls() = runBlocking(IMMEDIATE) {
-        val flow: Flow<Int?> = flowOf(1, null, 3)
-
-        val job = flow
-            .logOutputChange(logger, "testInts")
-            .launchIn(this)
-
-        val stringWriter = StringWriter()
-        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
-        assertThat(actualString).contains("1")
-        assertThat(actualString).contains("null")
-        assertThat(actualString).contains("3")
-
-        job.cancel()
-    }
-
-    @Test
-    fun logInputChange_unit_printsInputName() = runBlocking(IMMEDIATE) {
-        val flow: Flow<Unit> = flowOf(Unit, Unit)
-
-        val job = flow
-            .logInputChange(logger, "testInputs")
-            .launchIn(this)
-
-        val stringWriter = StringWriter()
-        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
-        assertThat(actualString).contains("testInputs")
-
-        job.cancel()
-    }
-
-    @Test
-    fun logInputChange_any_printsValuesAndNulls() = runBlocking(IMMEDIATE) {
-        val flow: Flow<Any?> = flowOf(null, 2, "threeString")
-
-        val job = flow
-            .logInputChange(logger, "testInputs")
-            .launchIn(this)
-
-        val stringWriter = StringWriter()
-        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
-        assertThat(actualString).contains("null")
-        assertThat(actualString).contains("2")
-        assertThat(actualString).contains("threeString")
-
-        job.cancel()
-    }
-
-    companion object {
-        private const val NET_1_ID = 100
-        private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also {
-            Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID)
-        }
-        private val NET_1_CAPS = NetworkCapabilities.Builder()
-            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
-            .build()
-        private val IMMEDIATE = Dispatchers.Main.immediate
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
index 6dbee2f..496f090 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityInputLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.DEFAULT_HIDDEN_ICONS_RESOURCE
@@ -52,7 +52,7 @@
 
     @Mock private lateinit var connectivitySlots: ConnectivitySlots
     @Mock private lateinit var dumpManager: DumpManager
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var logger: ConnectivityInputLogger
     private lateinit var scope: CoroutineScope
     @Mock private lateinit var tunerService: TunerService
 
@@ -61,14 +61,15 @@
         MockitoAnnotations.initMocks(this)
         scope = CoroutineScope(IMMEDIATE)
 
-        underTest = ConnectivityRepositoryImpl(
-            connectivitySlots,
-            context,
-            dumpManager,
-            logger,
-            scope,
-            tunerService,
-        )
+        underTest =
+            ConnectivityRepositoryImpl(
+                connectivitySlots,
+                context,
+                dumpManager,
+                logger,
+                scope,
+                tunerService,
+            )
     }
 
     @After
@@ -77,199 +78,179 @@
     }
 
     @Test
-    fun forceHiddenSlots_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
-        setUpEthernetWifiMobileSlotNames()
-        context.getOrCreateTestableResources().addOverride(
-            DEFAULT_HIDDEN_ICONS_RESOURCE,
-            arrayOf(SLOT_WIFI, SLOT_ETHERNET)
-        )
-        // Re-create our [ConnectivityRepositoryImpl], since it fetches
-        // config_statusBarIconsToExclude when it's first constructed
-        underTest = ConnectivityRepositoryImpl(
-            connectivitySlots,
-            context,
-            dumpManager,
-            logger,
-            scope,
-            tunerService,
-        )
+    fun forceHiddenSlots_initiallyGetsDefault() =
+        runBlocking(IMMEDIATE) {
+            setUpEthernetWifiMobileSlotNames()
+            context
+                .getOrCreateTestableResources()
+                .addOverride(DEFAULT_HIDDEN_ICONS_RESOURCE, arrayOf(SLOT_WIFI, SLOT_ETHERNET))
+            // Re-create our [ConnectivityRepositoryImpl], since it fetches
+            // config_statusBarIconsToExclude when it's first constructed
+            underTest =
+                ConnectivityRepositoryImpl(
+                    connectivitySlots,
+                    context,
+                    dumpManager,
+                    logger,
+                    scope,
+                    tunerService,
+                )
 
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).containsExactly(ConnectivitySlot.ETHERNET, ConnectivitySlot.WIFI)
+            assertThat(latest).containsExactly(ConnectivitySlot.ETHERNET, ConnectivitySlot.WIFI)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_slotNamesAdded_flowHasSlots() = runBlocking(IMMEDIATE) {
-        setUpEthernetWifiMobileSlotNames()
+    fun forceHiddenSlots_slotNamesAdded_flowHasSlots() =
+        runBlocking(IMMEDIATE) {
+            setUpEthernetWifiMobileSlotNames()
 
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
 
-        assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
+            assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_wrongKey_doesNotUpdate() = runBlocking(IMMEDIATE) {
-        setUpEthernetWifiMobileSlotNames()
+    fun forceHiddenSlots_wrongKey_doesNotUpdate() =
+        runBlocking(IMMEDIATE) {
+            setUpEthernetWifiMobileSlotNames()
 
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
 
-        // WHEN onTuningChanged with the wrong key
-        getTunable().onTuningChanged("wrongKey", SLOT_WIFI)
-        yield()
+            // WHEN onTuningChanged with the wrong key
+            getTunable().onTuningChanged("wrongKey", SLOT_WIFI)
+            yield()
 
-        // THEN we didn't update our value and still have the old one
-        assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
+            // THEN we didn't update our value and still have the old one
+            assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_slotNamesAddedThenNull_flowHasDefault() = runBlocking(IMMEDIATE) {
-        setUpEthernetWifiMobileSlotNames()
-        context.getOrCreateTestableResources().addOverride(
-            DEFAULT_HIDDEN_ICONS_RESOURCE,
-            arrayOf(SLOT_WIFI, SLOT_ETHERNET)
-        )
-        // Re-create our [ConnectivityRepositoryImpl], since it fetches
-        // config_statusBarIconsToExclude when it's first constructed
-        underTest = ConnectivityRepositoryImpl(
-            connectivitySlots,
-            context,
-            dumpManager,
-            logger,
-            scope,
-            tunerService,
-        )
+    fun forceHiddenSlots_slotNamesAddedThenNull_flowHasDefault() =
+        runBlocking(IMMEDIATE) {
+            setUpEthernetWifiMobileSlotNames()
+            context
+                .getOrCreateTestableResources()
+                .addOverride(DEFAULT_HIDDEN_ICONS_RESOURCE, arrayOf(SLOT_WIFI, SLOT_ETHERNET))
+            // Re-create our [ConnectivityRepositoryImpl], since it fetches
+            // config_statusBarIconsToExclude when it's first constructed
+            underTest =
+                ConnectivityRepositoryImpl(
+                    connectivitySlots,
+                    context,
+                    dumpManager,
+                    logger,
+                    scope,
+                    tunerService,
+                )
 
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        // First, update the slots
-        getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
-        assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
+            // First, update the slots
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
+            assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
 
-        // WHEN we update to a null value
-        getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, null)
-        yield()
+            // WHEN we update to a null value
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, null)
+            yield()
 
-        // THEN we go back to our default value
-        assertThat(latest).containsExactly(ConnectivitySlot.ETHERNET, ConnectivitySlot.WIFI)
+            // THEN we go back to our default value
+            assertThat(latest).containsExactly(ConnectivitySlot.ETHERNET, ConnectivitySlot.WIFI)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_someInvalidSlotNames_flowHasValidSlotsOnly() = runBlocking(IMMEDIATE) {
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+    fun forceHiddenSlots_someInvalidSlotNames_flowHasValidSlotsOnly() =
+        runBlocking(IMMEDIATE) {
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        whenever(connectivitySlots.getSlotFromName(SLOT_WIFI))
-            .thenReturn(ConnectivitySlot.WIFI)
-        whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(null)
+            whenever(connectivitySlots.getSlotFromName(SLOT_WIFI)).thenReturn(ConnectivitySlot.WIFI)
+            whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(null)
 
-        getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_WIFI,$SLOT_MOBILE")
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_WIFI,$SLOT_MOBILE")
 
-        assertThat(latest).containsExactly(ConnectivitySlot.WIFI)
+            assertThat(latest).containsExactly(ConnectivitySlot.WIFI)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_someEmptySlotNames_flowHasValidSlotsOnly() = runBlocking(IMMEDIATE) {
-        setUpEthernetWifiMobileSlotNames()
+    fun forceHiddenSlots_someEmptySlotNames_flowHasValidSlotsOnly() =
+        runBlocking(IMMEDIATE) {
+            setUpEthernetWifiMobileSlotNames()
 
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        // WHEN there's empty and blank slot names
-        getTunable().onTuningChanged(
-            HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_MOBILE,  ,,$SLOT_WIFI"
-        )
+            // WHEN there's empty and blank slot names
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_MOBILE,  ,,$SLOT_WIFI")
 
-        // THEN we skip that slot but still process the other ones
-        assertThat(latest).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.MOBILE)
+            // THEN we skip that slot but still process the other ones
+            assertThat(latest).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.MOBILE)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_allInvalidOrEmptySlotNames_flowHasEmpty() = runBlocking(IMMEDIATE) {
-        var latest: Set<ConnectivitySlot>? = null
-        val job = underTest
-            .forceHiddenSlots
-            .onEach { latest = it }
-            .launchIn(this)
+    fun forceHiddenSlots_allInvalidOrEmptySlotNames_flowHasEmpty() =
+        runBlocking(IMMEDIATE) {
+            var latest: Set<ConnectivitySlot>? = null
+            val job = underTest.forceHiddenSlots.onEach { latest = it }.launchIn(this)
 
-        whenever(connectivitySlots.getSlotFromName(SLOT_WIFI)).thenReturn(null)
-        whenever(connectivitySlots.getSlotFromName(SLOT_ETHERNET)).thenReturn(null)
-        whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(null)
+            whenever(connectivitySlots.getSlotFromName(SLOT_WIFI)).thenReturn(null)
+            whenever(connectivitySlots.getSlotFromName(SLOT_ETHERNET)).thenReturn(null)
+            whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(null)
 
-        getTunable().onTuningChanged(
-            HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_MOBILE,,$SLOT_WIFI,$SLOT_ETHERNET,,,"
-        )
+            getTunable()
+                .onTuningChanged(
+                    HIDDEN_ICONS_TUNABLE_KEY,
+                    "$SLOT_MOBILE,,$SLOT_WIFI,$SLOT_ETHERNET,,,"
+                )
 
-        assertThat(latest).isEmpty()
+            assertThat(latest).isEmpty()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun forceHiddenSlots_newSubscriberGetsCurrentValue() = runBlocking(IMMEDIATE) {
-        setUpEthernetWifiMobileSlotNames()
+    fun forceHiddenSlots_newSubscriberGetsCurrentValue() =
+        runBlocking(IMMEDIATE) {
+            setUpEthernetWifiMobileSlotNames()
 
-        var latest1: Set<ConnectivitySlot>? = null
-        val job1 = underTest
-            .forceHiddenSlots
-            .onEach { latest1 = it }
-            .launchIn(this)
+            var latest1: Set<ConnectivitySlot>? = null
+            val job1 = underTest.forceHiddenSlots.onEach { latest1 = it }.launchIn(this)
 
-        getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_WIFI,$SLOT_ETHERNET")
+            getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_WIFI,$SLOT_ETHERNET")
 
-        assertThat(latest1).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.ETHERNET)
+            assertThat(latest1).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.ETHERNET)
 
-        // WHEN we add a second subscriber after having already emitted a value
-        var latest2: Set<ConnectivitySlot>? = null
-        val job2 = underTest
-            .forceHiddenSlots
-            .onEach { latest2 = it }
-            .launchIn(this)
+            // WHEN we add a second subscriber after having already emitted a value
+            var latest2: Set<ConnectivitySlot>? = null
+            val job2 = underTest.forceHiddenSlots.onEach { latest2 = it }.launchIn(this)
 
-        // THEN the second subscribe receives the already-emitted value
-        assertThat(latest2).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.ETHERNET)
+            // THEN the second subscribe receives the already-emitted value
+            assertThat(latest2).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.ETHERNET)
 
-        job1.cancel()
-        job2.cancel()
-    }
+            job1.cancel()
+            job2.cancel()
+        }
 
     private fun getTunable(): TunerService.Tunable {
         val callbackCaptor = argumentCaptor<TunerService.Tunable>()
@@ -280,10 +261,8 @@
     private fun setUpEthernetWifiMobileSlotNames() {
         whenever(connectivitySlots.getSlotFromName(SLOT_ETHERNET))
             .thenReturn(ConnectivitySlot.ETHERNET)
-        whenever(connectivitySlots.getSlotFromName(SLOT_WIFI))
-            .thenReturn(ConnectivitySlot.WIFI)
-        whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE))
-            .thenReturn(ConnectivitySlot.MOBILE)
+        whenever(connectivitySlots.getSlotFromName(SLOT_WIFI)).thenReturn(ConnectivitySlot.WIFI)
+        whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(ConnectivitySlot.MOBILE)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
index 3fe6983..e4c8fd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.shared.ui.view
 
+import android.graphics.Rect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
@@ -118,6 +119,22 @@
         assertThat(view.isIconVisible).isEqualTo(false)
     }
 
+    @Test
+    fun getDrawingRect_takesTranslationIntoAccount() {
+        val view = createAndInitView()
+
+        view.translationX = 50f
+        view.translationY = 60f
+
+        val drawingRect = Rect()
+        view.getDrawingRect(drawingRect)
+
+        assertThat(drawingRect.left).isEqualTo(view.left + 50)
+        assertThat(drawingRect.right).isEqualTo(view.right + 50)
+        assertThat(drawingRect.top).isEqualTo(view.top + 60)
+        assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60)
+    }
+
     private fun createAndInitView(): ModernStatusBarView {
         val view = ModernStatusBarView(context, null)
         binding = TestBinding()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index f5837d6..1bf431b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 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.prod.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
index 1085c2b..25678b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -23,11 +23,11 @@
 import com.android.systemui.demomode.DemoMode
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
 import com.android.systemui.util.mockito.whenever
@@ -47,6 +47,7 @@
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @SmallTest
 class WifiRepositorySwitcherTest : SysuiTestCase() {
     private lateinit var underTest: WifiRepositorySwitcher
@@ -54,7 +55,7 @@
     private lateinit var demoImpl: DemoWifiRepository
 
     @Mock private lateinit var demoModeController: DemoModeController
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var logger: WifiInputLogger
     @Mock private lateinit var tableLogger: TableLogBuffer
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var wifiManager: WifiManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index 3c4e85b..9cf08c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 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.shared.model.WifiNetworkModel
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 8f07615..c7b31bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -21,19 +21,23 @@
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.TransportInfo
+import android.net.VpnTransportInfo
 import android.net.vcn.VcnTransportInfo
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 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.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
@@ -68,7 +72,7 @@
     private lateinit var underTest: WifiRepositoryImpl
 
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var logger: WifiInputLogger
     @Mock private lateinit var tableLogger: TableLogBuffer
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var wifiManager: WifiManager
@@ -79,13 +83,14 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(
-            broadcastDispatcher.broadcastFlow(
-                any(),
-                nullable(),
-                anyInt(),
-                nullable(),
+                broadcastDispatcher.broadcastFlow(
+                    any(),
+                    nullable(),
+                    anyInt(),
+                    nullable(),
+                )
             )
-        ).thenReturn(flowOf(Unit))
+            .thenReturn(flowOf(Unit))
         executor = FakeExecutor(FakeSystemClock())
         scope = CoroutineScope(IMMEDIATE)
         underTest = createRepo()
@@ -97,686 +102,830 @@
     }
 
     @Test
-    fun isWifiEnabled_initiallyGetsWifiManagerValue() = runBlocking(IMMEDIATE) {
-        whenever(wifiManager.isWifiEnabled).thenReturn(true)
+    fun isWifiEnabled_initiallyGetsWifiManagerValue() =
+        runBlocking(IMMEDIATE) {
+            whenever(wifiManager.isWifiEnabled).thenReturn(true)
 
-        underTest = createRepo()
+            underTest = createRepo()
 
-        assertThat(underTest.isWifiEnabled.value).isTrue()
-    }
-
-    @Test
-    fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() = runBlocking(IMMEDIATE) {
-        // We need to call launch on the flows so that they start updating
-        val networkJob = underTest.wifiNetwork.launchIn(this)
-        val enabledJob = underTest.isWifiEnabled.launchIn(this)
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(true)
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
-        )
-
-        assertThat(underTest.isWifiEnabled.value).isTrue()
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(false)
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
-        )
-
-        assertThat(underTest.isWifiEnabled.value).isFalse()
-
-        networkJob.cancel()
-        enabledJob.cancel()
-    }
-
-    @Test
-    fun isWifiEnabled_networkLost_valueUpdated() = runBlocking(IMMEDIATE) {
-        // We need to call launch on the flows so that they start updating
-        val networkJob = underTest.wifiNetwork.launchIn(this)
-        val enabledJob = underTest.isWifiEnabled.launchIn(this)
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(true)
-        getNetworkCallback().onLost(NETWORK)
-
-        assertThat(underTest.isWifiEnabled.value).isTrue()
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(false)
-        getNetworkCallback().onLost(NETWORK)
-
-        assertThat(underTest.isWifiEnabled.value).isFalse()
-
-        networkJob.cancel()
-        enabledJob.cancel()
-    }
-
-    @Test
-    fun isWifiEnabled_intentsReceived_valueUpdated() = runBlocking(IMMEDIATE) {
-        val intentFlow = MutableSharedFlow<Unit>()
-        whenever(
-            broadcastDispatcher.broadcastFlow(
-                any(),
-                nullable(),
-                anyInt(),
-                nullable(),
-            )
-        ).thenReturn(intentFlow)
-        underTest = createRepo()
-
-        val job = underTest.isWifiEnabled.launchIn(this)
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(true)
-        intentFlow.emit(Unit)
-
-        assertThat(underTest.isWifiEnabled.value).isTrue()
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(false)
-        intentFlow.emit(Unit)
-
-        assertThat(underTest.isWifiEnabled.value).isFalse()
-
-        job.cancel()
-    }
-
-    @Test
-    fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() = runBlocking(IMMEDIATE) {
-        val intentFlow = MutableSharedFlow<Unit>()
-        whenever(
-            broadcastDispatcher.broadcastFlow(
-                any(),
-                nullable(),
-                anyInt(),
-                nullable(),
-            )
-        ).thenReturn(intentFlow)
-        underTest = createRepo()
-
-        val networkJob = underTest.wifiNetwork.launchIn(this)
-        val enabledJob = underTest.isWifiEnabled.launchIn(this)
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(false)
-        intentFlow.emit(Unit)
-        assertThat(underTest.isWifiEnabled.value).isFalse()
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(true)
-        getNetworkCallback().onLost(NETWORK)
-        assertThat(underTest.isWifiEnabled.value).isTrue()
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(false)
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
-        )
-        assertThat(underTest.isWifiEnabled.value).isFalse()
-
-        whenever(wifiManager.isWifiEnabled).thenReturn(true)
-        intentFlow.emit(Unit)
-        assertThat(underTest.isWifiEnabled.value).isTrue()
-
-        networkJob.cancel()
-        enabledJob.cancel()
-    }
-
-    @Test
-    fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
-        val job = underTest.isWifiDefault.launchIn(this)
-
-        assertThat(underTest.isWifiDefault.value).isFalse()
-
-        job.cancel()
-    }
-
-    @Test
-    fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) {
-        val job = underTest.isWifiDefault.launchIn(this)
-
-        val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
+            assertThat(underTest.isWifiEnabled.value).isTrue()
         }
 
-        getDefaultNetworkCallback().onCapabilitiesChanged(
-            NETWORK,
-            createWifiNetworkCapabilities(wifiInfo)
-        )
-
-        assertThat(underTest.isWifiDefault.value).isTrue()
-
-        job.cancel()
-    }
-
     @Test
-    fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) {
-        val job = underTest.isWifiDefault.launchIn(this)
+    fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() =
+        runBlocking(IMMEDIATE) {
+            // We need to call launch on the flows so that they start updating
+            val networkJob = underTest.wifiNetwork.launchIn(this)
+            val enabledJob = underTest.isWifiEnabled.launchIn(this)
 
-        val capabilities = mock<NetworkCapabilities>().apply {
-            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-            whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+            whenever(wifiManager.isWifiEnabled).thenReturn(true)
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+            assertThat(underTest.isWifiEnabled.value).isTrue()
+
+            whenever(wifiManager.isWifiEnabled).thenReturn(false)
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+            assertThat(underTest.isWifiEnabled.value).isFalse()
+
+            networkJob.cancel()
+            enabledJob.cancel()
         }
 
-        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
-
-        assertThat(underTest.isWifiDefault.value).isTrue()
-
-        job.cancel()
-    }
-
     @Test
-    fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) {
-        val job = underTest.isWifiDefault.launchIn(this)
+    fun isWifiEnabled_networkLost_valueUpdated() =
+        runBlocking(IMMEDIATE) {
+            // We need to call launch on the flows so that they start updating
+            val networkJob = underTest.wifiNetwork.launchIn(this)
+            val enabledJob = underTest.isWifiEnabled.launchIn(this)
 
-        val capabilities = mock<NetworkCapabilities>().apply {
-            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-            whenever(this.transportInfo).thenReturn(mock())
+            whenever(wifiManager.isWifiEnabled).thenReturn(true)
+            getNetworkCallback().onLost(NETWORK)
+
+            assertThat(underTest.isWifiEnabled.value).isTrue()
+
+            whenever(wifiManager.isWifiEnabled).thenReturn(false)
+            getNetworkCallback().onLost(NETWORK)
+
+            assertThat(underTest.isWifiEnabled.value).isFalse()
+
+            networkJob.cancel()
+            enabledJob.cancel()
         }
 
-        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
-
-        assertThat(underTest.isWifiDefault.value).isFalse()
-
-        job.cancel()
-    }
-
     @Test
-    fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) {
-        val job = underTest.isWifiDefault.launchIn(this)
+    fun isWifiEnabled_intentsReceived_valueUpdated() =
+        runBlocking(IMMEDIATE) {
+            val intentFlow = MutableSharedFlow<Unit>()
+            whenever(
+                    broadcastDispatcher.broadcastFlow(
+                        any(),
+                        nullable(),
+                        anyInt(),
+                        nullable(),
+                    )
+                )
+                .thenReturn(intentFlow)
+            underTest = createRepo()
 
-        // First, add a network
-        getDefaultNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
-        assertThat(underTest.isWifiDefault.value).isTrue()
+            val job = underTest.isWifiEnabled.launchIn(this)
 
-        // WHEN the network is lost
-        getDefaultNetworkCallback().onLost(NETWORK)
+            whenever(wifiManager.isWifiEnabled).thenReturn(true)
+            intentFlow.emit(Unit)
 
-        // THEN we update to false
-        assertThat(underTest.isWifiDefault.value).isFalse()
+            assertThat(underTest.isWifiEnabled.value).isTrue()
 
-        job.cancel()
-    }
+            whenever(wifiManager.isWifiEnabled).thenReturn(false)
+            intentFlow.emit(Unit)
 
-    @Test
-    fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+            assertThat(underTest.isWifiEnabled.value).isFalse()
 
-        assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT)
-
-        job.cancel()
-    }
-
-    @Test
-    fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
-
-        val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
-            whenever(this.isPrimary).thenReturn(true)
-        }
-        val network = mock<Network>().apply {
-            whenever(this.getNetId()).thenReturn(NETWORK_ID)
+            job.cancel()
         }
 
-        getNetworkCallback().onCapabilitiesChanged(network, createWifiNetworkCapabilities(wifiInfo))
-
-        assertThat(latest is WifiNetworkModel.Active).isTrue()
-        val latestActive = latest as WifiNetworkModel.Active
-        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latestActive.ssid).isEqualTo(SSID)
-
-        job.cancel()
-    }
-
     @Test
-    fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() =
+        runBlocking(IMMEDIATE) {
+            val intentFlow = MutableSharedFlow<Unit>()
+            whenever(
+                    broadcastDispatcher.broadcastFlow(
+                        any(),
+                        nullable(),
+                        anyInt(),
+                        nullable(),
+                    )
+                )
+                .thenReturn(intentFlow)
+            underTest = createRepo()
 
-        val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
-            whenever(this.isPrimary).thenReturn(true)
-            whenever(this.isCarrierMerged).thenReturn(true)
+            val networkJob = underTest.wifiNetwork.launchIn(this)
+            val enabledJob = underTest.isWifiEnabled.launchIn(this)
+
+            whenever(wifiManager.isWifiEnabled).thenReturn(false)
+            intentFlow.emit(Unit)
+            assertThat(underTest.isWifiEnabled.value).isFalse()
+
+            whenever(wifiManager.isWifiEnabled).thenReturn(true)
+            getNetworkCallback().onLost(NETWORK)
+            assertThat(underTest.isWifiEnabled.value).isTrue()
+
+            whenever(wifiManager.isWifiEnabled).thenReturn(false)
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            assertThat(underTest.isWifiEnabled.value).isFalse()
+
+            whenever(wifiManager.isWifiEnabled).thenReturn(true)
+            intentFlow.emit(Unit)
+            assertThat(underTest.isWifiEnabled.value).isTrue()
+
+            networkJob.cancel()
+            enabledJob.cancel()
         }
 
-        getNetworkCallback().onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
-
-        assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
-
-        job.cancel()
-    }
-
     @Test
-    fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isWifiDefault_initiallyGetsDefault() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
 
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO, isValidated = false)
-        )
+            assertThat(underTest.isWifiDefault.value).isFalse()
 
-        assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse()
-
-        job.cancel()
-    }
-
-    @Test
-    fun wifiNetwork_validated_networkValidated() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
-
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO, isValidated = true)
-        )
-
-        assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue()
-
-        job.cancel()
-    }
-
-    @Test
-    fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
-
-        val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
-            whenever(this.isPrimary).thenReturn(false)
+            job.cancel()
         }
 
-        getNetworkCallback().onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+    @Test
+    fun isWifiDefault_wifiNetwork_isTrue() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
 
-        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+            val wifiInfo = mock<WifiInfo>().apply { whenever(this.ssid).thenReturn(SSID) }
 
-        job.cancel()
-    }
+            getDefaultNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+            assertThat(underTest.isWifiDefault.value).isTrue()
+
+            job.cancel()
+        }
+
+    /** Regression test for b/266628069. */
+    @Test
+    fun isWifiDefault_transportInfoIsNotWifi_andNoWifiTransport_false() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
+
+            val transportInfo =
+                VpnTransportInfo(
+                    /* type= */ 0,
+                    /* sessionId= */ "sessionId",
+                )
+            val networkCapabilities =
+                mock<NetworkCapabilities>().also {
+                    whenever(it.hasTransport(TRANSPORT_VPN)).thenReturn(true)
+                    whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false)
+                    whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
+                    whenever(it.transportInfo).thenReturn(transportInfo)
+                }
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities)
+
+            assertThat(underTest.isWifiDefault.value).isFalse()
+
+            job.cancel()
+        }
+
+    /** Regression test for b/266628069. */
+    @Test
+    fun isWifiDefault_transportInfoIsNotWifi_butHasWifiTransport_true() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
+
+            val transportInfo =
+                VpnTransportInfo(
+                    /* type= */ 0,
+                    /* sessionId= */ "sessionId",
+                )
+            val networkCapabilities =
+                mock<NetworkCapabilities>().also {
+                    whenever(it.hasTransport(TRANSPORT_VPN)).thenReturn(true)
+                    whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+                    whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
+                    whenever(it.transportInfo).thenReturn(transportInfo)
+                }
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities)
+
+            assertThat(underTest.isWifiDefault.value).isTrue()
+
+            job.cancel()
+        }
 
     @Test
-    fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isWifiDefault_cellularVcnNetwork_isTrue() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
 
-        val capabilities = mock<NetworkCapabilities>().apply {
-            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-            whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+                }
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(underTest.isWifiDefault.value).isTrue()
+
+            job.cancel()
         }
 
-        getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
-
-        assertThat(latest is WifiNetworkModel.Active).isTrue()
-        val latestActive = latest as WifiNetworkModel.Active
-        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latestActive.ssid).isEqualTo(SSID)
-
-        job.cancel()
-    }
-
     @Test
-    fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun wifiNetwork_cellularAndWifiTransports_usesCellular_isTrue() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
 
-        val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
-            whenever(this.isPrimary).thenReturn(false)
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+                }
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(underTest.isWifiDefault.value).isTrue()
+
+            job.cancel()
         }
-        val capabilities = mock<NetworkCapabilities>().apply {
-            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-            whenever(this.transportInfo).thenReturn(VcnTransportInfo(wifiInfo))
-        }
-
-        getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
-
-        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-        job.cancel()
-    }
 
     @Test
-    fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isWifiDefault_cellularNotVcnNetwork_isFalse() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
 
-        val capabilities = mock<NetworkCapabilities>().apply {
-            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-            whenever(this.transportInfo).thenReturn(mock())
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(mock())
+                }
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(underTest.isWifiDefault.value).isFalse()
+
+            job.cancel()
         }
 
-        getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
-
-        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-        job.cancel()
-    }
-
     @Test
-    fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isWifiDefault_wifiNetworkLost_isFalse() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isWifiDefault.launchIn(this)
 
-        // Start with the original network
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            // First, add a network
+            getDefaultNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            assertThat(underTest.isWifiDefault.value).isTrue()
 
-        // WHEN we update to a new primary network
-        val newNetworkId = 456
-        val newNetwork = mock<Network>().apply {
-            whenever(this.getNetId()).thenReturn(newNetworkId)
+            // WHEN the network is lost
+            getDefaultNetworkCallback().onLost(NETWORK)
+
+            // THEN we update to false
+            assertThat(underTest.isWifiDefault.value).isFalse()
+
+            job.cancel()
         }
-        val newSsid = "CD"
-        val newWifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(newSsid)
-            whenever(this.isPrimary).thenReturn(true)
-        }
-
-        getNetworkCallback().onCapabilitiesChanged(
-            newNetwork, createWifiNetworkCapabilities(newWifiInfo)
-        )
-
-        // THEN we use the new network
-        assertThat(latest is WifiNetworkModel.Active).isTrue()
-        val latestActive = latest as WifiNetworkModel.Active
-        assertThat(latestActive.networkId).isEqualTo(newNetworkId)
-        assertThat(latestActive.ssid).isEqualTo(newSsid)
-
-        job.cancel()
-    }
 
     @Test
-    fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun wifiNetwork_initiallyGetsDefault() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        // Start with the original network
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT)
 
-        // WHEN we notify of a new but non-primary network
-        val newNetworkId = 456
-        val newNetwork = mock<Network>().apply {
-            whenever(this.getNetId()).thenReturn(newNetworkId)
+            job.cancel()
         }
-        val newSsid = "EF"
-        val newWifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(newSsid)
-            whenever(this.isPrimary).thenReturn(false)
-        }
-
-        getNetworkCallback().onCapabilitiesChanged(
-            newNetwork, createWifiNetworkCapabilities(newWifiInfo)
-        )
-
-        // THEN we still use the original network
-        assertThat(latest is WifiNetworkModel.Active).isTrue()
-        val latestActive = latest as WifiNetworkModel.Active
-        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latestActive.ssid).isEqualTo(SSID)
-
-        job.cancel()
-    }
 
     @Test
-    fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
-            whenever(this.isPrimary).thenReturn(true)
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(SSID)
+                    whenever(this.isPrimary).thenReturn(true)
+                }
+            val network = mock<Network>().apply { whenever(this.getNetId()).thenReturn(NETWORK_ID) }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(network, createWifiNetworkCapabilities(wifiInfo))
+
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestActive.ssid).isEqualTo(SSID)
+
+            job.cancel()
         }
 
-        // Start with the original network
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(wifiInfo, isValidated = true)
-        )
+    @Test
+    fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        // WHEN we keep the same network ID but change the SSID
-        val newSsid = "CD"
-        val newWifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(newSsid)
-            whenever(this.isPrimary).thenReturn(true)
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.isPrimary).thenReturn(true)
+                    whenever(this.isCarrierMerged).thenReturn(true)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+
+            job.cancel()
         }
 
-        getNetworkCallback().onCapabilitiesChanged(
-            NETWORK, createWifiNetworkCapabilities(newWifiInfo, isValidated = false)
-        )
-
-        // THEN we've updated to the new SSID
-        assertThat(latest is WifiNetworkModel.Active).isTrue()
-        val latestActive = latest as WifiNetworkModel.Active
-        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latestActive.ssid).isEqualTo(newSsid)
-        assertThat(latestActive.isValidated).isFalse()
-
-        job.cancel()
-    }
-
     @Test
-    fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand
-        getNetworkCallback().onLost(NETWORK)
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.isPrimary).thenReturn(true)
+                    whenever(this.isCarrierMerged).thenReturn(true)
+                    whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID)
+                }
 
-        // THEN there's no crash and we still have no network
-        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(wifiInfo),
+                )
 
-        job.cancel()
-    }
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
 
-    @Test
-    fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
-
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
-        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
-
-        // WHEN we lose our current network
-        getNetworkCallback().onLost(NETWORK)
-
-        // THEN we update to no network
-        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-        job.cancel()
-    }
-
-    @Test
-    fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
-
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
-        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
-
-        // WHEN we lose an unknown network
-        val unknownNetwork = mock<Network>().apply {
-            whenever(this.getNetId()).thenReturn(543)
+            job.cancel()
         }
-        getNetworkCallback().onLost(unknownNetwork)
-
-        // THEN we still have our previous network
-        assertThat(latest is WifiNetworkModel.Active).isTrue()
-        val latestActive = latest as WifiNetworkModel.Active
-        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latestActive.ssid).isEqualTo(SSID)
-
-        job.cancel()
-    }
 
     @Test
-    fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = runBlocking(IMMEDIATE) {
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+    fun wifiNetwork_isCarrierMerged_getsCorrectValues() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
-        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+            val rssi = -57
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.isPrimary).thenReturn(true)
+                    whenever(this.isCarrierMerged).thenReturn(true)
+                    whenever(this.rssi).thenReturn(rssi)
+                    whenever(this.subscriptionId).thenReturn(567)
+                }
 
-        // WHEN we update to a new network...
-        val newNetworkId = 89
-        val newNetwork = mock<Network>().apply {
-            whenever(this.getNetId()).thenReturn(newNetworkId)
+            whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2)
+            whenever(wifiManager.maxSignalLevel).thenReturn(5)
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(wifiInfo),
+                )
+
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+            val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged
+            assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567)
+            assertThat(latestCarrierMerged.level).isEqualTo(2)
+            // numberOfLevels = maxSignalLevel + 1
+            assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6)
+
+            job.cancel()
         }
-        getNetworkCallback().onCapabilitiesChanged(
-            newNetwork, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
-        )
-        // ...and lose the old network
-        getNetworkCallback().onLost(NETWORK)
 
-        // THEN we still have the new network
-        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId)
+    @Test
+    fun wifiNetwork_notValidated_networkNotValidated() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        job.cancel()
-    }
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(PRIMARY_WIFI_INFO, isValidated = false)
+                )
+
+            assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_validated_networkValidated() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(PRIMARY_WIFI_INFO, isValidated = true)
+                )
+
+            assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(SSID)
+                    whenever(this.isPrimary).thenReturn(false)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
+    /** Regression test for b/266628069. */
+    @Test
+    fun wifiNetwork_transportInfoIsNotWifi_flowHasNoNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val transportInfo =
+                VpnTransportInfo(
+                    /* type= */ 0,
+                    /* sessionId= */ "sessionId",
+                )
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(transportInfo))
+
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+                }
+
+            getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestActive.ssid).isEqualTo(SSID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(SSID)
+                    whenever(this.isPrimary).thenReturn(false)
+                }
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(VcnTransportInfo(wifiInfo))
+                }
+
+            getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(mock())
+                }
+
+            getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_cellularAndWifiTransports_usesCellular() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val capabilities =
+                mock<NetworkCapabilities>().apply {
+                    whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                    whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+                    whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+                }
+
+            getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestActive.ssid).isEqualTo(SSID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            // Start with the original network
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+            // WHEN we update to a new primary network
+            val newNetworkId = 456
+            val newNetwork =
+                mock<Network>().apply { whenever(this.getNetId()).thenReturn(newNetworkId) }
+            val newSsid = "CD"
+            val newWifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(newSsid)
+                    whenever(this.isPrimary).thenReturn(true)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(newNetwork, createWifiNetworkCapabilities(newWifiInfo))
+
+            // THEN we use the new network
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(newNetworkId)
+            assertThat(latestActive.ssid).isEqualTo(newSsid)
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            // Start with the original network
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+            // WHEN we notify of a new but non-primary network
+            val newNetworkId = 456
+            val newNetwork =
+                mock<Network>().apply { whenever(this.getNetId()).thenReturn(newNetworkId) }
+            val newSsid = "EF"
+            val newWifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(newSsid)
+                    whenever(this.isPrimary).thenReturn(false)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(newNetwork, createWifiNetworkCapabilities(newWifiInfo))
+
+            // THEN we still use the original network
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestActive.ssid).isEqualTo(SSID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_newNetworkCapabilities_flowHasNewData() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(SSID)
+                    whenever(this.isPrimary).thenReturn(true)
+                }
+
+            // Start with the original network
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(wifiInfo, isValidated = true)
+                )
+
+            // WHEN we keep the same network ID but change the SSID
+            val newSsid = "CD"
+            val newWifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.ssid).thenReturn(newSsid)
+                    whenever(this.isPrimary).thenReturn(true)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(newWifiInfo, isValidated = false)
+                )
+
+            // THEN we've updated to the new SSID
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestActive.ssid).isEqualTo(newSsid)
+            assertThat(latestActive.isValidated).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand
+            getNetworkCallback().onLost(NETWORK)
+
+            // THEN there's no crash and we still have no network
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+            // WHEN we lose our current network
+            getNetworkCallback().onLost(NETWORK)
+
+            // THEN we update to no network
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+            // WHEN we lose an unknown network
+            val unknownNetwork = mock<Network>().apply { whenever(this.getNetId()).thenReturn(543) }
+            getNetworkCallback().onLost(unknownNetwork)
+
+            // THEN we still have our previous network
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestActive.ssid).isEqualTo(SSID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+            // WHEN we update to a new network...
+            val newNetworkId = 89
+            val newNetwork =
+                mock<Network>().apply { whenever(this.getNetId()).thenReturn(newNetworkId) }
+            getNetworkCallback()
+                .onCapabilitiesChanged(newNetwork, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            // ...and lose the old network
+            getNetworkCallback().onLost(NETWORK)
+
+            // THEN we still have the new network
+            assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId)
+
+            job.cancel()
+        }
 
     /** Regression test for b/244173280. */
     @Test
-    fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() = runBlocking(IMMEDIATE) {
-        var latest1: WifiNetworkModel? = null
-        val job1 = underTest
-            .wifiNetwork
-            .onEach { latest1 = it }
-            .launchIn(this)
+    fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() =
+        runBlocking(IMMEDIATE) {
+            var latest1: WifiNetworkModel? = null
+            val job1 = underTest.wifiNetwork.onEach { latest1 = it }.launchIn(this)
 
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
 
-        assertThat(latest1 is WifiNetworkModel.Active).isTrue()
-        val latest1Active = latest1 as WifiNetworkModel.Active
-        assertThat(latest1Active.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latest1Active.ssid).isEqualTo(SSID)
+            assertThat(latest1 is WifiNetworkModel.Active).isTrue()
+            val latest1Active = latest1 as WifiNetworkModel.Active
+            assertThat(latest1Active.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latest1Active.ssid).isEqualTo(SSID)
 
-        // WHEN we add a second subscriber after having already emitted a value
-        var latest2: WifiNetworkModel? = null
-        val job2 = underTest
-            .wifiNetwork
-            .onEach { latest2 = it }
-            .launchIn(this)
+            // WHEN we add a second subscriber after having already emitted a value
+            var latest2: WifiNetworkModel? = null
+            val job2 = underTest.wifiNetwork.onEach { latest2 = it }.launchIn(this)
 
-        // THEN the second subscribe receives the already-emitted value
-        assertThat(latest2 is WifiNetworkModel.Active).isTrue()
-        val latest2Active = latest2 as WifiNetworkModel.Active
-        assertThat(latest2Active.networkId).isEqualTo(NETWORK_ID)
-        assertThat(latest2Active.ssid).isEqualTo(SSID)
+            // THEN the second subscribe receives the already-emitted value
+            assertThat(latest2 is WifiNetworkModel.Active).isTrue()
+            val latest2Active = latest2 as WifiNetworkModel.Active
+            assertThat(latest2Active.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latest2Active.ssid).isEqualTo(SSID)
 
-        job1.cancel()
-        job2.cancel()
-    }
+            job1.cancel()
+            job2.cancel()
+        }
 
     @Test
-    fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
-        var latest: DataActivityModel? = null
-        val job = underTest
-                .wifiActivity
-                .onEach { latest = it }
-                .launchIn(this)
+    fun wifiActivity_callbackGivesNone_activityFlowHasNone() =
+        runBlocking(IMMEDIATE) {
+            var latest: DataActivityModel? = null
+            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
 
-        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
+            getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
 
-        assertThat(latest).isEqualTo(
-            DataActivityModel(hasActivityIn = false, hasActivityOut = false)
-        )
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
-        var latest: DataActivityModel? = null
-        val job = underTest
-                .wifiActivity
-                .onEach { latest = it }
-                .launchIn(this)
+    fun wifiActivity_callbackGivesIn_activityFlowHasIn() =
+        runBlocking(IMMEDIATE) {
+            var latest: DataActivityModel? = null
+            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
 
-        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
+            getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
 
-        assertThat(latest).isEqualTo(
-            DataActivityModel(hasActivityIn = true, hasActivityOut = false)
-        )
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
-        var latest: DataActivityModel? = null
-        val job = underTest
-                .wifiActivity
-                .onEach { latest = it }
-                .launchIn(this)
+    fun wifiActivity_callbackGivesOut_activityFlowHasOut() =
+        runBlocking(IMMEDIATE) {
+            var latest: DataActivityModel? = null
+            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
 
-        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
+            getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
 
-        assertThat(latest).isEqualTo(
-            DataActivityModel(hasActivityIn = false, hasActivityOut = true)
-        )
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
-        var latest: DataActivityModel? = null
-        val job = underTest
-                .wifiActivity
-                .onEach { latest = it }
-                .launchIn(this)
+    fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() =
+        runBlocking(IMMEDIATE) {
+            var latest: DataActivityModel? = null
+            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
 
-        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
+            getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
 
-        assertThat(latest).isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
+            assertThat(latest)
+                .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     private fun createRepo(): WifiRepositoryImpl {
         return WifiRepositoryImpl(
@@ -809,26 +958,25 @@
     }
 
     private fun createWifiNetworkCapabilities(
-        wifiInfo: WifiInfo,
+        transportInfo: TransportInfo,
         isValidated: Boolean = true,
     ): NetworkCapabilities {
         return mock<NetworkCapabilities>().also {
             whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-            whenever(it.transportInfo).thenReturn(wifiInfo)
+            whenever(it.transportInfo).thenReturn(transportInfo)
             whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(isValidated)
         }
     }
 
     private companion object {
         const val NETWORK_ID = 45
-        val NETWORK = mock<Network>().apply {
-            whenever(this.getNetId()).thenReturn(NETWORK_ID)
-        }
+        val NETWORK = mock<Network>().apply { whenever(this.getNetId()).thenReturn(NETWORK_ID) }
         const val SSID = "AB"
-        val PRIMARY_WIFI_INFO: WifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
-            whenever(this.isPrimary).thenReturn(true)
-        }
+        val PRIMARY_WIFI_INFO: WifiInfo =
+            mock<WifiInfo>().apply {
+                whenever(this.ssid).thenReturn(SSID)
+                whenever(this.isPrimary).thenReturn(true)
+            }
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 01d59f9..fc2277b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -22,8 +22,8 @@
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -57,10 +57,7 @@
             wifiRepository.setWifiNetwork(WifiNetworkModel.Unavailable)
 
             var latest: String? = "default"
-            val job = underTest
-                .ssid
-                .onEach { latest = it }
-                .launchIn(this)
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
             assertThat(latest).isNull()
 
@@ -68,236 +65,223 @@
         }
 
     @Test
-    fun ssid_inactiveNetwork_outputsNull() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+    fun ssid_inactiveNetwork_outputsNull() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
 
-        var latest: String? = "default"
-        val job = underTest
-            .ssid
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: String? = "default"
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isNull()
+            assertThat(latest).isNull()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
+    fun ssid_carrierMergedNetwork_outputsNull() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1)
+            )
 
-        var latest: String? = "default"
-        val job = underTest
-            .ssid
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: String? = "default"
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isNull()
+            assertThat(latest).isNull()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun ssid_isPasspointAccessPoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
-            networkId = 1,
-            level = 1,
-            isPasspointAccessPoint = true,
-            passpointProviderFriendlyName = "friendly",
-        ))
+    fun ssid_isPasspointAccessPoint_outputsPasspointName() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 1,
+                    isPasspointAccessPoint = true,
+                    passpointProviderFriendlyName = "friendly",
+                )
+            )
 
-        var latest: String? = null
-        val job = underTest
-            .ssid
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: String? = null
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo("friendly")
+            assertThat(latest).isEqualTo("friendly")
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
-            networkId = 1,
-            level = 1,
-            isOnlineSignUpForPasspointAccessPoint = true,
-            passpointProviderFriendlyName = "friendly",
-        ))
+    fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 1,
+                    isOnlineSignUpForPasspointAccessPoint = true,
+                    passpointProviderFriendlyName = "friendly",
+                )
+            )
 
-        var latest: String? = null
-        val job = underTest
-            .ssid
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: String? = null
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo("friendly")
+            assertThat(latest).isEqualTo("friendly")
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun ssid_unknownSsid_outputsNull() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
-            networkId = 1,
-            level = 1,
-            ssid = WifiManager.UNKNOWN_SSID,
-        ))
+    fun ssid_unknownSsid_outputsNull() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 1,
+                    ssid = WifiManager.UNKNOWN_SSID,
+                )
+            )
 
-        var latest: String? = "default"
-        val job = underTest
-            .ssid
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: String? = "default"
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isNull()
+            assertThat(latest).isNull()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun ssid_validSsid_outputsSsid() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
-            networkId = 1,
-            level = 1,
-            ssid = "MyAwesomeWifiNetwork",
-        ))
+    fun ssid_validSsid_outputsSsid() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 1,
+                    ssid = "MyAwesomeWifiNetwork",
+                )
+            )
 
-        var latest: String? = null
-        val job = underTest
-            .ssid
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: String? = null
+            val job = underTest.ssid.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo("MyAwesomeWifiNetwork")
+            assertThat(latest).isEqualTo("MyAwesomeWifiNetwork")
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun isEnabled_matchesRepoIsEnabled() = runBlocking(IMMEDIATE) {
-        var latest: Boolean? = null
-        val job = underTest
-            .isEnabled
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isEnabled_matchesRepoIsEnabled() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isEnabled.onEach { latest = it }.launchIn(this)
 
-        wifiRepository.setIsWifiEnabled(true)
-        yield()
-        assertThat(latest).isTrue()
+            wifiRepository.setIsWifiEnabled(true)
+            yield()
+            assertThat(latest).isTrue()
 
-        wifiRepository.setIsWifiEnabled(false)
-        yield()
-        assertThat(latest).isFalse()
+            wifiRepository.setIsWifiEnabled(false)
+            yield()
+            assertThat(latest).isFalse()
 
-        wifiRepository.setIsWifiEnabled(true)
-        yield()
-        assertThat(latest).isTrue()
+            wifiRepository.setIsWifiEnabled(true)
+            yield()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) {
-        var latest: Boolean? = null
-        val job = underTest
-            .isDefault
-            .onEach { latest = it }
-            .launchIn(this)
+    fun isDefault_matchesRepoIsDefault() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDefault.onEach { latest = it }.launchIn(this)
 
-        wifiRepository.setIsWifiDefault(true)
-        yield()
-        assertThat(latest).isTrue()
+            wifiRepository.setIsWifiDefault(true)
+            yield()
+            assertThat(latest).isTrue()
 
-        wifiRepository.setIsWifiDefault(false)
-        yield()
-        assertThat(latest).isFalse()
+            wifiRepository.setIsWifiDefault(false)
+            yield()
+            assertThat(latest).isFalse()
 
-        wifiRepository.setIsWifiDefault(true)
-        yield()
-        assertThat(latest).isTrue()
+            wifiRepository.setIsWifiDefault(true)
+            yield()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) {
-        val wifiNetwork = WifiNetworkModel.Active(
-            networkId = 45,
-            isValidated = true,
-            level = 3,
-            ssid = "AB",
-            passpointProviderFriendlyName = "friendly"
-        )
-        wifiRepository.setWifiNetwork(wifiNetwork)
+    fun wifiNetwork_matchesRepoWifiNetwork() =
+        runBlocking(IMMEDIATE) {
+            val wifiNetwork =
+                WifiNetworkModel.Active(
+                    networkId = 45,
+                    isValidated = true,
+                    level = 3,
+                    ssid = "AB",
+                    passpointProviderFriendlyName = "friendly"
+                )
+            wifiRepository.setWifiNetwork(wifiNetwork)
 
-        var latest: WifiNetworkModel? = null
-        val job = underTest
-            .wifiNetwork
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo(wifiNetwork)
+            assertThat(latest).isEqualTo(wifiNetwork)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activity_matchesRepoWifiActivity() = runBlocking(IMMEDIATE) {
-        var latest: DataActivityModel? = null
-        val job = underTest
-            .activity
-            .onEach { latest = it }
-            .launchIn(this)
+    fun activity_matchesRepoWifiActivity() =
+        runBlocking(IMMEDIATE) {
+            var latest: DataActivityModel? = null
+            val job = underTest.activity.onEach { latest = it }.launchIn(this)
 
-        val activity1 = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity1)
-        yield()
-        assertThat(latest).isEqualTo(activity1)
+            val activity1 = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity1)
+            yield()
+            assertThat(latest).isEqualTo(activity1)
 
-        val activity2 = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
-        wifiRepository.setWifiActivity(activity2)
-        yield()
-        assertThat(latest).isEqualTo(activity2)
+            val activity2 = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity2)
+            yield()
+            assertThat(latest).isEqualTo(activity2)
 
-        val activity3 = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
-        wifiRepository.setWifiActivity(activity3)
-        yield()
-        assertThat(latest).isEqualTo(activity3)
+            val activity3 = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity3)
+            yield()
+            assertThat(latest).isEqualTo(activity3)
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun isForceHidden_repoHasWifiHidden_outputsTrue() = runBlocking(IMMEDIATE) {
-        connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
+    fun isForceHidden_repoHasWifiHidden_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
 
-        var latest: Boolean? = null
-        val job = underTest
-            .isForceHidden
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() = runBlocking(IMMEDIATE) {
-        connectivityRepository.setForceHiddenIcons(setOf())
+    fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
 
-        var latest: Boolean? = null
-        val job = underTest
-            .isForceHidden
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isFalse()
+            assertThat(latest).isFalse()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 }
 
 private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt
similarity index 68%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt
index 30ac8d4..ab4e93c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.wifi.data.model
+package com.android.systemui.statusbar.pipeline.wifi.shared.model
 
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableRowLogger
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 
@@ -44,9 +45,53 @@
         WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
     }
 
+    @Test(expected = IllegalArgumentException::class)
+    fun carrierMerged_invalidSubId_exceptionThrown() {
+        WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1)
+    }
+
     // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
 
     @Test
+    fun logDiffs_carrierMergedToInactive_resetsAllFields() {
+        val logger = TestLogger()
+        val prevVal =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 5,
+                subscriptionId = 3,
+                level = 1,
+            )
+
+        WifiNetworkModel.Inactive.logDiffs(prevVal, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+    }
+
+    @Test
+    fun logDiffs_inactiveToCarrierMerged_logsAllFields() {
+        val logger = TestLogger()
+        val carrierMerged =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 6,
+                subscriptionId = 3,
+                level = 2,
+            )
+
+        carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+        assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+    }
+
+    @Test
     fun logDiffs_inactiveToActive_logsAllActiveFields() {
         val logger = TestLogger()
         val activeNetwork =
@@ -95,8 +140,14 @@
                 level = 3,
                 ssid = "Test SSID"
             )
+        val prevVal =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 5,
+                subscriptionId = 3,
+                level = 1,
+            )
 
-        activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger)
+        activeNetwork.logDiffs(prevVal, logger)
 
         assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
         assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
@@ -105,7 +156,7 @@
         assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
     }
     @Test
-    fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() {
+    fun logDiffs_activeToCarrierMerged_logsAllFields() {
         val logger = TestLogger()
         val activeNetwork =
             WifiNetworkModel.Active(
@@ -114,13 +165,20 @@
                 level = 3,
                 ssid = "Test SSID"
             )
+        val carrierMerged =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 6,
+                subscriptionId = 3,
+                level = 2,
+            )
 
-        WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger)
+        carrierMerged.logDiffs(prevVal = activeNetwork, logger)
 
         assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
-        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
-        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
-        assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+        assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
         assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index b8ace2f..64810d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -36,13 +36,12 @@
 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.shared.ConnectivityConstants
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import com.android.systemui.util.mockito.whenever
@@ -62,16 +61,10 @@
 
     private lateinit var testableLooper: TestableLooper
 
-    @Mock
-    private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
-    @Mock
-    private lateinit var logger: ConnectivityPipelineLogger
-    @Mock
-    private lateinit var tableLogBuffer: TableLogBuffer
-    @Mock
-    private lateinit var connectivityConstants: ConnectivityConstants
-    @Mock
-    private lateinit var wifiConstants: WifiConstants
+    @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+    @Mock private lateinit var connectivityConstants: ConnectivityConstants
+    @Mock private lateinit var wifiConstants: WifiConstants
     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
@@ -91,25 +84,27 @@
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractorImpl(connectivityRepository, wifiRepository)
         scope = CoroutineScope(Dispatchers.Unconfined)
-        airplaneModeViewModel = AirplaneModeViewModelImpl(
-            AirplaneModeInteractor(
-                airplaneModeRepository,
-                connectivityRepository,
-            ),
-            logger,
-            scope,
-        )
-        viewModel = WifiViewModel(
-            airplaneModeViewModel,
-            connectivityConstants,
-            context,
-            logger,
-            tableLogBuffer,
-            interactor,
-            scope,
-            statusBarPipelineFlags,
-            wifiConstants,
-        ).home
+        airplaneModeViewModel =
+            AirplaneModeViewModelImpl(
+                AirplaneModeInteractor(
+                    airplaneModeRepository,
+                    connectivityRepository,
+                ),
+                tableLogBuffer,
+                scope,
+            )
+        viewModel =
+            WifiViewModel(
+                    airplaneModeViewModel,
+                    connectivityConstants,
+                    context,
+                    tableLogBuffer,
+                    interactor,
+                    scope,
+                    statusBarPipelineFlags,
+                    wifiConstants,
+                )
+                .home
     }
 
     // Note: The following tests are more like integration tests, since they stand up a full
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 726e813..12b1664 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -33,14 +33,13 @@
 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.shared.ConnectivityConstants
-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.FakeConnectivityRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET
 import com.google.common.truth.Truth.assertThat
@@ -68,7 +67,6 @@
     private lateinit var underTest: WifiViewModel
 
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
@@ -94,7 +92,7 @@
                     airplaneModeRepository,
                     connectivityRepository,
                 ),
-                logger,
+                tableLogBuffer,
                 scope,
             )
     }
@@ -125,7 +123,6 @@
                     airplaneModeViewModel,
                     connectivityConstants,
                     context,
-                    logger,
                     tableLogBuffer,
                     interactor,
                     scope,
@@ -206,7 +203,8 @@
                 // Enabled = false => no networks shown
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.CarrierMerged,
+                    network =
+                        WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
                     expected = null,
                 ),
                 TestCase(
@@ -228,7 +226,8 @@
                 // forceHidden = true => no networks shown
                 TestCase(
                     forceHidden = true,
-                    network = WifiNetworkModel.CarrierMerged,
+                    network =
+                        WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
                     expected = null,
                 ),
                 TestCase(
@@ -369,7 +368,8 @@
 
                 // network = CarrierMerged => not shown
                 TestCase(
-                    network = WifiNetworkModel.CarrierMerged,
+                    network =
+                        WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
                     expected = null,
                 ),
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index e5cfec9..7a62cb8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -25,15 +25,14 @@
 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.shared.ConnectivityConstants
-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.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -59,7 +58,6 @@
     private lateinit var underTest: WifiViewModel
 
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
@@ -79,14 +77,15 @@
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractorImpl(connectivityRepository, wifiRepository)
         scope = CoroutineScope(IMMEDIATE)
-        airplaneModeViewModel = AirplaneModeViewModelImpl(
-            AirplaneModeInteractor(
-                airplaneModeRepository,
-                connectivityRepository,
-            ),
-            logger,
-            scope,
-        )
+        airplaneModeViewModel =
+            AirplaneModeViewModelImpl(
+                AirplaneModeInteractor(
+                    airplaneModeRepository,
+                    connectivityRepository,
+                ),
+                tableLogBuffer,
+                scope,
+            )
 
         createAndSetViewModel()
     }
@@ -104,451 +103,385 @@
     // instances. There are also some tests that verify all 3 instances received the same data.
 
     @Test
-    fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
-        var latestHome: WifiIcon? = null
-        val jobHome = underTest
-            .home
-            .wifiIcon
-            .onEach { latestHome = it }
-            .launchIn(this)
+    fun wifiIcon_allLocationViewModelsReceiveSameData() =
+        runBlocking(IMMEDIATE) {
+            var latestHome: WifiIcon? = null
+            val jobHome = underTest.home.wifiIcon.onEach { latestHome = it }.launchIn(this)
 
-        var latestKeyguard: WifiIcon? = null
-        val jobKeyguard = underTest
-            .keyguard
-            .wifiIcon
-            .onEach { latestKeyguard = it }
-            .launchIn(this)
+            var latestKeyguard: WifiIcon? = null
+            val jobKeyguard =
+                underTest.keyguard.wifiIcon.onEach { latestKeyguard = it }.launchIn(this)
 
-        var latestQs: WifiIcon? = null
-        val jobQs = underTest
-            .qs
-            .wifiIcon
-            .onEach { latestQs = it }
-            .launchIn(this)
+            var latestQs: WifiIcon? = null
+            val jobQs = underTest.qs.wifiIcon.onEach { latestQs = it }.launchIn(this)
 
-        wifiRepository.setWifiNetwork(
-            WifiNetworkModel.Active(
-                NETWORK_ID,
-                isValidated = true,
-                level = 1
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1)
             )
-        )
-        yield()
+            yield()
 
-        assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java)
-        assertThat(latestHome).isEqualTo(latestKeyguard)
-        assertThat(latestKeyguard).isEqualTo(latestQs)
+            assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java)
+            assertThat(latestHome).isEqualTo(latestKeyguard)
+            assertThat(latestKeyguard).isEqualTo(latestQs)
 
-        jobHome.cancel()
-        jobKeyguard.cancel()
-        jobQs.cancel()
-    }
+            jobHome.cancel()
+            jobKeyguard.cancel()
+            jobQs.cancel()
+        }
 
     @Test
-    fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activity_showActivityConfigFalse_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var activityIn: Boolean? = null
-        val activityInJob = underTest
-            .home
-            .isActivityInViewVisible
-            .onEach { activityIn = it }
-            .launchIn(this)
+            var activityIn: Boolean? = null
+            val activityInJob =
+                underTest.home.isActivityInViewVisible.onEach { activityIn = it }.launchIn(this)
 
-        var activityOut: Boolean? = null
-        val activityOutJob = underTest
-            .home
-            .isActivityOutViewVisible
-            .onEach { activityOut = it }
-            .launchIn(this)
+            var activityOut: Boolean? = null
+            val activityOutJob =
+                underTest.home.isActivityOutViewVisible.onEach { activityOut = it }.launchIn(this)
 
-        var activityContainer: Boolean? = null
-        val activityContainerJob = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { activityContainer = it }
-            .launchIn(this)
+            var activityContainer: Boolean? = null
+            val activityContainerJob =
+                underTest.home.isActivityContainerVisible
+                    .onEach { activityContainer = it }
+                    .launchIn(this)
 
-        // Verify that on launch, we receive false.
-        assertThat(activityIn).isFalse()
-        assertThat(activityOut).isFalse()
-        assertThat(activityContainer).isFalse()
+            // Verify that on launch, we receive false.
+            assertThat(activityIn).isFalse()
+            assertThat(activityOut).isFalse()
+            assertThat(activityContainer).isFalse()
 
-        activityInJob.cancel()
-        activityOutJob.cancel()
-        activityContainerJob.cancel()
-    }
+            activityInJob.cancel()
+            activityOutJob.cancel()
+            activityContainerJob.cancel()
+        }
 
     @Test
-    fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activity_showActivityConfigFalse_noUpdatesReceived() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var activityIn: Boolean? = null
-        val activityInJob = underTest
-            .home
-            .isActivityInViewVisible
-            .onEach { activityIn = it }
-            .launchIn(this)
+            var activityIn: Boolean? = null
+            val activityInJob =
+                underTest.home.isActivityInViewVisible.onEach { activityIn = it }.launchIn(this)
 
-        var activityOut: Boolean? = null
-        val activityOutJob = underTest
-            .home
-            .isActivityOutViewVisible
-            .onEach { activityOut = it }
-            .launchIn(this)
+            var activityOut: Boolean? = null
+            val activityOutJob =
+                underTest.home.isActivityOutViewVisible.onEach { activityOut = it }.launchIn(this)
 
-        var activityContainer: Boolean? = null
-        val activityContainerJob = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { activityContainer = it }
-            .launchIn(this)
+            var activityContainer: Boolean? = null
+            val activityContainerJob =
+                underTest.home.isActivityContainerVisible
+                    .onEach { activityContainer = it }
+                    .launchIn(this)
 
-        // WHEN we update the repo to have activity
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            // WHEN we update the repo to have activity
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        // THEN we didn't update to the new activity (because our config is false)
-        assertThat(activityIn).isFalse()
-        assertThat(activityOut).isFalse()
-        assertThat(activityContainer).isFalse()
+            // THEN we didn't update to the new activity (because our config is false)
+            assertThat(activityIn).isFalse()
+            assertThat(activityOut).isFalse()
+            assertThat(activityContainer).isFalse()
 
-        activityInJob.cancel()
-        activityOutJob.cancel()
-        activityContainerJob.cancel()
-    }
+            activityInJob.cancel()
+            activityOutJob.cancel()
+            activityContainerJob.cancel()
+        }
 
     @Test
-    fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
+    fun activity_nullSsid_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
 
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null, level = 1))
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(NETWORK_ID, ssid = null, level = 1)
+            )
 
-        var activityIn: Boolean? = null
-        val activityInJob = underTest
-            .home
-            .isActivityInViewVisible
-            .onEach { activityIn = it }
-            .launchIn(this)
+            var activityIn: Boolean? = null
+            val activityInJob =
+                underTest.home.isActivityInViewVisible.onEach { activityIn = it }.launchIn(this)
 
-        var activityOut: Boolean? = null
-        val activityOutJob = underTest
-            .home
-            .isActivityOutViewVisible
-            .onEach { activityOut = it }
-            .launchIn(this)
+            var activityOut: Boolean? = null
+            val activityOutJob =
+                underTest.home.isActivityOutViewVisible.onEach { activityOut = it }.launchIn(this)
 
-        var activityContainer: Boolean? = null
-        val activityContainerJob = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { activityContainer = it }
-            .launchIn(this)
+            var activityContainer: Boolean? = null
+            val activityContainerJob =
+                underTest.home.isActivityContainerVisible
+                    .onEach { activityContainer = it }
+                    .launchIn(this)
 
-        // WHEN we update the repo to have activity
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            // WHEN we update the repo to have activity
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        // THEN we still output false because our network's SSID is null
-        assertThat(activityIn).isFalse()
-        assertThat(activityOut).isFalse()
-        assertThat(activityContainer).isFalse()
+            // THEN we still output false because our network's SSID is null
+            assertThat(activityIn).isFalse()
+            assertThat(activityOut).isFalse()
+            assertThat(activityContainer).isFalse()
 
-        activityInJob.cancel()
-        activityOutJob.cancel()
-        activityContainerJob.cancel()
-    }
+            activityInJob.cancel()
+            activityOutJob.cancel()
+            activityContainerJob.cancel()
+        }
 
     @Test
-    fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activity_allLocationViewModelsReceiveSameData() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latestHome: Boolean? = null
-        val jobHome = underTest
-            .home
-            .isActivityInViewVisible
-            .onEach { latestHome = it }
-            .launchIn(this)
+            var latestHome: Boolean? = null
+            val jobHome =
+                underTest.home.isActivityInViewVisible.onEach { latestHome = it }.launchIn(this)
 
-        var latestKeyguard: Boolean? = null
-        val jobKeyguard = underTest
-            .keyguard
-            .isActivityInViewVisible
-            .onEach { latestKeyguard = it }
-            .launchIn(this)
+            var latestKeyguard: Boolean? = null
+            val jobKeyguard =
+                underTest.keyguard.isActivityInViewVisible
+                    .onEach { latestKeyguard = it }
+                    .launchIn(this)
 
-        var latestQs: Boolean? = null
-        val jobQs = underTest
-            .qs
-            .isActivityInViewVisible
-            .onEach { latestQs = it }
-            .launchIn(this)
+            var latestQs: Boolean? = null
+            val jobQs = underTest.qs.isActivityInViewVisible.onEach { latestQs = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latestHome).isTrue()
-        assertThat(latestKeyguard).isTrue()
-        assertThat(latestQs).isTrue()
+            assertThat(latestHome).isTrue()
+            assertThat(latestKeyguard).isTrue()
+            assertThat(latestQs).isTrue()
 
-        jobHome.cancel()
-        jobKeyguard.cancel()
-        jobQs.cancel()
-    }
+            jobHome.cancel()
+            jobKeyguard.cancel()
+            jobQs.cancel()
+        }
 
     @Test
-    fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityIn_hasActivityInTrue_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityInViewVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job = underTest.home.isActivityInViewVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityIn_hasActivityInFalse_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityInViewVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job = underTest.home.isActivityInViewVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isFalse()
+            assertThat(latest).isFalse()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityOut_hasActivityOutTrue_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityOutViewVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job = underTest.home.isActivityOutViewVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityOut_hasActivityOutFalse_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityOutViewVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job = underTest.home.isActivityOutViewVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isFalse()
+            assertThat(latest).isFalse()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityContainer_hasActivityInTrue_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job =
+                underTest.home.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityContainer_hasActivityOutTrue_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job =
+                underTest.home.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityContainer_inAndOutTrue_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job =
+                underTest.home.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
-        whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
-        createAndSetViewModel()
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun activityContainer_inAndOutFalse_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-        var latest: Boolean? = null
-        val job = underTest
-            .home
-            .isActivityContainerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+            var latest: Boolean? = null
+            val job =
+                underTest.home.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
 
-        val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
-        wifiRepository.setWifiActivity(activity)
-        yield()
+            val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity)
+            yield()
 
-        assertThat(latest).isFalse()
+            assertThat(latest).isFalse()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) {
-        var latest: Boolean? = null
-        val job = underTest
-            .qs
-            .isAirplaneSpacerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+    fun airplaneSpacer_notAirplaneMode_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.qs.isAirplaneSpacerVisible.onEach { latest = it }.launchIn(this)
 
-        airplaneModeRepository.setIsAirplaneMode(false)
-        yield()
+            airplaneModeRepository.setIsAirplaneMode(false)
+            yield()
 
-        assertThat(latest).isFalse()
+            assertThat(latest).isFalse()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) {
-        var latest: Boolean? = null
-        val job = underTest
-            .qs
-            .isAirplaneSpacerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+    fun airplaneSpacer_airplaneForceHidden_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.qs.isAirplaneSpacerVisible.onEach { latest = it }.launchIn(this)
 
-        airplaneModeRepository.setIsAirplaneMode(true)
-        connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
-        yield()
+            airplaneModeRepository.setIsAirplaneMode(true)
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+            yield()
 
-        assertThat(latest).isFalse()
+            assertThat(latest).isFalse()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     @Test
-    fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) {
-        var latest: Boolean? = null
-        val job = underTest
-            .qs
-            .isAirplaneSpacerVisible
-            .onEach { latest = it }
-            .launchIn(this)
+    fun airplaneSpacer_airplaneIconVisible_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.qs.isAirplaneSpacerVisible.onEach { latest = it }.launchIn(this)
 
-        airplaneModeRepository.setIsAirplaneMode(true)
-        yield()
+            airplaneModeRepository.setIsAirplaneMode(true)
+            yield()
 
-        assertThat(latest).isTrue()
+            assertThat(latest).isTrue()
 
-        job.cancel()
-    }
+            job.cancel()
+        }
 
     private fun createAndSetViewModel() {
         // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow
         // creations rely on certain config values that we mock out in individual tests. This method
         // allows tests to create the view model only after those configs are correctly set up.
-        underTest = WifiViewModel(
-            airplaneModeViewModel,
-            connectivityConstants,
-            context,
-            logger,
-            tableLogBuffer,
-            interactor,
-            scope,
-            statusBarPipelineFlags,
-            wifiConstants,
-        )
+        underTest =
+            WifiViewModel(
+                airplaneModeViewModel,
+                connectivityConstants,
+                context,
+                tableLogBuffer,
+                interactor,
+                scope,
+                statusBarPipelineFlags,
+                wifiConstants,
+            )
     }
 
     companion object {
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/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 4b32ee2..0cca7b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -390,19 +390,27 @@
         bindController(view, row.getEntry());
         view.setVisibility(View.GONE);
 
-        View crossFadeView = new View(mContext);
+        View fadeOutView = new View(mContext);
+        fadeOutView.setId(com.android.internal.R.id.actions_container_layout);
+
+        FrameLayout parent = new FrameLayout(mContext);
+        parent.addView(view);
+        parent.addView(fadeOutView);
 
         // Start focus animation
-        view.focusAnimated(crossFadeView);
-
+        view.focusAnimated();
         assertTrue(view.isAnimatingAppearance());
 
-        // fast forward to end of animation
-        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+        // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f
+        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1);
+        assertEquals(0f, fadeOutView.getAlpha());
 
-        // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind
+        // fast forward to end of animation
+        mAnimatorTestRule.advanceTimeBy(1);
+
+        // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind
         // RemoteInputView)
-        assertEquals(1f, crossFadeView.getAlpha());
+        assertEquals(1f, fadeOutView.getAlpha());
         assertFalse(view.isAnimatingAppearance());
         assertEquals(View.VISIBLE, view.getVisibility());
         assertEquals(1f, view.getAlpha());
@@ -415,20 +423,27 @@
                 mDependency,
                 TestableLooper.get(this));
         ExpandableNotificationRow row = helper.createRow();
-        FrameLayout remoteInputViewParent = new FrameLayout(mContext);
         RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
-        remoteInputViewParent.addView(view);
         bindController(view, row.getEntry());
 
+        View fadeInView = new View(mContext);
+        fadeInView.setId(com.android.internal.R.id.actions_container_layout);
+
+        FrameLayout parent = new FrameLayout(mContext);
+        parent.addView(view);
+        parent.addView(fadeInView);
+
         // Start defocus animation
-        view.onDefocus(true, false);
+        view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */);
         assertEquals(View.VISIBLE, view.getVisibility());
+        assertEquals(0f, fadeInView.getAlpha());
 
         // fast forward to end of animation
         mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
 
         // assert that RemoteInputView is no longer visible
         assertEquals(View.GONE, view.getVisibility());
+        assertEquals(1f, fadeInView.getAlpha());
     }
 
     // NOTE: because we're refactoring the RemoteInputView and moving logic into the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
index 67733e9..7e01088 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
@@ -11,15 +11,15 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
-package com.android.systemui.keyguard.shared.model
 
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.stylus
 
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
+import android.hardware.BatteryState
+
+class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
+    override fun getCapacity() = capacity
+    override fun getStatus() = 0
+    override fun isPresent() = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt
deleted file mode 100644
index 8dd088f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.stylus
-
-import android.content.Context
-import android.hardware.BatteryState
-import android.hardware.input.InputManager
-import android.os.Handler
-import android.testing.AndroidTestingRunner
-import android.view.InputDevice
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-@Ignore("TODO(b/20579491): unignore on main")
-class StylusFirstUsageListenerTest : SysuiTestCase() {
-    @Mock lateinit var context: Context
-    @Mock lateinit var inputManager: InputManager
-    @Mock lateinit var stylusManager: StylusManager
-    @Mock lateinit var featureFlags: FeatureFlags
-    @Mock lateinit var internalStylusDevice: InputDevice
-    @Mock lateinit var otherDevice: InputDevice
-    @Mock lateinit var externalStylusDevice: InputDevice
-    @Mock lateinit var batteryState: BatteryState
-    @Mock lateinit var handler: Handler
-
-    private lateinit var stylusListener: StylusFirstUsageListener
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
-        whenever(inputManager.isStylusEverUsed(context)).thenReturn(false)
-
-        stylusListener =
-            StylusFirstUsageListener(
-                context,
-                inputManager,
-                stylusManager,
-                featureFlags,
-                EXECUTOR,
-                handler
-            )
-        stylusListener.hasStarted = false
-
-        whenever(handler.post(any())).thenAnswer {
-            (it.arguments[0] as Runnable).run()
-            true
-        }
-
-        whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
-        whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
-        whenever(internalStylusDevice.isExternal).thenReturn(false)
-        whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
-        whenever(externalStylusDevice.isExternal).thenReturn(true)
-
-        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
-        whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
-        whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID))
-            .thenReturn(internalStylusDevice)
-        whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID))
-            .thenReturn(externalStylusDevice)
-    }
-
-    @Test
-    fun start_flagDisabled_doesNotRegister() {
-        whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false)
-
-        stylusListener.start()
-
-        verify(stylusManager, never()).registerCallback(any())
-        verify(inputManager, never()).setStylusEverUsed(context, true)
-    }
-
-    @Test
-    fun start_toggleHasStarted() {
-        stylusListener.start()
-
-        assert(stylusListener.hasStarted)
-    }
-
-    @Test
-    fun start_hasStarted_doesNotRegister() {
-        stylusListener.hasStarted = true
-
-        stylusListener.start()
-
-        verify(stylusManager, never()).registerCallback(any())
-    }
-
-    @Test
-    fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
-        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID))
-
-        stylusListener.start()
-
-        verify(stylusManager, never()).registerCallback(any())
-        verify(inputManager, never()).setStylusEverUsed(context, true)
-    }
-
-    @Test
-    fun start_stylusEverUsed_doesNotRegister() {
-        whenever(inputManager.inputDeviceIds)
-            .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
-        whenever(inputManager.isStylusEverUsed(context)).thenReturn(true)
-
-        stylusListener.start()
-
-        verify(stylusManager, never()).registerCallback(any())
-        verify(inputManager, never()).setStylusEverUsed(context, true)
-    }
-
-    @Test
-    fun start_hostDeviceSupportsStylus_registersListener() {
-        whenever(inputManager.inputDeviceIds)
-            .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
-
-        stylusListener.start()
-
-        verify(stylusManager).registerCallback(any())
-        verify(inputManager, never()).setStylusEverUsed(context, true)
-    }
-
-    @Test
-    fun onStylusAdded_hasNotStarted_doesNotRegisterListener() {
-        stylusListener.hasStarted = false
-
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
-        verifyZeroInteractions(inputManager)
-    }
-
-    @Test
-    fun onStylusAdded_internalStylus_registersListener() {
-        stylusListener.hasStarted = true
-
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
-        verify(inputManager, times(1))
-            .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener)
-    }
-
-    @Test
-    fun onStylusAdded_externalStylus_doesNotRegisterListener() {
-        stylusListener.hasStarted = true
-
-        stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID)
-
-        verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
-    }
-
-    @Test
-    fun onStylusAdded_otherDevice_doesNotRegisterListener() {
-        stylusListener.onStylusAdded(OTHER_DEVICE_ID)
-
-        verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
-    }
-
-    @Test
-    fun onStylusRemoved_registeredDevice_unregistersListener() {
-        stylusListener.hasStarted = true
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
-        stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
-
-        verify(inputManager, times(1))
-            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
-    }
-
-    @Test
-    fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() {
-        stylusListener.hasStarted = false
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
-        stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
-
-        verifyZeroInteractions(inputManager)
-    }
-
-    @Test
-    fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() {
-        stylusListener.hasStarted = true
-
-        stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
-
-        verifyNoMoreInteractions(inputManager)
-    }
-
-    @Test
-    fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() {
-        stylusListener.hasStarted = true
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
-        stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
-
-        verify(inputManager).setStylusEverUsed(context, true)
-        verify(inputManager, times(1))
-            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
-        verify(stylusManager).unregisterCallback(stylusListener)
-    }
-
-    @Test
-    fun onStylusBluetoothConnected_hasNotStarted_doesNoting() {
-        stylusListener.hasStarted = false
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
-        stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
-
-        verifyZeroInteractions(inputManager)
-        verifyZeroInteractions(stylusManager)
-    }
-
-    @Test
-    fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() {
-        stylusListener.hasStarted = true
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-        whenever(batteryState.isPresent).thenReturn(true)
-
-        stylusListener.onBatteryStateChanged(0, 1, batteryState)
-
-        verify(inputManager).setStylusEverUsed(context, true)
-        verify(inputManager, times(1))
-            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
-        verify(stylusManager).unregisterCallback(stylusListener)
-    }
-
-    @Test
-    fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() {
-        stylusListener.hasStarted = true
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-        whenever(batteryState.isPresent).thenReturn(false)
-
-        stylusListener.onBatteryStateChanged(0, 1, batteryState)
-
-        verifyZeroInteractions(stylusManager)
-        verify(inputManager, never())
-            .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
-    }
-
-    @Test
-    fun onBatteryStateChanged_hasNotStarted_doesNothing() {
-        stylusListener.hasStarted = false
-        stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-        whenever(batteryState.isPresent).thenReturn(false)
-
-        stylusListener.onBatteryStateChanged(0, 1, batteryState)
-
-        verifyZeroInteractions(inputManager)
-        verifyZeroInteractions(stylusManager)
-    }
-
-    companion object {
-        private const val OTHER_DEVICE_ID = 0
-        private const val INTERNAL_STYLUS_DEVICE_ID = 1
-        private const val EXTERNAL_STYLUS_DEVICE_ID = 2
-        private val EXECUTOR = FakeExecutor(FakeSystemClock())
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
index 984de5b..6d6e40a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
@@ -17,12 +17,15 @@
 
 import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothDevice
+import android.hardware.BatteryState
 import android.hardware.input.InputManager
 import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.view.InputDevice
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import java.util.concurrent.Executor
@@ -31,30 +34,27 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
-@Ignore("b/257936830 until bt APIs")
 class StylusManagerTest : SysuiTestCase() {
     @Mock lateinit var inputManager: InputManager
-
     @Mock lateinit var stylusDevice: InputDevice
-
     @Mock lateinit var btStylusDevice: InputDevice
-
     @Mock lateinit var otherDevice: InputDevice
-
+    @Mock lateinit var batteryState: BatteryState
     @Mock lateinit var bluetoothAdapter: BluetoothAdapter
-
     @Mock lateinit var bluetoothDevice: BluetoothDevice
-
     @Mock lateinit var handler: Handler
+    @Mock lateinit var featureFlags: FeatureFlags
 
     @Mock lateinit var stylusCallback: StylusManager.StylusCallback
 
@@ -75,11 +75,8 @@
             true
         }
 
-        stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR)
-
-        stylusManager.registerCallback(stylusCallback)
-
-        stylusManager.registerBatteryCallback(stylusBatteryCallback)
+        stylusManager =
+            StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
 
         whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
         whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
@@ -92,19 +89,47 @@
         whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice)
         whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice)
         whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID))
+        whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(false)
 
         whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice)
         whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS)
+
+        whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
+
+        stylusManager.startListener()
+        stylusManager.registerCallback(stylusCallback)
+        stylusManager.registerBatteryCallback(stylusBatteryCallback)
+        clearInvocations(inputManager)
     }
 
     @Test
-    fun startListener_registersInputDeviceListener() {
+    fun startListener_hasNotStarted_registersInputDeviceListener() {
+        stylusManager =
+            StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+
         stylusManager.startListener()
 
         verify(inputManager, times(1)).registerInputDeviceListener(any(), any())
     }
 
     @Test
+    fun startListener_hasStarted_doesNothing() {
+        stylusManager.startListener()
+
+        verifyZeroInteractions(inputManager)
+    }
+
+    @Test
+    fun onInputDeviceAdded_hasNotStarted_doesNothing() {
+        stylusManager =
+            StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+
+        stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+        verifyZeroInteractions(stylusCallback)
+    }
+
+    @Test
     fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() {
         stylusManager.registerCallback(otherStylusCallback)
 
@@ -117,6 +142,26 @@
     }
 
     @Test
+    fun onInputDeviceAdded_internalStylus_registersBatteryListener() {
+        whenever(stylusDevice.isExternal).thenReturn(false)
+
+        stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+        verify(inputManager, times(1))
+            .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager)
+    }
+
+    @Test
+    fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() {
+        whenever(stylusDevice.isExternal).thenReturn(true)
+
+        stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+        verify(inputManager, never())
+            .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager)
+    }
+
+    @Test
     fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
 
@@ -125,6 +170,23 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
+    fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() {
+        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
+
+        verify(stylusCallback, times(1)).onStylusFirstUsed()
+    }
+
+    @Test
+    @Ignore("b/257936830 until bt APIs")
+    fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() {
+        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
+
+        verify(inputManager, times(1)).setStylusEverUsed(mContext, true)
+    }
+
+    @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -143,6 +205,17 @@
     }
 
     @Test
+    fun onInputDeviceChanged_hasNotStarted_doesNothing() {
+        stylusManager =
+            StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+
+        stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
+
+        verifyZeroInteractions(stylusCallback)
+    }
+
+    @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
         // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
@@ -157,6 +230,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
         // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
@@ -168,6 +242,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
         // whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
@@ -179,6 +254,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -189,6 +265,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
 
@@ -198,6 +275,17 @@
     }
 
     @Test
+    fun onInputDeviceRemoved_hasNotStarted_doesNothing() {
+        stylusManager =
+            StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+        stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+        stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
+
+        verifyZeroInteractions(stylusCallback)
+    }
+
+    @Test
     fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
         stylusManager.registerCallback(otherStylusCallback)
@@ -219,6 +307,17 @@
     }
 
     @Test
+    fun onInputDeviceRemoved_unregistersBatteryListener() {
+        stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+        stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
+
+        verify(inputManager, times(1))
+            .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
+    }
+
+    @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceRemoved_btStylus_callsCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -232,6 +331,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onStylusBluetoothConnected_registersMetadataListener() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -239,6 +339,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
         whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)
 
@@ -248,6 +349,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -257,6 +359,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
         stylusManager.registerBatteryCallback(otherStylusBatteryCallback)
@@ -274,6 +377,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -288,6 +392,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -302,6 +407,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
         stylusManager.onMetadataChanged(
             bluetoothDevice,
@@ -313,6 +419,7 @@
     }
 
     @Test
+    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -326,6 +433,63 @@
             .onStylusBluetoothChargingStateChanged(any(), any(), any())
     }
 
+    @Test
+    @Ignore("TODO(b/261826950): remove on main")
+    fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() {
+        whenever(batteryState.isPresent).thenReturn(true)
+
+        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+        verify(inputManager).setStylusEverUsed(mContext, true)
+    }
+
+    @Test
+    @Ignore("TODO(b/261826950): remove on main")
+    fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() {
+        whenever(batteryState.isPresent).thenReturn(true)
+
+        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+        verify(stylusCallback, times(1)).onStylusFirstUsed()
+    }
+
+    @Test
+    @Ignore("TODO(b/261826950): remove on main")
+    fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() {
+        whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true)
+        whenever(batteryState.isPresent).thenReturn(true)
+
+        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+        verify(inputManager, never()).setStylusEverUsed(mContext, true)
+    }
+
+    @Test
+    @Ignore("TODO(b/261826950): remove on main")
+    fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() {
+        whenever(batteryState.isPresent).thenReturn(false)
+
+        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+        verify(inputManager, never())
+            .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
+    }
+
+    @Test
+    fun onBatteryStateChanged_hasNotStarted_doesNothing() {
+        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+        verifyZeroInteractions(inputManager)
+    }
+
+    @Test
+    fun onBatteryStateChanged_executesBatteryCallbacks() {
+        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+        verify(stylusBatteryCallback, times(1))
+            .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+    }
+
     companion object {
         private val EXECUTOR = Executor { r -> r.run() }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
index ff382a3..cc6be5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
@@ -25,17 +25,15 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.util.mockito.whenever
-import java.util.concurrent.Executor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -60,7 +58,6 @@
                 inputManager,
                 stylusUsiPowerUi,
                 featureFlags,
-                DIRECT_EXECUTOR,
             )
 
         whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true)
@@ -79,40 +76,33 @@
     }
 
     @Test
-    fun start_addsBatteryListenerForInternalStylus() {
+    fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
+        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID))
+
         startable.start()
 
-        verify(inputManager, times(1))
-            .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
+        verifyZeroInteractions(stylusManager)
     }
 
     @Test
-    fun onStylusAdded_internalStylus_addsBatteryListener() {
+    fun start_initStylusUsiPowerUi() {
+        startable.start()
+
+        verify(stylusUsiPowerUi, times(1)).init()
+    }
+
+    @Test
+    fun onStylusAdded_internal_updatesNotificationSuppression() {
         startable.onStylusAdded(STYLUS_DEVICE_ID)
 
-        verify(inputManager, times(1))
-            .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
+        verify(stylusUsiPowerUi, times(1)).updateSuppression(false)
     }
 
     @Test
-    fun onStylusAdded_externalStylus_doesNotAddBatteryListener() {
+    fun onStylusAdded_external_noop() {
         startable.onStylusAdded(EXTERNAL_DEVICE_ID)
 
-        verify(inputManager, never())
-            .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable)
-    }
-
-    @Test
-    fun onStylusRemoved_registeredStylus_removesBatteryListener() {
-        startable.onStylusAdded(STYLUS_DEVICE_ID)
-        startable.onStylusRemoved(STYLUS_DEVICE_ID)
-
-        inOrder(inputManager).let {
-            it.verify(inputManager, times(1))
-                .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
-            it.verify(inputManager, times(1))
-                .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable)
-        }
+        verifyZeroInteractions(stylusUsiPowerUi)
     }
 
     @Test
@@ -130,28 +120,34 @@
     }
 
     @Test
-    fun onBatteryStateChanged_batteryPresent_refreshesNotification() {
-        val batteryState = mock(BatteryState::class.java)
-        whenever(batteryState.isPresent).thenReturn(true)
+    fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() {
+        val batteryState = FixedCapacityBatteryState(0.1f)
 
-        startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+        startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
 
-        verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState)
+        verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState)
     }
 
     @Test
-    fun onBatteryStateChanged_batteryNotPresent_noop() {
+    fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() {
+        val batteryState = FixedCapacityBatteryState(0f)
+
+        startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+
+        verifyNoMoreInteractions(stylusUsiPowerUi)
+    }
+
+    @Test
+    fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() {
         val batteryState = mock(BatteryState::class.java)
         whenever(batteryState.isPresent).thenReturn(false)
 
-        startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+        startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
 
         verifyNoMoreInteractions(stylusUsiPowerUi)
     }
 
     companion object {
-        private val DIRECT_EXECUTOR = Executor { r -> r.run() }
-
         private const val EXTERNAL_DEVICE_ID = 0
         private const val STYLUS_DEVICE_ID = 1
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
index 5987550..1e81dc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
@@ -16,8 +16,12 @@
 
 package com.android.systemui.stylus
 
-import android.hardware.BatteryState
+import android.app.Notification
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
 import android.hardware.input.InputManager
+import android.os.Bundle
 import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.view.InputDevice
@@ -26,14 +30,22 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
@@ -46,13 +58,19 @@
     @Mock lateinit var inputManager: InputManager
     @Mock lateinit var handler: Handler
     @Mock lateinit var btStylusDevice: InputDevice
+    @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification>
 
     private lateinit var stylusUsiPowerUi: StylusUsiPowerUI
+    private lateinit var broadcastReceiver: BroadcastReceiver
+    private lateinit var contextSpy: Context
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        contextSpy = spy(mContext)
+        doNothing().whenever(contextSpy).startActivity(any())
+
         whenever(handler.post(any())).thenAnswer {
             (it.arguments[0] as Runnable).run()
             true
@@ -63,56 +81,77 @@
         whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
         // whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES")
 
-        stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler)
+        stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler)
+        broadcastReceiver = stylusUsiPowerUi.receiver
+    }
+
+    @Test
+    fun updateBatteryState_capacityZero_noop() {
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f))
+
+        verifyNoMoreInteractions(notificationManager)
     }
 
     @Test
     fun updateBatteryState_capacityBelowThreshold_notifies() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
 
-        verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
+        verify(notificationManager, times(1))
+            .notify(eq(R.string.stylus_battery_low_percentage), any())
         verifyNoMoreInteractions(notificationManager)
     }
 
     @Test
     fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
 
-        verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+        verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
         verifyNoMoreInteractions(notificationManager)
     }
 
     @Test
     fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
 
         inOrder(notificationManager).let {
-            it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
-            it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+            it.verify(notificationManager, times(1))
+                .notify(eq(R.string.stylus_battery_low_percentage), any())
+            it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
             it.verifyNoMoreInteractions()
         }
     }
 
     @Test
     fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f))
 
-        verify(notificationManager, times(2)).notify(eq(R.string.stylus_battery_low), any())
+        verify(notificationManager, times(2))
+            .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture())
+        assertEquals(
+            notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE),
+            context.getString(R.string.stylus_battery_low_percentage, "15%")
+        )
+        assertEquals(
+            notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT),
+            context.getString(R.string.stylus_battery_low_subtitle)
+        )
         verifyNoMoreInteractions(notificationManager)
     }
 
     @Test
     fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
 
         inOrder(notificationManager).let {
-            it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
-            it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
-            it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
+            it.verify(notificationManager, times(1))
+                .notify(eq(R.string.stylus_battery_low_percentage), any())
+            it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
+            it.verify(notificationManager, times(1))
+                .notify(eq(R.string.stylus_battery_low_percentage), any())
             it.verifyNoMoreInteractions()
         }
     }
@@ -121,47 +160,66 @@
     fun updateSuppression_noExistingNotification_cancelsNotification() {
         stylusUsiPowerUi.updateSuppression(true)
 
-        verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+        verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
         verifyNoMoreInteractions(notificationManager)
     }
 
     @Test
     fun updateSuppression_existingNotification_cancelsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
 
         stylusUsiPowerUi.updateSuppression(true)
 
         inOrder(notificationManager).let {
-            it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
-            it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+            it.verify(notificationManager, times(1))
+                .notify(eq(R.string.stylus_battery_low_percentage), any())
+            it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
             it.verifyNoMoreInteractions()
         }
     }
 
     @Test
     @Ignore("TODO(b/257936830): get bt address once input api available")
-    fun refresh_hasConnectedBluetoothStylus_doesNotNotify() {
+    fun refresh_hasConnectedBluetoothStylus_cancelsNotification() {
         whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
 
         stylusUsiPowerUi.refresh()
 
-        verifyNoMoreInteractions(notificationManager)
+        verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
     }
 
     @Test
     @Ignore("TODO(b/257936830): get bt address once input api available")
     fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
         whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
 
         stylusUsiPowerUi.refresh()
 
-        verify(notificationManager).cancel(R.string.stylus_battery_low)
+        verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
     }
 
-    class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
-        override fun getCapacity() = capacity
-        override fun getStatus() = 0
-        override fun isPresent() = true
+    @Test
+    fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() {
+        val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+        val activityIntentCaptor = argumentCaptor<Intent>()
+        stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f))
+        broadcastReceiver.onReceive(contextSpy, intent)
+
+        verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture())
+        assertThat(activityIntentCaptor.value.action)
+            .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS)
+        val args =
+            activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS)
+                as Bundle
+        assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1)
+    }
+
+    @Test
+    fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() {
+        val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+        broadcastReceiver.onReceive(contextSpy, intent)
+
+        verify(contextSpy, never()).startActivity(any())
     }
 }
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/surfaceeffects/ripple/RippleShaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleShaderTest.kt
new file mode 100644
index 0000000..89cc18cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleShaderTest.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.surfaceeffects.ripple
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RippleShaderTest : SysuiTestCase() {
+
+    private lateinit var rippleShader: RippleShader
+
+    @Before
+    fun setup() {
+        rippleShader = RippleShader()
+    }
+
+    @Test
+    fun setMaxSize_hasCorrectSizes() {
+        val expectedMaxWidth = 300f
+        val expectedMaxHeight = 500f
+
+        rippleShader.rippleSize.setMaxSize(expectedMaxWidth, expectedMaxHeight)
+
+        assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(2)
+        assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(rippleShader.rippleSize.initialSize)
+        val maxSize = rippleShader.rippleSize.sizes[1]
+        assertThat(maxSize.t).isEqualTo(1f)
+        assertThat(maxSize.width).isEqualTo(expectedMaxWidth)
+        assertThat(maxSize.height).isEqualTo(expectedMaxHeight)
+    }
+
+    @Test
+    fun setSizeAtProgresses_hasCorrectSizes() {
+        val expectedSize0 = RippleShader.SizeAtProgress(t = 0f, width = 100f, height = 100f)
+        val expectedSize1 = RippleShader.SizeAtProgress(t = 0.2f, width = 1500f, height = 1200f)
+        val expectedSize2 = RippleShader.SizeAtProgress(t = 0.4f, width = 200f, height = 70f)
+
+        rippleShader.rippleSize.setSizeAtProgresses(expectedSize0, expectedSize1, expectedSize2)
+
+        assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(3)
+        assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(expectedSize0)
+        assertThat(rippleShader.rippleSize.sizes[1]).isEqualTo(expectedSize1)
+        assertThat(rippleShader.rippleSize.sizes[2]).isEqualTo(expectedSize2)
+    }
+
+    @Test
+    fun setSizeAtProgresses_sizeListIsSortedByT() {
+        val expectedSize0 = RippleShader.SizeAtProgress(t = 0f, width = 100f, height = 100f)
+        val expectedSize1 = RippleShader.SizeAtProgress(t = 0.2f, width = 1500f, height = 1200f)
+        val expectedSize2 = RippleShader.SizeAtProgress(t = 0.4f, width = 200f, height = 70f)
+        val expectedSize3 = RippleShader.SizeAtProgress(t = 0.8f, width = 300f, height = 900f)
+        val expectedSize4 = RippleShader.SizeAtProgress(t = 1f, width = 500f, height = 300f)
+
+        // Add them in unsorted order
+        rippleShader.rippleSize.setSizeAtProgresses(
+            expectedSize0,
+            expectedSize3,
+            expectedSize2,
+            expectedSize4,
+            expectedSize1
+        )
+
+        assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(5)
+        assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(expectedSize0)
+        assertThat(rippleShader.rippleSize.sizes[1]).isEqualTo(expectedSize1)
+        assertThat(rippleShader.rippleSize.sizes[2]).isEqualTo(expectedSize2)
+        assertThat(rippleShader.rippleSize.sizes[3]).isEqualTo(expectedSize3)
+        assertThat(rippleShader.rippleSize.sizes[4]).isEqualTo(expectedSize4)
+    }
+
+    @Test
+    fun update_getsCorrectNextTargetSize() {
+        val expectedSize0 = RippleShader.SizeAtProgress(t = 0f, width = 100f, height = 100f)
+        val expectedSize1 = RippleShader.SizeAtProgress(t = 0.2f, width = 1500f, height = 1200f)
+        val expectedSize2 = RippleShader.SizeAtProgress(t = 0.4f, width = 200f, height = 70f)
+        val expectedSize3 = RippleShader.SizeAtProgress(t = 0.8f, width = 300f, height = 900f)
+        val expectedSize4 = RippleShader.SizeAtProgress(t = 1f, width = 500f, height = 300f)
+
+        rippleShader.rippleSize.setSizeAtProgresses(
+            expectedSize0,
+            expectedSize1,
+            expectedSize2,
+            expectedSize3,
+            expectedSize4
+        )
+
+        rippleShader.rippleSize.update(0.5f)
+        // Progress is between 0.4 and 0.8 (expectedSize3 and 4), so the index should be 3.
+        assertThat(rippleShader.rippleSize.currentSizeIndex).isEqualTo(3)
+    }
+
+    @Test
+    fun update_sizeListIsEmpty_setsInitialSize() {
+        assertThat(rippleShader.rippleSize.sizes).isEmpty()
+
+        rippleShader.rippleSize.update(0.3f)
+
+        assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(1)
+        assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(rippleShader.rippleSize.initialSize)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 99e2012..c7c6b94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -159,7 +159,7 @@
         underTest.displayView(getState())
         assertThat(fakeWakeLock.isHeld).isTrue()
 
-        underTest.removeView("id", "test reason")
+        underTest.removeView(DEFAULT_ID, "test reason")
 
         assertThat(fakeWakeLock.isHeld).isFalse()
     }
@@ -175,6 +175,8 @@
 
     @Test
     fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "name",
@@ -199,10 +201,15 @@
         assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
         assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
         verify(windowManager).removeView(viewCaptor.allValues[0])
+        // Since the controller is still storing the older view in case it'll get re-displayed
+        // later, the listener shouldn't be notified
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
     fun displayView_viewDoesNotDisappearsBeforeTimeout() {
+        val listener = registerListener()
+
         val state = getState()
         underTest.displayView(state)
         reset(windowManager)
@@ -210,10 +217,13 @@
         fakeClock.advanceTime(TIMEOUT_MS - 1)
 
         verify(windowManager, never()).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
     fun displayView_viewDisappearsAfterTimeout() {
+        val listener = registerListener()
+
         val state = getState()
         underTest.displayView(state)
         reset(windowManager)
@@ -221,10 +231,13 @@
         fakeClock.advanceTime(TIMEOUT_MS + 1)
 
         verify(windowManager).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
     }
 
     @Test
     fun displayView_calledAgainBeforeTimeout_timeoutReset() {
+        val listener = registerListener()
+
         // First, display the view
         val state = getState()
         underTest.displayView(state)
@@ -239,10 +252,13 @@
 
         // Verify we didn't hide the view
         verify(windowManager, never()).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
     fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() {
+        val listener = registerListener()
+
         // First, display the view
         val state = getState()
         underTest.displayView(state)
@@ -255,6 +271,7 @@
         fakeClock.advanceTime(TIMEOUT_MS + 1)
 
         verify(windowManager).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
     }
 
     @Test
@@ -271,25 +288,9 @@
     }
 
     @Test
-    fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() {
-        var runnable1Run = false
-        underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
-            runnable1Run = true
-        }
-
-        var runnable2Run = false
-        underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
-            runnable2Run = true
-        }
-
-        fakeClock.advanceTime(TIMEOUT_MS + 1)
-
-        assertThat(runnable1Run).isFalse()
-        assertThat(runnable2Run).isTrue()
-    }
-
-    @Test
     fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "name",
@@ -315,10 +316,16 @@
         assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
         verify(windowManager).removeView(viewCaptor.allValues[0])
         verify(configurationController, never()).removeCallback(any())
+
+        // Since the controller is still storing the older view in case it'll get re-displayed
+        // later, the listener shouldn't be notified
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
-    fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
+    fun multipleViewsWithDifferentIds_newViewRemoved_previousViewIsDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(ViewInfo("First name", id = "id1"))
 
         verify(windowManager).addView(any(), any())
@@ -329,24 +336,35 @@
         verify(windowManager).removeView(any())
         verify(windowManager).addView(any(), any())
         reset(windowManager)
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
 
+        // WHEN the current view is removed
         underTest.removeView("id2", "test reason")
 
+        // THEN it's correctly removed
         verify(windowManager).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly("id2")
+
+        // And the previous view is correctly added
         verify(windowManager).addView(any(), any())
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
 
+        // WHEN the previous view times out
         reset(windowManager)
         fakeClock.advanceTime(TIMEOUT_MS + 1)
 
+        // THEN it is also removed
         verify(windowManager).removeView(any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id2", "id1"))
     }
 
     @Test
     fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(ViewInfo("First name", id = "id1"))
 
         verify(windowManager).addView(any(), any())
@@ -361,7 +379,8 @@
         // WHEN an old view is removed
         underTest.removeView("id1", "test reason")
 
-        // THEN we don't update anything
+        // THEN we don't update anything except the listener
+        assertThat(listener.permanentlyRemovedIds).containsExactly("id1")
         verify(windowManager, never()).removeView(any())
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
@@ -372,10 +391,13 @@
         verify(windowManager).removeView(any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id1", "id2"))
     }
 
     @Test
     fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(ViewInfo("First name", id = "id1"))
         underTest.displayView(ViewInfo("Second name", id = "id2"))
         underTest.displayView(ViewInfo("Third name", id = "id3"))
@@ -387,6 +409,7 @@
         underTest.removeView("id3", "test reason")
 
         verify(windowManager).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3"))
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
         verify(configurationController, never()).removeCallback(any())
@@ -395,6 +418,7 @@
         underTest.removeView("id2", "test reason")
 
         verify(windowManager).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2"))
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
         verify(configurationController, never()).removeCallback(any())
@@ -403,6 +427,7 @@
         fakeClock.advanceTime(TIMEOUT_MS + 1)
 
         verify(windowManager).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2", "id1"))
         assertThat(underTest.activeViews.size).isEqualTo(0)
         verify(configurationController).removeCallback(any())
     }
@@ -438,6 +463,8 @@
 
     @Test
     fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
         fakeClock.advanceTime(1000)
         underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
@@ -451,10 +478,13 @@
         verify(windowManager, never()).addView(any(), any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2", "id3")
     }
 
     @Test
     fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
         fakeClock.advanceTime(1000)
         underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
@@ -467,10 +497,13 @@
         verify(windowManager, never()).addView(any(), any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
     }
 
     @Test
     fun lowerThenHigherPriority_higherReplacesLower() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "normal",
@@ -499,10 +532,15 @@
         verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
         assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
         verify(configurationController, never()).removeCallback(any())
+        // Since the controller is still storing the older view in case it'll get re-displayed
+        // later, the listener shouldn't be notified
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
     fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "normal",
@@ -537,6 +575,7 @@
 
         // THEN the normal view is re-displayed
         verify(windowManager).removeView(viewCaptor.allValues[1])
+        assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
         verify(windowManager).addView(any(), capture(windowParamsCaptor))
         assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
         verify(configurationController, never()).removeCallback(any())
@@ -544,6 +583,8 @@
 
     @Test
     fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "normal",
@@ -573,6 +614,7 @@
         verify(windowManager, never()).addView(any(), any())
         assertThat(underTest.activeViews).isEmpty()
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
     }
 
     @Test
@@ -609,6 +651,8 @@
 
     @Test
     fun higherThenLowerPriority_lowerEventuallyDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "critical",
@@ -644,6 +688,7 @@
 
         // THEN the second normal view is displayed
         verify(windowManager).removeView(viewCaptor.value)
+        assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
         verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
         assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
         assertThat(underTest.activeViews.size).isEqualTo(1)
@@ -652,6 +697,8 @@
 
     @Test
     fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "critical",
@@ -691,10 +738,13 @@
         verify(windowManager, never()).addView(any(), any())
         assertThat(underTest.activeViews).isEmpty()
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
     }
 
     @Test
     fun criticalThenNewCritical_newCriticalDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "critical 1",
@@ -724,10 +774,15 @@
         assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
         assertThat(underTest.activeViews.size).isEqualTo(2)
         verify(configurationController, never()).removeCallback(any())
+        // Since the controller is still storing the older view in case it'll get re-displayed
+        // later, the listener shouldn't be notified
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
     fun normalThenNewNormal_newNormalDisplayed() {
+        val listener = registerListener()
+
         underTest.displayView(
             ViewInfo(
                 name = "normal 1",
@@ -757,6 +812,9 @@
         assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
         assertThat(underTest.activeViews.size).isEqualTo(2)
         verify(configurationController, never()).removeCallback(any())
+        // Since the controller is still storing the older view in case it'll get re-displayed
+        // later, the listener shouldn't be notified
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
     }
 
     @Test
@@ -957,25 +1015,103 @@
     }
 
     @Test
-    fun removeView_viewRemovedAndRemovalLogged() {
+    fun removeView_viewRemovedAndRemovalLoggedAndListenerNotified() {
+        val listener = registerListener()
+
         // First, add the view
         underTest.displayView(getState())
 
         // Then, remove it
         val reason = "test reason"
-        val deviceId = "id"
-        underTest.removeView(deviceId, reason)
+        underTest.removeView(DEFAULT_ID, reason)
 
         verify(windowManager).removeView(any())
-        verify(logger).logViewRemoval(deviceId, reason)
+        verify(logger).logViewRemoval(DEFAULT_ID, reason)
         verify(configurationController).removeCallback(any())
+        assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
     }
 
     @Test
-    fun removeView_noAdd_viewNotRemoved() {
+    fun removeView_noAdd_viewNotRemovedAndListenerNotNotified() {
+        val listener = registerListener()
+
         underTest.removeView("id", "reason")
 
         verify(windowManager, never()).removeView(any())
+        assertThat(listener.permanentlyRemovedIds).isEmpty()
+    }
+
+    @Test
+    fun listenerRegistered_notifiedOnRemoval() {
+        val listener = registerListener()
+        underTest.displayView(getState())
+
+        underTest.removeView(DEFAULT_ID, "reason")
+
+        assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+    }
+
+    @Test
+    fun listenerRegistered_notifiedOnTimedOutEvenWhenNotDisplayed() {
+        val listener = registerListener()
+        underTest.displayView(
+            ViewInfo(
+                id = "id1",
+                name = "name1",
+                timeoutMs = 3000,
+            ),
+        )
+
+        // Display a second view
+        underTest.displayView(
+            ViewInfo(
+                id = "id2",
+                name = "name2",
+                timeoutMs = 2500,
+            ),
+        )
+
+        // WHEN the second view times out
+        fakeClock.advanceTime(2501)
+
+        // THEN the listener is notified of both IDs, since id2 timed out and id1 doesn't have
+        // enough time left to be redisplayed
+        assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
+    }
+
+    @Test
+    fun multipleListeners_allNotified() {
+        val listener1 = registerListener()
+        val listener2 = registerListener()
+        val listener3 = registerListener()
+
+        underTest.displayView(getState())
+
+        underTest.removeView(DEFAULT_ID, "reason")
+
+        assertThat(listener1.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+        assertThat(listener2.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+        assertThat(listener3.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+    }
+
+    @Test
+    fun sameListenerRegisteredMultipleTimes_onlyNotifiedOnce() {
+        val listener = registerListener()
+        underTest.registerListener(listener)
+        underTest.registerListener(listener)
+
+        underTest.displayView(getState())
+
+        underTest.removeView(DEFAULT_ID, "reason")
+
+        assertThat(listener.permanentlyRemovedIds).hasSize(1)
+        assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+    }
+
+    private fun registerListener(): Listener {
+        return Listener().also {
+            underTest.registerListener(it)
+        }
     }
 
     private fun getState(name: String = "name") = ViewInfo(name)
@@ -1030,9 +1166,17 @@
         override val windowTitle: String = "Window Title",
         override val wakeReason: String = "WAKE_REASON",
         override val timeoutMs: Int = TIMEOUT_MS.toInt(),
-        override val id: String = "id",
+        override val id: String = DEFAULT_ID,
         override val priority: ViewPriority = ViewPriority.NORMAL,
     ) : TemporaryViewInfo()
+
+    inner class Listener : TemporaryViewDisplayController.Listener {
+        val permanentlyRemovedIds = mutableListOf<String>()
+        override fun onInfoPermanentlyRemoved(id: String, reason: String) {
+            permanentlyRemovedIds.add(id)
+        }
+    }
 }
 
 private const val TIMEOUT_MS = 10000L
+private const val DEFAULT_ID = "defaultId"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 90178c6..586bdc6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -20,12 +20,14 @@
 import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.core.animation.doOnCancel
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.R
@@ -43,6 +45,8 @@
 import com.android.systemui.temporarydisplay.ViewPriority
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
@@ -54,6 +58,7 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -62,7 +67,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class ChipbarCoordinatorTest : SysuiTestCase() {
-    private lateinit var underTest: FakeChipbarCoordinator
+    private lateinit var underTest: ChipbarCoordinator
 
     @Mock private lateinit var logger: ChipbarLogger
     @Mock private lateinit var accessibilityManager: AccessibilityManager
@@ -74,6 +79,8 @@
     @Mock private lateinit var falsingCollector: FalsingCollector
     @Mock private lateinit var viewUtil: ViewUtil
     @Mock private lateinit var vibratorHelper: VibratorHelper
+    @Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
+    private lateinit var chipbarAnimator: TestChipbarAnimator
     private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
     private lateinit var fakeWakeLock: WakeLockFake
     private lateinit var fakeClock: FakeSystemClock
@@ -93,9 +100,10 @@
         fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
 
         uiEventLoggerFake = UiEventLoggerFake()
+        chipbarAnimator = TestChipbarAnimator()
 
         underTest =
-            FakeChipbarCoordinator(
+            ChipbarCoordinator(
                 context,
                 logger,
                 windowManager,
@@ -104,8 +112,10 @@
                 configurationController,
                 dumpManager,
                 powerManager,
+                chipbarAnimator,
                 falsingManager,
                 falsingCollector,
+                swipeGestureHandler,
                 viewUtil,
                 vibratorHelper,
                 fakeWakeLockBuilder,
@@ -352,6 +362,105 @@
     }
 
     @Test
+    fun displayView_loading_animationStarted() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue()
+    }
+
+    @Test
+    fun displayView_notLoading_noAnimation() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Error,
+            )
+        )
+
+        assertThat(underTest.loadingDetails).isNull()
+    }
+
+    @Test
+    fun displayView_loadingThenNotLoading_animationStopped() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val animator = underTest.loadingDetails!!.animator
+        var cancelled = false
+        animator.doOnCancel { cancelled = true }
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Button(Text.Loaded("button")) {},
+            )
+        )
+
+        assertThat(cancelled).isTrue()
+        assertThat(underTest.loadingDetails).isNull()
+    }
+
+    @Test
+    fun displayView_loadingThenHideView_animationStopped() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val animator = underTest.loadingDetails!!.animator
+        var cancelled = false
+        animator.doOnCancel { cancelled = true }
+
+        underTest.removeView(DEVICE_ID, "TestReason")
+
+        assertThat(cancelled).isTrue()
+        assertThat(underTest.loadingDetails).isNull()
+    }
+
+    @Test
+    fun displayView_loadingThenNewLoading_animationStaysTheSame() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val animator = underTest.loadingDetails!!.animator
+        var cancelled = false
+        animator.doOnCancel { cancelled = true }
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("new text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        assertThat(underTest.loadingDetails!!.animator).isEqualTo(animator)
+        assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue()
+        assertThat(cancelled).isFalse()
+    }
+
+    @Test
     fun displayView_vibrationEffect_doubleClickEffect() {
         underTest.displayView(
             createChipbarInfo(
@@ -365,6 +474,26 @@
         verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
     }
 
+    /** Regression test for b/266119467. */
+    @Test
+    fun displayView_animationFailure_viewsStillBecomeVisible() {
+        chipbarAnimator.allowAnimation = false
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val view = getChipbarView()
+        assertThat(view.getInnerView().alpha).isEqualTo(1f)
+        assertThat(view.getStartIconView().alpha).isEqualTo(1f)
+        assertThat(view.getLoadingIcon().alpha).isEqualTo(1f)
+        assertThat(view.getChipTextView().alpha).isEqualTo(1f)
+    }
+
     @Test
     fun updateView_viewUpdated() {
         // First, display a view
@@ -430,17 +559,137 @@
         verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any())
     }
 
+    /** Regression test for b/266209420. */
+    @Test
+    fun displayViewThenImmediateRemoval_viewStillRemoved() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Error,
+            ),
+        )
+        val chipbarView = getChipbarView()
+
+        underTest.removeView(DEVICE_ID, "test reason")
+
+        verify(windowManager).removeView(chipbarView)
+    }
+
+    /** Regression test for b/266209420. */
+    @Test
+    fun removeView_animationFailure_viewStillRemoved() {
+        chipbarAnimator.allowAnimation = false
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Error,
+            ),
+        )
+        val chipbarView = getChipbarView()
+
+        underTest.removeView(DEVICE_ID, "test reason")
+
+        verify(windowManager).removeView(chipbarView)
+    }
+
+    @Test
+    fun swipeToDismiss_false_neverListensForGesture() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+                allowSwipeToDismiss = false,
+            )
+        )
+
+        verify(swipeGestureHandler, never()).addOnGestureDetectedCallback(any(), any())
+    }
+
+    @Test
+    fun swipeToDismiss_true_listensForGesture() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+                allowSwipeToDismiss = true,
+            )
+        )
+
+        verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), any())
+    }
+
+    @Test
+    fun swipeToDismiss_swipeOccurs_viewDismissed() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+                allowSwipeToDismiss = true,
+            )
+        )
+        val view = getChipbarView()
+
+        val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
+        verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
+
+        callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
+
+        verify(windowManager).removeView(view)
+    }
+
+    @Test
+    fun swipeToDismiss_viewUpdatedToFalse_swipeOccurs_viewNotDismissed() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+                allowSwipeToDismiss = true,
+            )
+        )
+        val view = getChipbarView()
+        val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
+        verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
+
+        // WHEN the view is updated to not allow swipe-to-dismiss
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+                allowSwipeToDismiss = false,
+            )
+        )
+
+        // THEN the callback is removed
+        verify(swipeGestureHandler).removeOnGestureDetectedCallback(any())
+
+        // And WHEN the old callback is invoked
+        callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
+
+        // THEN it is ignored and view isn't removed
+        verify(windowManager, never()).removeView(view)
+    }
+
     private fun createChipbarInfo(
         startIcon: Icon,
         text: Text,
         endItem: ChipbarEndItem?,
         vibrationEffect: VibrationEffect? = null,
+        allowSwipeToDismiss: Boolean = false,
     ): ChipbarInfo {
         return ChipbarInfo(
             TintedIcon(startIcon, tintAttr = null),
             text,
             endItem,
             vibrationEffect,
+            allowSwipeToDismiss,
             windowTitle = WINDOW_TITLE,
             wakeReason = WAKE_REASON,
             timeoutMs = TIMEOUT,
@@ -453,8 +702,9 @@
 
     private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
 
-    private fun ViewGroup.getChipText(): String =
-        (this.requireViewById<TextView>(R.id.text)).text as String
+    private fun ViewGroup.getChipTextView() = this.requireViewById<TextView>(R.id.text)
+
+    private fun ViewGroup.getChipText(): String = this.getChipTextView().text as String
 
     private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
@@ -467,6 +717,25 @@
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
+
+    /** Test class that lets us disallow animations. */
+    inner class TestChipbarAnimator : ChipbarAnimator() {
+        var allowAnimation: Boolean = true
+
+        override fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+            if (!allowAnimation) {
+                return false
+            }
+            return super.animateViewIn(innerView, onAnimationEnd)
+        }
+
+        override fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+            if (!allowAnimation) {
+                return false
+            }
+            return super.animateViewOut(innerView, onAnimationEnd)
+        }
+    }
 }
 
 private const val TIMEOUT = 10000
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
deleted file mode 100644
index 4ef4e6c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.temporarydisplay.chipbar
-
-import android.content.Context
-import android.os.PowerManager
-import android.view.ViewGroup
-import android.view.WindowManager
-import android.view.accessibility.AccessibilityManager
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.util.view.ViewUtil
-import com.android.systemui.util.wakelock.WakeLock
-
-/** A fake implementation of [ChipbarCoordinator] for testing. */
-class FakeChipbarCoordinator(
-    context: Context,
-    logger: ChipbarLogger,
-    windowManager: WindowManager,
-    mainExecutor: DelayableExecutor,
-    accessibilityManager: AccessibilityManager,
-    configurationController: ConfigurationController,
-    dumpManager: DumpManager,
-    powerManager: PowerManager,
-    falsingManager: FalsingManager,
-    falsingCollector: FalsingCollector,
-    viewUtil: ViewUtil,
-    vibratorHelper: VibratorHelper,
-    wakeLockBuilder: WakeLock.Builder,
-    systemClock: SystemClock,
-) :
-    ChipbarCoordinator(
-        context,
-        logger,
-        windowManager,
-        mainExecutor,
-        accessibilityManager,
-        configurationController,
-        dumpManager,
-        powerManager,
-        falsingManager,
-        falsingCollector,
-        viewUtil,
-        vibratorHelper,
-        wakeLockBuilder,
-        systemClock,
-    ) {
-    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
-        // Just bypass the animation in tests
-        onAnimationEnd.run()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt
new file mode 100644
index 0000000..c539246
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class SwipeChipbarAwayGestureHandlerTest : SysuiTestCase() {
+
+    private lateinit var underTest: SwipeChipbarAwayGestureHandler
+
+    @Before
+    fun setUp() {
+        underTest = SwipeChipbarAwayGestureHandler(context, FakeDisplayTracker(mContext), mock())
+    }
+
+    @Test
+    fun startOfGestureIsWithinBounds_noViewFetcher_returnsFalse() {
+        assertThat(underTest.startOfGestureIsWithinBounds(createMotionEvent())).isFalse()
+    }
+
+    @Test
+    fun startOfGestureIsWithinBounds_usesViewFetcher_aboveBottom_returnsTrue() {
+        val view = createMockView()
+
+        underTest.setViewFetcher { view }
+
+        val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f)
+        assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
+    }
+
+    @Test
+    fun startOfGestureIsWithinBounds_usesViewFetcher_slightlyBelowBottom_returnsTrue() {
+        val view = createMockView()
+
+        underTest.setViewFetcher { view }
+
+        val motionEvent = createMotionEvent(y = VIEW_BOTTOM + 20f)
+        assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
+    }
+
+    @Test
+    fun startOfGestureIsWithinBounds_usesViewFetcher_tooFarDown_returnsFalse() {
+        val view = createMockView()
+
+        underTest.setViewFetcher { view }
+
+        val motionEvent = createMotionEvent(y = VIEW_BOTTOM * 4f)
+        assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse()
+    }
+
+    @Test
+    fun startOfGestureIsWithinBounds_viewFetcherReset_returnsFalse() {
+        val view = createMockView()
+
+        underTest.setViewFetcher { view }
+
+        val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f)
+        assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
+
+        underTest.resetViewFetcher()
+        assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse()
+    }
+
+    private fun createMotionEvent(y: Float = 0f): MotionEvent {
+        return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, y, 0)
+    }
+
+    private fun createMockView(): View {
+        return mock<View>().also {
+            doAnswer { invocation ->
+                    val out: Rect = invocation.getArgument(0)
+                    out.set(0, 0, 0, VIEW_BOTTOM)
+                    null
+                }
+                .whenever(it)
+                .getBoundsOnScreen(any())
+        }
+    }
+
+    private companion object {
+        const val VIEW_BOTTOM = 455
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 2a93fff..1710709 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -172,7 +172,7 @@
         verify(mDumpManager).registerDumpable(any(), any());
         verify(mDeviceProvisionedController).addCallback(mDeviceProvisionedListener.capture());
         verify(mSecureSettings).registerContentObserverForUser(
-                eq(Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES)),
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES),
                 eq(false), mSettingsObserver.capture(), eq(UserHandle.USER_ALL)
         );
     }
@@ -790,15 +790,15 @@
 
         reset(mResources);
         when(mResources.getColor(eq(android.R.color.system_accent1_500), any()))
-                .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().get(6));
+                .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().getS500());
         when(mResources.getColor(eq(android.R.color.system_accent2_500), any()))
-                .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().get(6));
+                .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().getS500());
         when(mResources.getColor(eq(android.R.color.system_accent3_500), any()))
-                .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().get(6));
+                .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().getS500());
         when(mResources.getColor(eq(android.R.color.system_neutral1_500), any()))
-                .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().get(6));
+                .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().getS500());
         when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
-                .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().get(6));
+                .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().getS500());
 
         // Defers event because we already have initial colors.
         verify(mThemeOverlayApplier, never())
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 89402de..a87e61a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -25,10 +25,14 @@
 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.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.unfold.util.FoldableDeviceStates
@@ -73,6 +77,8 @@
 
     @Mock lateinit var viewTreeObserver: ViewTreeObserver
 
+    @Mock private lateinit var commandQueue: CommandQueue
+
     @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
 
     private lateinit var deviceStates: FoldableDeviceStates
@@ -102,7 +108,14 @@
             }
 
         keyguardRepository = FakeKeyguardRepository()
-        val keyguardInteractor = KeyguardInteractor(repository = keyguardRepository)
+        val featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) }
+        val keyguardInteractor =
+            KeyguardInteractor(
+                repository = keyguardRepository,
+                commandQueue = commandQueue,
+                featureFlags = featureFlags,
+                bouncerRepository = FakeKeyguardBouncerRepository(),
+            )
 
         // Needs to be run on the main thread
         runBlocking(IMMEDIATE) {
@@ -171,8 +184,10 @@
 
             fold()
             underTest.onScreenTurningOn({})
-            underTest.onStartedWakingUp()
+            // The body of onScreenTurningOn is executed on fakeExecutor,
+            // run all pending tasks before calling the next method
             fakeExecutor.runAllReady()
+            underTest.onStartedWakingUp()
 
             verify(latencyTracker).onActionStart(any())
             verify(latencyTracker).onActionCancel(any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
index c316402..4a28cd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
@@ -26,6 +26,10 @@
         listeners.forEach { it.onTransitionFinished() }
     }
 
+    override fun onTransitionFinishing() {
+        listeners.forEach { it.onTransitionFinishing() }
+    }
+
     override fun onTransitionProgress(progress: Float) {
         listeners.forEach { it.onTransitionProgress(progress) }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
new file mode 100644
index 0000000..d3fdbd9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.unfold
+
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UnfoldHapticsPlayerTest : SysuiTestCase() {
+
+    private val progressProvider = TestUnfoldTransitionProvider()
+    private val vibrator: Vibrator = mock()
+    private val testFoldProvider = TestFoldProvider()
+
+    private lateinit var player: UnfoldHapticsPlayer
+
+    @Before
+    fun before() {
+        player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, Runnable::run, vibrator)
+    }
+
+    @Test
+    fun testUnfoldingTransitionFinishingEarly_playsHaptics() {
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+
+        verify(vibrator).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun testUnfoldingTransitionFinishingLate_doesNotPlayHaptics() {
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.99f)
+        progressProvider.onTransitionFinishing()
+
+        verify(vibrator, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun testFoldingAfterUnfolding_doesNotPlayHaptics() {
+        // Unfold
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+        progressProvider.onTransitionFinished()
+        clearInvocations(vibrator)
+
+        // Fold
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinished()
+        testFoldProvider.onFoldUpdate(isFolded = true)
+
+        verify(vibrator, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun testUnfoldingAfterFoldingAndUnfolding_playsHaptics() {
+        // Unfold
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+        progressProvider.onTransitionFinished()
+
+        // Fold
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinished()
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        clearInvocations(vibrator)
+
+        // Unfold again
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+        progressProvider.onTransitionFinished()
+
+        verify(vibrator).vibrate(any<VibrationEffect>())
+    }
+
+    private class TestFoldProvider : FoldProvider {
+        private val listeners = arrayListOf<FoldProvider.FoldCallback>()
+
+        override fun registerCallback(callback: FoldProvider.FoldCallback, executor: Executor) {
+            listeners += callback
+        }
+
+        override fun unregisterCallback(callback: FoldProvider.FoldCallback) {
+            listeners -= callback
+        }
+
+        fun onFoldUpdate(isFolded: Boolean) {
+            listeners.forEach { it.onFoldUpdated(isFolded) }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
index abbdab0..0413d92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
@@ -20,17 +20,12 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
-import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
-import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
+import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
 import com.android.systemui.unfold.util.TestFoldStateProvider
-import com.android.systemui.util.leak.ReferenceTestUtils.waitForCondition
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -45,9 +40,7 @@
 
     @Before
     fun setUp() {
-        progressProvider = PhysicsBasedUnfoldTransitionProgressProvider(
-            foldStateProvider
-        )
+        progressProvider = PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
         progressProvider.addCallback(listener)
     }
 
@@ -56,7 +49,7 @@
         runOnMainThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
             { foldStateProvider.sendHingeAngleUpdate(10f) },
-            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() },
             { foldStateProvider.sendHingeAngleUpdate(90f) },
             { foldStateProvider.sendHingeAngleUpdate(180f) },
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
@@ -73,15 +66,13 @@
         runOnMainThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
             { foldStateProvider.sendHingeAngleUpdate(10f) },
-            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() },
             { foldStateProvider.sendHingeAngleUpdate(90f) },
             { foldStateProvider.sendHingeAngleUpdate(180f) },
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
         )
 
-        with(listener.ensureTransitionFinished()) {
-            assertHasSingleFinishingEvent()
-        }
+        with(listener.ensureTransitionFinished()) { assertHasSingleFinishingEvent() }
     }
 
     @Test
@@ -92,7 +83,7 @@
             { foldStateProvider.sendHingeAngleUpdate(90f) },
             { foldStateProvider.sendHingeAngleUpdate(180f) },
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
-            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() },
         )
 
         with(listener.ensureTransitionFinished()) {
@@ -121,7 +112,7 @@
     fun testUnfoldAndStopUnfolding_finishesTheUnfoldTransition() {
         runOnMainThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
-            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() },
             { foldStateProvider.sendHingeAngleUpdate(10f) },
             { foldStateProvider.sendHingeAngleUpdate(90f) },
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN) },
@@ -137,7 +128,7 @@
     fun testFoldImmediatelyAfterUnfold_runsFoldAnimation() {
         runOnMainThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
-            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() },
             { foldStateProvider.sendHingeAngleUpdate(10f) },
             { foldStateProvider.sendHingeAngleUpdate(90f) },
             {
@@ -150,106 +141,12 @@
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) },
         )
 
-        with(listener.ensureTransitionFinished()) {
-            assertHasFoldAnimationAtTheEnd()
-        }
-    }
-
-    private class TestUnfoldProgressListener : TransitionProgressListener {
-
-        private val recordings: MutableList<UnfoldTransitionRecording> = arrayListOf()
-        private var currentRecording: UnfoldTransitionRecording? = null
-
-        override fun onTransitionStarted() {
-            assertWithMessage("Trying to start a transition when it is already in progress")
-                .that(currentRecording).isNull()
-
-            currentRecording = UnfoldTransitionRecording()
-        }
-
-        override fun onTransitionProgress(progress: Float) {
-            assertWithMessage("Received transition progress event when it's not started")
-                .that(currentRecording).isNotNull()
-            currentRecording!!.addProgress(progress)
-        }
-
-        override fun onTransitionFinishing() {
-            assertWithMessage("Received transition finishing event when it's not started")
-                    .that(currentRecording).isNotNull()
-            currentRecording!!.onFinishing()
-        }
-
-        override fun onTransitionFinished() {
-            assertWithMessage("Received transition finish event when it's not started")
-                .that(currentRecording).isNotNull()
-            recordings += currentRecording!!
-            currentRecording = null
-        }
-
-        fun ensureTransitionFinished(): UnfoldTransitionRecording {
-            waitForCondition { recordings.size == 1 }
-            return recordings.first()
-        }
-
-        class UnfoldTransitionRecording {
-            private val progressHistory: MutableList<Float> = arrayListOf()
-            private var finishingInvocations: Int = 0
-
-            fun addProgress(progress: Float) {
-                assertThat(progress).isAtMost(1.0f)
-                assertThat(progress).isAtLeast(0.0f)
-
-                progressHistory += progress
-            }
-
-            fun onFinishing() {
-                finishingInvocations++
-            }
-
-            fun assertIncreasingProgress() {
-                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
-                assertThat(progressHistory).isInOrder()
-            }
-
-            fun assertDecreasingProgress() {
-                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
-                assertThat(progressHistory).isInOrder(Comparator.reverseOrder<Float>())
-            }
-
-            fun assertFinishedWithUnfold() {
-                assertThat(progressHistory).isNotEmpty()
-                assertThat(progressHistory.last()).isEqualTo(1.0f)
-            }
-
-            fun assertFinishedWithFold() {
-                assertThat(progressHistory).isNotEmpty()
-                assertThat(progressHistory.last()).isEqualTo(0.0f)
-            }
-
-            fun assertHasFoldAnimationAtTheEnd() {
-                // Check that there are at least a few decreasing events at the end
-                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
-                assertThat(progressHistory.takeLast(MIN_ANIMATION_EVENTS))
-                    .isInOrder(Comparator.reverseOrder<Float>())
-                assertThat(progressHistory.last()).isEqualTo(0.0f)
-            }
-
-            fun assertHasSingleFinishingEvent() {
-                assertWithMessage("onTransitionFinishing callback should be invoked exactly " +
-                        "one time").that(finishingInvocations).isEqualTo(1)
-            }
-        }
-
-        private companion object {
-            private const val MIN_ANIMATION_EVENTS = 5
-        }
+        with(listener.ensureTransitionFinished()) { assertHasFoldAnimationAtTheEnd() }
     }
 
     private fun runOnMainThreadWithInterval(vararg blocks: () -> Unit, intervalMillis: Long = 60) {
         blocks.forEach {
-            InstrumentationRegistry.getInstrumentation().runOnMainSync {
-                it()
-            }
+            InstrumentationRegistry.getInstrumentation().runOnMainSync { it() }
             Thread.sleep(intervalMillis)
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt
new file mode 100644
index 0000000..0e7e039
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.unfold.progress
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class RemoteUnfoldTransitionReceiverTest : SysuiTestCase() {
+
+    private val progressProvider = RemoteUnfoldTransitionReceiver { it.run() }
+    private val listener = TestUnfoldProgressListener()
+
+    @Before
+    fun setUp() {
+        progressProvider.addCallback(listener)
+    }
+
+    @Test
+    fun onTransitionStarted_propagated() {
+        progressProvider.onTransitionStarted()
+
+        listener.assertStarted()
+    }
+
+    @Test
+    fun onTransitionProgress_propagated() {
+        progressProvider.onTransitionStarted()
+
+        progressProvider.onTransitionProgress(0.5f)
+
+        listener.assertLastProgress(0.5f)
+    }
+
+    @Test
+    fun onTransitionEnded_propagated() {
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+
+        progressProvider.onTransitionFinished()
+
+        listener.ensureTransitionFinished()
+    }
+
+    @Test
+    fun onTransitionStarted_afterCallbackRemoved_notPropagated() {
+        progressProvider.removeCallback(listener)
+
+        progressProvider.onTransitionStarted()
+
+        listener.assertNotStarted()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
new file mode 100644
index 0000000..f653207
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.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.unfold.progress
+
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.util.leak.ReferenceTestUtils.waitForCondition
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+
+/** Listener usable by tests with some handy assertions. */
+class TestUnfoldProgressListener : UnfoldTransitionProgressProvider.TransitionProgressListener {
+
+    private val recordings: MutableList<UnfoldTransitionRecording> = arrayListOf()
+    private var currentRecording: UnfoldTransitionRecording? = null
+
+    override fun onTransitionStarted() {
+        assertWithMessage("Trying to start a transition when it is already in progress")
+            .that(currentRecording)
+            .isNull()
+
+        currentRecording = UnfoldTransitionRecording()
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        assertWithMessage("Received transition progress event when it's not started")
+            .that(currentRecording)
+            .isNotNull()
+        currentRecording!!.addProgress(progress)
+    }
+
+    override fun onTransitionFinishing() {
+        assertWithMessage("Received transition finishing event when it's not started")
+            .that(currentRecording)
+            .isNotNull()
+        currentRecording!!.onFinishing()
+    }
+
+    override fun onTransitionFinished() {
+        assertWithMessage("Received transition finish event when it's not started")
+            .that(currentRecording)
+            .isNotNull()
+        recordings += currentRecording!!
+        currentRecording = null
+    }
+
+    fun ensureTransitionFinished(): UnfoldTransitionRecording {
+        waitForCondition { recordings.size == 1 }
+        return recordings.first()
+    }
+
+    fun assertStarted() {
+        assertWithMessage("Transition didn't start").that(currentRecording).isNotNull()
+    }
+
+    fun assertNotStarted() {
+        assertWithMessage("Transition started").that(currentRecording).isNull()
+    }
+
+    fun assertLastProgress(progress: Float) {
+        currentRecording?.assertLastProgress(progress) ?: error("unfold not in progress.")
+    }
+
+    class UnfoldTransitionRecording {
+        private val progressHistory: MutableList<Float> = arrayListOf()
+        private var finishingInvocations: Int = 0
+
+        fun addProgress(progress: Float) {
+            assertThat(progress).isAtMost(1.0f)
+            assertThat(progress).isAtLeast(0.0f)
+
+            progressHistory += progress
+        }
+
+        fun onFinishing() {
+            finishingInvocations++
+        }
+
+        fun assertIncreasingProgress() {
+            assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
+            assertThat(progressHistory).isInOrder()
+        }
+
+        fun assertDecreasingProgress() {
+            assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
+            assertThat(progressHistory).isInOrder(Comparator.reverseOrder<Float>())
+        }
+
+        fun assertFinishedWithUnfold() {
+            assertThat(progressHistory).isNotEmpty()
+            assertThat(progressHistory.last()).isEqualTo(1.0f)
+        }
+
+        fun assertFinishedWithFold() {
+            assertThat(progressHistory).isNotEmpty()
+            assertThat(progressHistory.last()).isEqualTo(0.0f)
+        }
+
+        fun assertHasFoldAnimationAtTheEnd() {
+            // Check that there are at least a few decreasing events at the end
+            assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
+            assertThat(progressHistory.takeLast(MIN_ANIMATION_EVENTS))
+                .isInOrder(Comparator.reverseOrder<Float>())
+            assertThat(progressHistory.last()).isEqualTo(0.0f)
+        }
+
+        fun assertHasSingleFinishingEvent() {
+            assertWithMessage(
+                    "onTransitionFinishing callback should be invoked exactly " + "one time"
+                )
+                .that(finishingInvocations)
+                .isEqualTo(1)
+        }
+
+        fun assertLastProgress(progress: Float) {
+            assertThat(progressHistory.last()).isEqualTo(progress)
+        }
+    }
+
+    private companion object {
+        private const val MIN_ANIMATION_EVENTS = 5
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index 6086e16..8476d0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider
 import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
 import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
 import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
@@ -71,6 +72,7 @@
 
     private val foldUpdates: MutableList<Int> = arrayListOf()
     private val hingeAngleUpdates: MutableList<Float> = arrayListOf()
+    private val unfoldedScreenAvailabilityUpdates: MutableList<Unit> = arrayListOf()
 
     private var scheduledRunnable: Runnable? = null
     private var scheduledRunnableDelay: Long? = null
@@ -106,6 +108,10 @@
                 override fun onFoldUpdate(update: Int) {
                     foldUpdates.add(update)
                 }
+
+                override fun onUnfoldedScreenAvailable() {
+                    unfoldedScreenAvailabilityUpdates.add(Unit)
+                }
             })
         foldStateProvider.start()
 
@@ -156,8 +162,8 @@
         sendHingeAngleEvent(10)
         screenOnStatusProvider.notifyScreenTurnedOn()
 
-        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
-                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
+        assertThat(unfoldedScreenAvailabilityUpdates).hasSize(1)
     }
 
     @Test
@@ -174,8 +180,9 @@
         sendHingeAngleEvent(40)
         sendHingeAngleEvent(10)
 
-        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
-                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_START_CLOSING)
+        assertThat(foldUpdates)
+                .containsExactly(FOLD_UPDATE_START_OPENING, FOLD_UPDATE_START_CLOSING)
+        assertThat(unfoldedScreenAvailabilityUpdates).hasSize(1)
     }
 
     @Test
@@ -223,7 +230,7 @@
 
         fireScreenOnEvent()
 
-        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
+        assertThat(unfoldedScreenAvailabilityUpdates).hasSize(1)
     }
 
     @Test
@@ -277,7 +284,7 @@
 
     @Test
     fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() {
-        sendHingeAngleEvent(90)
+        setInitialHingeAngle(90)
         sendHingeAngleEvent(80)
 
         simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS)
@@ -288,7 +295,7 @@
 
     @Test
     fun startClosingEvent_beforeTimeout_abortNotEmitted() {
-        sendHingeAngleEvent(90)
+        setInitialHingeAngle(90)
         sendHingeAngleEvent(80)
 
         simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1)
@@ -298,7 +305,7 @@
 
     @Test
     fun startClosingEvent_eventBeforeTimeout_oneEventEmitted() {
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
         sendHingeAngleEvent(90)
 
         simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1)
@@ -309,7 +316,7 @@
 
     @Test
     fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() {
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
         sendHingeAngleEvent(90)
 
         // The timeout should not trigger here.
@@ -323,7 +330,7 @@
 
     @Test
     fun startClosingEvent_shortTimeBetween_emitsOnlyOneEvents() {
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(90)
         sendHingeAngleEvent(80)
@@ -334,20 +341,19 @@
     @Test
     fun startClosingEvent_whileClosing_emittedDespiteInitialAngle() {
         val maxAngle = 180 - FULLY_OPEN_THRESHOLD_DEGREES.toInt()
-        for (i in 1..maxAngle) {
-            foldUpdates.clear()
-
-            simulateFolding(startAngle = i)
+        val minAngle = Math.ceil(HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toDouble()).toInt() + 1
+        for (startAngle in minAngle..maxAngle) {
+            setInitialHingeAngle(startAngle)
+            sendHingeAngleEvent(startAngle - HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toInt() - 1)
 
             assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
-            simulateTimeout() // Timeout to set the state to aborted.
         }
     }
 
     @Test
     fun startClosingEvent_whileNotOnLauncher_doesNotTriggerBeforeThreshold() {
         setupForegroundActivityType(isHomeActivity = false)
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
 
@@ -357,7 +363,7 @@
     @Test
     fun startClosingEvent_whileActivityTypeNotAvailable_triggerBeforeThreshold() {
         setupForegroundActivityType(isHomeActivity = null)
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
 
@@ -367,7 +373,7 @@
     @Test
     fun startClosingEvent_whileOnLauncher_doesTriggerBeforeThreshold() {
         setupForegroundActivityType(isHomeActivity = true)
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
 
@@ -377,9 +383,11 @@
     @Test
     fun startClosingEvent_whileNotOnLauncher_triggersAfterThreshold() {
         setupForegroundActivityType(isHomeActivity = false)
-        sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES)
+        setInitialHingeAngle(START_CLOSING_ON_APPS_THRESHOLD_DEGREES)
 
-        sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES - 1)
+        sendHingeAngleEvent(
+                START_CLOSING_ON_APPS_THRESHOLD_DEGREES -
+                        HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toInt() - 1)
 
         assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
     }
@@ -388,7 +396,7 @@
     fun startClosingEvent_whileNotOnKeyguardAndNotOnLauncher_doesNotTriggerBeforeThreshold() {
         setKeyguardVisibility(visible = false)
         setupForegroundActivityType(isHomeActivity = false)
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
 
@@ -398,7 +406,7 @@
     @Test
     fun startClosingEvent_whileKeyguardStateNotAvailable_triggerBeforeThreshold() {
         setKeyguardVisibility(visible = null)
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
 
@@ -408,7 +416,7 @@
     @Test
     fun startClosingEvent_whileonKeyguard_doesTriggerBeforeThreshold() {
         setKeyguardVisibility(visible = true)
-        sendHingeAngleEvent(180)
+        setInitialHingeAngle(180)
 
         sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
 
@@ -418,9 +426,59 @@
     @Test
     fun startClosingEvent_whileNotOnKeyguard_triggersAfterThreshold() {
         setKeyguardVisibility(visible = false)
-        sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES)
+        setInitialHingeAngle(START_CLOSING_ON_APPS_THRESHOLD_DEGREES)
 
-        sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES - 1)
+        sendHingeAngleEvent(
+                START_CLOSING_ON_APPS_THRESHOLD_DEGREES -
+                        HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toInt() - 1)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
+    }
+
+    @Test
+    fun startClosingEvent_doesNotTriggerBelowThreshold() {
+        val thresholdAngle = (FULLY_OPEN_DEGREES - FULLY_OPEN_THRESHOLD_DEGREES).toInt()
+        setInitialHingeAngle(180)
+        sendHingeAngleEvent(thresholdAngle + 1)
+
+        assertThat(foldUpdates).isEmpty()
+    }
+
+    @Test
+    fun startClosingEvent_triggersAfterThreshold() {
+        val thresholdAngle = (FULLY_OPEN_DEGREES - FULLY_OPEN_THRESHOLD_DEGREES).toInt()
+        setInitialHingeAngle(180)
+        sendHingeAngleEvent(thresholdAngle + 1)
+        sendHingeAngleEvent(thresholdAngle - 1)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
+    }
+
+    @Test
+    fun startClosingEvent_triggersAfterThreshold_fromHalfOpen() {
+        setInitialHingeAngle(120)
+        sendHingeAngleEvent((120 - HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES + 1).toInt())
+        assertThat(foldUpdates).isEmpty()
+        sendHingeAngleEvent((120 - HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES - 1).toInt())
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
+    }
+
+    @Test
+    fun startOpeningAndClosingEvents_triggerWithOpenAndClose() {
+        setInitialHingeAngle(120)
+        sendHingeAngleEvent(130)
+        sendHingeAngleEvent(120)
+        assertThat(foldUpdates)
+                .containsExactly(FOLD_UPDATE_START_OPENING, FOLD_UPDATE_START_CLOSING)
+    }
+
+    @Test
+    fun startClosingEvent_notInterrupted_whenAngleIsSlightlyIncreased() {
+        setInitialHingeAngle(120)
+        sendHingeAngleEvent(110)
+        sendHingeAngleEvent(111)
+        sendHingeAngleEvent(100)
 
         assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
     }
@@ -504,11 +562,6 @@
         }
     }
 
-    private fun simulateFolding(startAngle: Int) {
-        sendHingeAngleEvent(startAngle)
-        sendHingeAngleEvent(startAngle - 1)
-    }
-
     private fun setFoldState(folded: Boolean) {
         foldProvider.notifyFolded(folded)
     }
@@ -521,6 +574,17 @@
         testHingeAngleProvider.notifyAngle(angle.toFloat())
     }
 
+    private fun setInitialHingeAngle(angle: Int) {
+        setFoldState(angle == 0)
+        sendHingeAngleEvent(angle)
+        if (scheduledRunnableDelay != null) {
+            simulateTimeout()
+        }
+        hingeAngleUpdates.clear()
+        foldUpdates.clear()
+        unfoldedScreenAvailabilityUpdates.clear()
+    }
+
     private class TestFoldProvider : FoldProvider {
         private val callbacks = arrayListOf<FoldCallback>()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt
index a064e8c..fbb0e5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt
@@ -57,4 +57,8 @@
     fun sendHingeAngleUpdate(angle: Float) {
         listeners.forEach { it.onHingeAngleUpdate(angle) }
     }
+
+    fun sendUnfoldedScreenAvailable() {
+        listeners.forEach { it.onUnfoldedScreenAvailable() }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 034c618..ddd880b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -23,6 +23,8 @@
 import android.provider.Settings
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.util.settings.FakeSettings
@@ -40,6 +42,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
@@ -214,6 +217,35 @@
         assertThat(selectedUserInfo?.id).isEqualTo(1)
     }
 
+    @Test
+    fun userSwitchingInProgress_registersUserTrackerCallback() = runSelfCancelingTest {
+        underTest = create(this)
+
+        underTest.userSwitchingInProgress.launchIn(this)
+        underTest.userSwitchingInProgress.launchIn(this)
+        underTest.userSwitchingInProgress.launchIn(this)
+
+        // Two callbacks registered - one for observing user switching and one for observing the
+        // selected user
+        assertThat(tracker.callbacks.size).isEqualTo(2)
+    }
+
+    @Test
+    fun userSwitchingInProgress_propagatesStateFromUserTracker() = runSelfCancelingTest {
+        underTest = create(this)
+        assertThat(tracker.callbacks.size).isEqualTo(2)
+
+        tracker.onUserChanging(0)
+
+        var mostRecentSwitchingValue = false
+        underTest.userSwitchingInProgress.onEach { mostRecentSwitchingValue = it }.launchIn(this)
+
+        assertThat(mostRecentSwitchingValue).isTrue()
+
+        tracker.onUserChanged(0)
+        assertThat(mostRecentSwitchingValue).isFalse()
+    }
+
     private fun createUserInfo(
         id: Int,
         isGuest: Boolean,
@@ -280,6 +312,8 @@
         }
 
     private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+        val featureFlags = FakeFeatureFlags()
+        featureFlags.set(FACE_AUTH_REFACTOR, true)
         return UserRepositoryImpl(
             appContext = context,
             manager = manager,
@@ -288,6 +322,7 @@
             backgroundDispatcher = IMMEDIATE,
             globalSettings = globalSettings,
             tracker = tracker,
+            featureFlags = featureFlags,
         )
     }
 
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 78b0cbe..d00acb8 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
@@ -28,7 +28,6 @@
 import android.os.UserManager
 import android.provider.Settings
 import androidx.test.filters.SmallTest
-import com.android.internal.R.drawable.ic_account_circle
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.GuestResetOrExitSessionReceiver
 import com.android.systemui.GuestResumeSessionReceiver
@@ -36,12 +35,15 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -60,12 +62,12 @@
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+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
@@ -73,16 +75,19 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class UserInteractorTest : SysuiTestCase() {
 
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode
     @Mock private lateinit var activityManager: ActivityManager
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
@@ -90,10 +95,11 @@
     @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+    @Mock private lateinit var commandQueue: CommandQueue
 
     private lateinit var underTest: UserInteractor
 
-    private lateinit var testCoroutineScope: TestCoroutineScope
+    private lateinit var testScope: TestScope
     private lateinit var userRepository: FakeUserRepository
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var telephonyRepository: FakeTelephonyRepository
@@ -112,16 +118,20 @@
             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()
-        testCoroutineScope = TestCoroutineScope()
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         val refreshUsersScheduler =
             RefreshUsersScheduler(
-                applicationScope = testCoroutineScope,
-                mainDispatcher = IMMEDIATE,
+                applicationScope = testScope.backgroundScope,
+                mainDispatcher = testDispatcher,
                 repository = userRepository,
             )
         underTest =
@@ -132,23 +142,27 @@
                 keyguardInteractor =
                     KeyguardInteractor(
                         repository = keyguardRepository,
+                        commandQueue = commandQueue,
+                        featureFlags = featureFlags,
+                        bouncerRepository = FakeKeyguardBouncerRepository(),
                     ),
                 manager = manager,
-                applicationScope = testCoroutineScope,
+                headlessSystemUserMode = headlessSystemUserMode,
+                applicationScope = testScope.backgroundScope,
                 telephonyInteractor =
                     TelephonyInteractor(
                         repository = telephonyRepository,
                     ),
                 broadcastDispatcher = fakeBroadcastDispatcher,
-                backgroundDispatcher = IMMEDIATE,
+                backgroundDispatcher = testDispatcher,
                 activityManager = activityManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor =
                     GuestUserInteractor(
                         applicationContext = context,
-                        applicationScope = testCoroutineScope,
-                        mainDispatcher = IMMEDIATE,
-                        backgroundDispatcher = IMMEDIATE,
+                        applicationScope = testScope.backgroundScope,
+                        mainDispatcher = testDispatcher,
+                        backgroundDispatcher = testDispatcher,
                         manager = manager,
                         repository = userRepository,
                         deviceProvisionedController = deviceProvisionedController,
@@ -164,7 +178,7 @@
 
     @Test
     fun `onRecordSelected - user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -179,7 +193,7 @@
 
     @Test
     fun `onRecordSelected - switch to guest user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -193,7 +207,7 @@
 
     @Test
     fun `onRecordSelected - enter guest mode`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -202,6 +216,7 @@
             whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
 
             underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
+            runCurrent()
 
             verify(dialogShower).dismiss()
             verify(manager).createGuest(any())
@@ -210,7 +225,7 @@
 
     @Test
     fun `onRecordSelected - action`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -224,81 +239,72 @@
 
     @Test
     fun `users - switcher enabled`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            assertUsers(models = value, count = 3, includeGuest = true)
+            val value = collectLastValue(underTest.users)
 
-            job.cancel()
+            assertUsers(models = value(), count = 3, includeGuest = true)
         }
 
     @Test
     fun `users - switches to second user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.users)
             userRepository.setSelectedUserInfo(userInfos[1])
 
-            assertUsers(models = value, count = 2, selectedIndex = 1)
-            job.cancel()
+            assertUsers(models = value(), count = 2, selectedIndex = 1)
         }
 
     @Test
     fun `users - switcher not enabled`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
 
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            assertUsers(models = value, count = 1)
-
-            job.cancel()
+            val value = collectLastValue(underTest.users)
+            assertUsers(models = value(), count = 1)
         }
 
     @Test
     fun selectedUser() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var value: UserModel? = null
-            val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
-            assertUser(value, id = 0, isSelected = true)
+            val value = collectLastValue(underTest.selectedUser)
+            assertUser(value(), id = 0, isSelected = true)
 
             userRepository.setSelectedUserInfo(userInfos[1])
-            assertUser(value, id = 1, isSelected = true)
-
-            job.cancel()
+            assertUser(value(), id = 1, isSelected = true)
         }
 
     @Test
     fun `actions - device unlocked`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            runCurrent()
+
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ENTER_GUEST_MODE,
@@ -307,13 +313,11 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device unlocked - full screen`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
@@ -321,10 +325,9 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ADD_USER,
@@ -333,46 +336,38 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device unlocked user not primary - empty list`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
-            job.cancel()
+            assertThat(value()).isEqualTo(emptyList<UserActionModel>())
         }
 
     @Test
     fun `actions - device unlocked user is guest - empty list`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = true)
             assertThat(userInfos[1].isGuest).isTrue()
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
-            job.cancel()
+            assertThat(value()).isEqualTo(emptyList<UserActionModel>())
         }
 
     @Test
     fun `actions - device locked add from lockscreen set - full list`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -383,10 +378,9 @@
                 )
             )
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ENTER_GUEST_MODE,
@@ -395,13 +389,11 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device locked add from lockscreen set - full list - full screen`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
@@ -413,10 +405,9 @@
                 )
             )
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ADD_USER,
@@ -425,39 +416,33 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device locked - only  manage user is shown`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(true)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
-
-            job.cancel()
+            assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
         }
 
     @Test
     fun `executeAction - add user - dialog shown`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             keyguardRepository.setKeyguardShowing(false)
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
             val dialogShower: UserSwitchDialogController.DialogShower = mock()
 
             underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isEqualTo(
                     ShowDialogRequestModel.ShowAddUserDialog(
                         userHandle = userInfos[0].userHandle,
@@ -468,14 +453,12 @@
                 )
 
             underTest.onDialogShown()
-            assertThat(dialogRequest).isNull()
-
-            job.cancel()
+            assertThat(dialogRequest()).isNull()
         }
 
     @Test
-    fun `executeAction - add supervised user - starts activity`() =
-        runBlocking(IMMEDIATE) {
+    fun `executeAction - add supervised user - dismisses dialog and starts activity`() =
+        testScope.runTest {
             underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
 
             val intentCaptor = kotlinArgumentCaptor<Intent>()
@@ -487,7 +470,7 @@
 
     @Test
     fun `executeAction - navigate to manage users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
 
             val intentCaptor = kotlinArgumentCaptor<Intent>()
@@ -497,7 +480,7 @@
 
     @Test
     fun `executeAction - guest mode`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -505,110 +488,103 @@
             val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
             whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
             val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
-            val showDialogsJob =
-                underTest.dialogShowRequests
-                    .onEach {
-                        dialogRequests.add(it)
-                        if (it != null) {
-                            underTest.onDialogShown()
-                        }
+            backgroundScope.launch {
+                underTest.dialogShowRequests.collect {
+                    dialogRequests.add(it)
+                    if (it != null) {
+                        underTest.onDialogShown()
                     }
-                    .launchIn(this)
-            val dismissDialogsJob =
-                underTest.dialogDismissRequests
-                    .onEach {
-                        if (it != null) {
-                            underTest.onDialogDismissed()
-                        }
+                }
+            }
+            backgroundScope.launch {
+                underTest.dialogDismissRequests.collect {
+                    if (it != null) {
+                        underTest.onDialogDismissed()
                     }
-                    .launchIn(this)
+                }
+            }
 
             underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+            runCurrent()
 
             assertThat(dialogRequests)
                 .contains(
                     ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
                 )
             verify(activityManager).switchUser(guestUserInfo.id)
-
-            showDialogsJob.cancel()
-            dismissDialogsJob.cancel()
         }
 
     @Test
     fun `selectUser - already selected guest re-selected - exit guest dialog`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = true)
             val guestUserInfo = userInfos[1]
             assertThat(guestUserInfo.isGuest).isTrue()
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(guestUserInfo)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             underTest.selectUser(
                 newlySelectedUserId = guestUserInfo.id,
                 dialogShower = dialogShower,
             )
 
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
             verify(dialogShower, never()).dismiss()
-            job.cancel()
         }
 
     @Test
     fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = true)
             val guestUserInfo = userInfos[1]
             assertThat(guestUserInfo.isGuest).isTrue()
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(guestUserInfo)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
 
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
             verify(dialogShower, never()).dismiss()
-            job.cancel()
         }
 
     @Test
     fun `selectUser - not currently guest - switches users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
 
-            assertThat(dialogRequest).isNull()
+            assertThat(dialogRequest()).isNull()
             verify(activityManager).switchUser(userInfos[1].id)
             verify(dialogShower).dismiss()
-            job.cancel()
         }
 
     @Test
     fun `Telephony call state changes - refreshes users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
+            runCurrent()
+
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
             telephonyRepository.setCallState(1)
+            runCurrent()
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `User switched broadcast`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -617,9 +593,11 @@
             val callback2: UserInteractor.UserCallback = mock()
             underTest.addCallback(callback1)
             underTest.addCallback(callback2)
+            runCurrent()
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
             userRepository.setSelectedUserInfo(userInfos[1])
+            runCurrent()
             fakeBroadcastDispatcher.registeredReceivers.forEach {
                 it.onReceive(
                     context,
@@ -627,16 +605,17 @@
                         .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
                 )
             }
+            runCurrent()
 
-            verify(callback1).onUserStateChanged()
-            verify(callback2).onUserStateChanged()
+            verify(callback1, atLeastOnce()).onUserStateChanged()
+            verify(callback2, atLeastOnce()).onUserStateChanged()
             assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `User info changed broadcast`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -649,12 +628,14 @@
                 )
             }
 
+            runCurrent()
+
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `System user unlocked broadcast - refresh users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -667,13 +648,14 @@
                         .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
                 )
             }
+            runCurrent()
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `Non-system user unlocked broadcast - do not refresh users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -691,14 +673,14 @@
 
     @Test
     fun userRecords() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             keyguardRepository.setKeyguardShowing(false)
 
-            testCoroutineScope.advanceUntilIdle()
+            runCurrent()
 
             assertRecords(
                 records = underTest.userRecords.value,
@@ -717,7 +699,7 @@
 
     @Test
     fun userRecordsFullScreen() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
@@ -725,7 +707,7 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             keyguardRepository.setKeyguardShowing(false)
 
-            testCoroutineScope.advanceUntilIdle()
+            runCurrent()
 
             assertRecords(
                 records = underTest.userRecords.value,
@@ -744,7 +726,7 @@
 
     @Test
     fun selectedUserRecord() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
@@ -762,63 +744,54 @@
 
     @Test
     fun `users - secondary user - guest user can be switched to`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserModel>? = null
-            val job = underTest.users.onEach { res = it }.launchIn(this)
-            assertThat(res?.size == 3).isTrue()
-            assertThat(res?.find { it.isGuest }).isNotNull()
-            job.cancel()
+            val res = collectLastValue(underTest.users)
+            assertThat(res()?.size == 3).isTrue()
+            assertThat(res()?.find { it.isGuest }).isNotNull()
         }
 
     @Test
     fun `users - secondary user - no guest action`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { res = it }.launchIn(this)
-            assertThat(res?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
-            job.cancel()
+            val res = collectLastValue(underTest.actions)
+            assertThat(res()?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
         }
 
     @Test
     fun `users - secondary user - no guest user record`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserRecord>? = null
-            val job = underTest.userRecords.onEach { res = it }.launchIn(this)
-            assertThat(res?.find { it.isGuest }).isNull()
-            job.cancel()
+            assertThat(underTest.userRecords.value.find { it.isGuest }).isNull()
         }
 
     @Test
     fun `show user switcher - full screen disabled - shows dialog switcher`() =
-        runBlocking(IMMEDIATE) {
-            var dialogRequest: ShowDialogRequestModel? = null
+        testScope.runTest {
             val expandable = mock<Expandable>()
             underTest.showUserSwitcher(context, expandable)
 
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             // Dialog is shown.
-            assertThat(dialogRequest).isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog)
+            assertThat(dialogRequest())
+                .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable))
 
             underTest.onDialogShown()
-            assertThat(dialogRequest).isNull()
-
-            job.cancel()
+            assertThat(dialogRequest()).isNull()
         }
 
     @Test
@@ -849,8 +822,8 @@
 
     @Test
     fun `users - secondary user - managed profile is not included`() =
-        runBlocking(IMMEDIATE) {
-            var userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
+        testScope.runTest {
+            val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
             userInfos.add(
                 UserInfo(
                     50,
@@ -863,23 +836,63 @@
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserModel>? = null
-            val job = underTest.users.onEach { res = it }.launchIn(this)
-            assertThat(res?.size == 3).isTrue()
-            job.cancel()
+            val res = collectLastValue(underTest.users)
+            assertThat(res()?.size == 3).isTrue()
         }
 
     @Test
     fun `current user is not primary and user switcher is disabled`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
-            var selectedUser: UserModel? = null
-            val job = underTest.selectedUser.onEach { selectedUser = it }.launchIn(this)
-            assertThat(selectedUser).isNotNull()
-            job.cancel()
+            val selectedUser = collectLastValue(underTest.selectedUser)
+            assertThat(selectedUser()).isNotNull()
+        }
+
+    @Test
+    fun userRecords_isActionAndNoUsersUnlocked_actionIsDisabled() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+            whenever(manager.getUserSwitchability(any()))
+                .thenReturn(UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED)
+            val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(
+                UserSwitcherSettingsModel(
+                    isUserSwitcherEnabled = true,
+                    isAddUsersFromLockscreen = true
+                )
+            )
+
+            runCurrent()
+            underTest.userRecords.value
+                .filter { it.info == null }
+                .forEach { action -> assertThat(action.isSwitchToEnabled).isFalse() }
+        }
+
+    @Test
+    fun userRecords_isActionAndNoUsersUnlocked_actionIsDisabled_HeadlessMode() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+            whenever(headlessSystemUserMode.isHeadlessSystemUserMode()).thenReturn(true)
+            whenever(manager.isUserUnlocked(anyInt())).thenReturn(false)
+            val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(
+                UserSwitcherSettingsModel(
+                    isUserSwitcherEnabled = true,
+                    isAddUsersFromLockscreen = true
+                )
+            )
+
+            runCurrent()
+            underTest.userRecords.value
+                .filter { it.info == null }
+                .forEach { action -> assertThat(action.isSwitchToEnabled).isFalse() }
         }
 
     private fun assertUsers(
@@ -1017,7 +1030,6 @@
     }
 
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
         private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
         private val GUEST_ICON: Drawable = mock()
         private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
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 108fa62..22fc32a 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
@@ -31,15 +31,18 @@
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.util.mockito.mock
@@ -70,17 +73,18 @@
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var activityManager: ActivityManager
     @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+    @Mock private lateinit var commandQueue: CommandQueue
 
     private lateinit var underTest: StatusBarUserChipViewModel
 
     private val userRepository = FakeUserRepository()
     private val keyguardRepository = FakeKeyguardRepository()
-    private val featureFlags = FakeFeatureFlags()
     private lateinit var guestUserInteractor: GuestUserInteractor
     private lateinit var refreshUsersScheduler: RefreshUsersScheduler
 
@@ -231,6 +235,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 =
@@ -241,10 +250,13 @@
                     keyguardInteractor =
                         KeyguardInteractor(
                             repository = keyguardRepository,
+                            commandQueue = commandQueue,
+                            featureFlags = featureFlags,
+                            bouncerRepository = FakeKeyguardBouncerRepository(),
                         ),
-                    featureFlags =
-                        FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) },
+                    featureFlags = featureFlags,
                     manager = manager,
+                    headlessSystemUserMode = headlessSystemUserMode,
                     applicationScope = testScope.backgroundScope,
                     telephonyInteractor =
                         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 784a26b..a2bd8d3 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
@@ -29,17 +29,20 @@
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
@@ -71,11 +74,13 @@
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var activityManager: ActivityManager
     @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+    @Mock private lateinit var commandQueue: CommandQueue
 
     private lateinit var underTest: UserSwitcherViewModel
 
@@ -132,6 +137,11 @@
                 resetOrExitSessionReceiver = resetOrExitSessionReceiver,
             )
 
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
         underTest =
             UserSwitcherViewModel.Factory(
                     userInteractor =
@@ -142,12 +152,13 @@
                             keyguardInteractor =
                                 KeyguardInteractor(
                                     repository = keyguardRepository,
+                                    commandQueue = commandQueue,
+                                    featureFlags = featureFlags,
+                                    bouncerRepository = FakeKeyguardBouncerRepository(),
                                 ),
-                            featureFlags =
-                                FakeFeatureFlags().apply {
-                                    set(Flags.FULL_SCREEN_USER_SWITCHER, false)
-                                },
+                            featureFlags = featureFlags,
                             manager = manager,
+                            headlessSystemUserMode = headlessSystemUserMode,
                             applicationScope = testScope.backgroundScope,
                             telephonyInteractor =
                                 TelephonyInteractor(
@@ -169,273 +180,295 @@
     }
 
     @Test
-    fun users() = testScope.runTest {
-        val userInfos =
-            listOf(
-                UserInfo(
-                    /* id= */ 0,
-                    /* name= */ "zero",
-                    /* iconPath= */ "",
-                    /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
-                    UserManager.USER_TYPE_FULL_SYSTEM,
-                ),
-                UserInfo(
-                    /* id= */ 1,
-                    /* name= */ "one",
-                    /* iconPath= */ "",
-                    /* flags= */ UserInfo.FLAG_FULL,
-                    UserManager.USER_TYPE_FULL_SYSTEM,
-                ),
-                UserInfo(
-                    /* id= */ 2,
-                    /* name= */ "two",
-                    /* iconPath= */ "",
-                    /* flags= */ UserInfo.FLAG_FULL,
-                    UserManager.USER_TYPE_FULL_SYSTEM,
-                ),
-            )
-        userRepository.setUserInfos(userInfos)
-        userRepository.setSelectedUserInfo(userInfos[0])
-
-        val userViewModels = mutableListOf<List<UserViewModel>>()
-        val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
-
-        assertThat(userViewModels.last()).hasSize(3)
-        assertUserViewModel(
-            viewModel = userViewModels.last()[0],
-            viewKey = 0,
-            name = Text.Loaded("zero"),
-            isSelectionMarkerVisible = true,
-        )
-        assertUserViewModel(
-            viewModel = userViewModels.last()[1],
-            viewKey = 1,
-            name = Text.Loaded("one"),
-            isSelectionMarkerVisible = false,
-        )
-        assertUserViewModel(
-            viewModel = userViewModels.last()[2],
-            viewKey = 2,
-            name = Text.Loaded("two"),
-            isSelectionMarkerVisible = false,
-        )
-        job.cancel()
-    }
-
-    @Test
-    fun `maximumUserColumns - few users`() = testScope.runTest {
-        setUsers(count = 2)
-        val values = mutableListOf<Int>()
-        val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
-
-        assertThat(values.last()).isEqualTo(4)
-
-        job.cancel()
-    }
-
-    @Test
-    fun `maximumUserColumns - many users`() = testScope.runTest {
-        setUsers(count = 5)
-        val values = mutableListOf<Int>()
-        val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
-
-        assertThat(values.last()).isEqualTo(3)
-        job.cancel()
-    }
-
-    @Test
-    fun `isOpenMenuButtonVisible - has actions - true`() = testScope.runTest {
-        setUsers(2)
-
-        val isVisible = mutableListOf<Boolean>()
-        val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
-
-        assertThat(isVisible.last()).isTrue()
-        job.cancel()
-    }
-
-    @Test
-    fun `isOpenMenuButtonVisible - no actions - false`() = testScope.runTest {
-        val userInfos = setUsers(2)
-        userRepository.setSelectedUserInfo(userInfos[1])
-        keyguardRepository.setKeyguardShowing(true)
-        whenever(manager.canAddMoreUsers(any())).thenReturn(false)
-
-        val isVisible = mutableListOf<Boolean>()
-        val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
-
-        assertThat(isVisible.last()).isFalse()
-        job.cancel()
-    }
-
-    @Test
-    fun menu() = testScope.runTest {
-        val isMenuVisible = mutableListOf<Boolean>()
-        val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) }
-        assertThat(isMenuVisible.last()).isFalse()
-
-        underTest.onOpenMenuButtonClicked()
-        assertThat(isMenuVisible.last()).isTrue()
-
-        underTest.onMenuClosed()
-        assertThat(isMenuVisible.last()).isFalse()
-
-        job.cancel()
-    }
-
-    @Test
-    fun `menu actions`() = testScope.runTest {
-        setUsers(2)
-        val actions = mutableListOf<List<UserActionViewModel>>()
-        val job = launch(testDispatcher) { underTest.menu.toList(actions) }
-
-        assertThat(actions.last().map { it.viewKey })
-            .isEqualTo(
+    fun users() =
+        testScope.runTest {
+            val userInfos =
                 listOf(
-                    UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
-                    UserActionModel.ADD_USER.ordinal.toLong(),
-                    UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
-                    UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+                    UserInfo(
+                        /* id= */ 0,
+                        /* name= */ "zero",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_PRIMARY or
+                            UserInfo.FLAG_ADMIN or
+                            UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
+                    UserInfo(
+                        /* id= */ 1,
+                        /* name= */ "one",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
+                    UserInfo(
+                        /* id= */ 2,
+                        /* name= */ "two",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
                 )
-            )
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
 
-        job.cancel()
-    }
+            val userViewModels = mutableListOf<List<UserViewModel>>()
+            val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
 
-    @Test
-    fun `isFinishRequested - finishes when user is switched`() = testScope.runTest {
-        val userInfos = setUsers(count = 2)
-        val isFinishRequested = mutableListOf<Boolean>()
-        val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
-        assertThat(isFinishRequested.last()).isFalse()
-
-        userRepository.setSelectedUserInfo(userInfos[1])
-
-        assertThat(isFinishRequested.last()).isTrue()
-
-        job.cancel()
-    }
-
-    @Test
-    fun `isFinishRequested - finishes when the screen turns off`() = testScope.runTest {
-        setUsers(count = 2)
-        powerRepository.setInteractive(true)
-        val isFinishRequested = mutableListOf<Boolean>()
-        val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
-        assertThat(isFinishRequested.last()).isFalse()
-
-        powerRepository.setInteractive(false)
-
-        assertThat(isFinishRequested.last()).isTrue()
-
-        job.cancel()
-    }
-
-    @Test
-    fun `isFinishRequested - finishes when cancel button is clicked`() = testScope.runTest {
-        setUsers(count = 2)
-        powerRepository.setInteractive(true)
-        val isFinishRequested = mutableListOf<Boolean>()
-        val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
-        assertThat(isFinishRequested.last()).isFalse()
-
-        underTest.onCancelButtonClicked()
-
-        assertThat(isFinishRequested.last()).isTrue()
-
-        underTest.onFinished()
-
-        assertThat(isFinishRequested.last()).isFalse()
-
-        job.cancel()
-    }
-
-    @Test
-    fun `guest selected -- name is exit guest`() = testScope.runTest {
-        val userInfos =
-                listOf(
-                        UserInfo(
-                                /* id= */ 0,
-                                /* name= */ "zero",
-                                /* iconPath= */ "",
-                                /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
-                                UserManager.USER_TYPE_FULL_SYSTEM,
-                        ),
-                        UserInfo(
-                                /* id= */ 1,
-                                /* name= */ "one",
-                                /* iconPath= */ "",
-                                /* flags= */ UserInfo.FLAG_FULL,
-                                UserManager.USER_TYPE_FULL_GUEST,
-                        ),
-                )
-
-        userRepository.setUserInfos(userInfos)
-        userRepository.setSelectedUserInfo(userInfos[1])
-
-        val userViewModels = mutableListOf<List<UserViewModel>>()
-        val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
-
-        assertThat(userViewModels.last()).hasSize(2)
-        assertUserViewModel(
-                viewModel = userViewModels.last()[0],
-                viewKey = 0,
-                name = Text.Loaded("zero"),
-                isSelectionMarkerVisible = false,
-        )
-        assertUserViewModel(
-                viewModel = userViewModels.last()[1],
-                viewKey = 1,
-                name = Text.Resource(
-                    com.android.settingslib.R.string.guest_exit_quick_settings_button
-                ),
-                isSelectionMarkerVisible = true,
-        )
-        job.cancel()
-    }
-
-    @Test
-    fun `guest not selected -- name is guest`() = testScope.runTest {
-        val userInfos =
-                listOf(
-                        UserInfo(
-                                /* id= */ 0,
-                                /* name= */ "zero",
-                                /* iconPath= */ "",
-                                /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
-                                UserManager.USER_TYPE_FULL_SYSTEM,
-                        ),
-                        UserInfo(
-                                /* id= */ 1,
-                                /* name= */ "one",
-                                /* iconPath= */ "",
-                                /* flags= */ UserInfo.FLAG_FULL,
-                                UserManager.USER_TYPE_FULL_GUEST,
-                        ),
-                )
-
-        userRepository.setUserInfos(userInfos)
-        userRepository.setSelectedUserInfo(userInfos[0])
-        runCurrent()
-
-        val userViewModels = mutableListOf<List<UserViewModel>>()
-        val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
-
-        assertThat(userViewModels.last()).hasSize(2)
-        assertUserViewModel(
+            assertThat(userViewModels.last()).hasSize(3)
+            assertUserViewModel(
                 viewModel = userViewModels.last()[0],
                 viewKey = 0,
                 name = Text.Loaded("zero"),
                 isSelectionMarkerVisible = true,
-        )
-        assertUserViewModel(
+            )
+            assertUserViewModel(
                 viewModel = userViewModels.last()[1],
                 viewKey = 1,
                 name = Text.Loaded("one"),
                 isSelectionMarkerVisible = false,
-        )
-        job.cancel()
-    }
+            )
+            assertUserViewModel(
+                viewModel = userViewModels.last()[2],
+                viewKey = 2,
+                name = Text.Loaded("two"),
+                isSelectionMarkerVisible = false,
+            )
+            job.cancel()
+        }
+
+    @Test
+    fun `maximumUserColumns - few users`() =
+        testScope.runTest {
+            setUsers(count = 2)
+            val values = mutableListOf<Int>()
+            val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
+
+            assertThat(values.last()).isEqualTo(4)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `maximumUserColumns - many users`() =
+        testScope.runTest {
+            setUsers(count = 5)
+            val values = mutableListOf<Int>()
+            val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
+
+            assertThat(values.last()).isEqualTo(3)
+            job.cancel()
+        }
+
+    @Test
+    fun `isOpenMenuButtonVisible - has actions - true`() =
+        testScope.runTest {
+            setUsers(2)
+
+            val isVisible = mutableListOf<Boolean>()
+            val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
+
+            assertThat(isVisible.last()).isTrue()
+            job.cancel()
+        }
+
+    @Test
+    fun `isOpenMenuButtonVisible - no actions - false`() =
+        testScope.runTest {
+            val userInfos = setUsers(2)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            keyguardRepository.setKeyguardShowing(true)
+            whenever(manager.canAddMoreUsers(any())).thenReturn(false)
+
+            val isVisible = mutableListOf<Boolean>()
+            val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
+
+            assertThat(isVisible.last()).isFalse()
+            job.cancel()
+        }
+
+    @Test
+    fun menu() =
+        testScope.runTest {
+            val isMenuVisible = mutableListOf<Boolean>()
+            val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) }
+            assertThat(isMenuVisible.last()).isFalse()
+
+            underTest.onOpenMenuButtonClicked()
+            assertThat(isMenuVisible.last()).isTrue()
+
+            underTest.onMenuClosed()
+            assertThat(isMenuVisible.last()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `menu actions`() =
+        testScope.runTest {
+            setUsers(2)
+            val actions = mutableListOf<List<UserActionViewModel>>()
+            val job = launch(testDispatcher) { underTest.menu.toList(actions) }
+
+            assertThat(actions.last().map { it.viewKey })
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+                        UserActionModel.ADD_USER.ordinal.toLong(),
+                        UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `isFinishRequested - finishes when user is switched`() =
+        testScope.runTest {
+            val userInfos = setUsers(count = 2)
+            val isFinishRequested = mutableListOf<Boolean>()
+            val job =
+                launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+            assertThat(isFinishRequested.last()).isFalse()
+
+            userRepository.setSelectedUserInfo(userInfos[1])
+
+            assertThat(isFinishRequested.last()).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `isFinishRequested - finishes when the screen turns off`() =
+        testScope.runTest {
+            setUsers(count = 2)
+            powerRepository.setInteractive(true)
+            val isFinishRequested = mutableListOf<Boolean>()
+            val job =
+                launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+            assertThat(isFinishRequested.last()).isFalse()
+
+            powerRepository.setInteractive(false)
+
+            assertThat(isFinishRequested.last()).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `isFinishRequested - finishes when cancel button is clicked`() =
+        testScope.runTest {
+            setUsers(count = 2)
+            powerRepository.setInteractive(true)
+            val isFinishRequested = mutableListOf<Boolean>()
+            val job =
+                launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+            assertThat(isFinishRequested.last()).isFalse()
+
+            underTest.onCancelButtonClicked()
+
+            assertThat(isFinishRequested.last()).isTrue()
+
+            underTest.onFinished()
+
+            assertThat(isFinishRequested.last()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `guest selected -- name is exit guest`() =
+        testScope.runTest {
+            val userInfos =
+                listOf(
+                    UserInfo(
+                        /* id= */ 0,
+                        /* name= */ "zero",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_PRIMARY or
+                            UserInfo.FLAG_ADMIN or
+                            UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
+                    UserInfo(
+                        /* id= */ 1,
+                        /* name= */ "one",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_GUEST,
+                    ),
+                )
+
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+
+            val userViewModels = mutableListOf<List<UserViewModel>>()
+            val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
+
+            assertThat(userViewModels.last()).hasSize(2)
+            assertUserViewModel(
+                viewModel = userViewModels.last()[0],
+                viewKey = 0,
+                name = Text.Loaded("zero"),
+                isSelectionMarkerVisible = false,
+            )
+            assertUserViewModel(
+                viewModel = userViewModels.last()[1],
+                viewKey = 1,
+                name =
+                    Text.Resource(
+                        com.android.settingslib.R.string.guest_exit_quick_settings_button
+                    ),
+                isSelectionMarkerVisible = true,
+            )
+            job.cancel()
+        }
+
+    @Test
+    fun `guest not selected -- name is guest`() =
+        testScope.runTest {
+            val userInfos =
+                listOf(
+                    UserInfo(
+                        /* id= */ 0,
+                        /* name= */ "zero",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_PRIMARY or
+                            UserInfo.FLAG_ADMIN or
+                            UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
+                    UserInfo(
+                        /* id= */ 1,
+                        /* name= */ "one",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_GUEST,
+                    ),
+                )
+
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            runCurrent()
+
+            val userViewModels = mutableListOf<List<UserViewModel>>()
+            val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
+
+            assertThat(userViewModels.last()).hasSize(2)
+            assertUserViewModel(
+                viewModel = userViewModels.last()[0],
+                viewKey = 0,
+                name = Text.Loaded("zero"),
+                isSelectionMarkerVisible = true,
+            )
+            assertUserViewModel(
+                viewModel = userViewModels.last()[1],
+                viewKey = 1,
+                name = Text.Loaded("one"),
+                isSelectionMarkerVisible = false,
+            )
+            job.cancel()
+        }
 
     private suspend fun setUsers(count: Int): List<UserInfo> {
         val userInfos =
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..b367a60
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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;
+        }
+
+        public FakeConditionalCoreStartable(Monitor monitor, Callback callback) {
+            super(monitor);
+            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);
+    }
+
+    @Test
+    public void testOnStartCallbackWithNoConditions() {
+        final CoreStartable coreStartable =
+                new FakeConditionalCoreStartable(mMonitor,
+                        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()).isEmpty();
+
+        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/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
index 6bfc2f1..7d0d57b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
@@ -19,9 +19,12 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -35,6 +38,10 @@
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.yield
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -231,6 +238,170 @@
     }
 }
 
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ThrottleFlowTest : SysuiTestCase() {
+
+    @Test
+    fun doesNotAffectEmissions_whenDelayAtLeastEqualToPeriod() = runTest {
+        // Arrange
+        val choreographer = createChoreographer(this)
+        val output = mutableListOf<Int>()
+        val collectJob = backgroundScope.launch {
+            flow {
+                emit(1)
+                delay(1000)
+                emit(2)
+            }.throttle(1000, choreographer.fakeClock).toList(output)
+        }
+
+        // Act
+        choreographer.advanceAndRun(0)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(999)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(1)
+
+        // Assert
+        assertThat(output).containsExactly(1, 2)
+
+        // Cleanup
+        collectJob.cancel()
+    }
+
+    @Test
+    fun delaysEmissions_withShorterThanPeriodDelay_untilPeriodElapses() = runTest {
+        // Arrange
+        val choreographer = createChoreographer(this)
+        val output = mutableListOf<Int>()
+        val collectJob = backgroundScope.launch {
+            flow {
+                emit(1)
+                delay(500)
+                emit(2)
+            }.throttle(1000, choreographer.fakeClock).toList(output)
+        }
+
+        // Act
+        choreographer.advanceAndRun(0)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(500)
+        choreographer.advanceAndRun(499)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(1)
+
+        // Assert
+        assertThat(output).containsExactly(1, 2)
+
+        // Cleanup
+        collectJob.cancel()
+    }
+
+    @Test
+    fun filtersAllButLastEmission_whenMultipleEmissionsInPeriod() = runTest {
+        // Arrange
+        val choreographer = createChoreographer(this)
+        val output = mutableListOf<Int>()
+        val collectJob = backgroundScope.launch {
+            flow {
+                emit(1)
+                delay(500)
+                emit(2)
+                delay(500)
+                emit(3)
+            }.throttle(1000, choreographer.fakeClock).toList(output)
+        }
+
+        // Act
+        choreographer.advanceAndRun(0)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(500)
+        choreographer.advanceAndRun(499)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(1)
+
+        // Assert
+        assertThat(output).containsExactly(1, 3)
+
+        // Cleanup
+        collectJob.cancel()
+    }
+
+    @Test
+    fun filtersAllButLastEmission_andDelaysIt_whenMultipleEmissionsInShorterThanPeriod() = runTest {
+        // Arrange
+        val choreographer = createChoreographer(this)
+        val output = mutableListOf<Int>()
+        val collectJob = backgroundScope.launch {
+            flow {
+                emit(1)
+                delay(500)
+                emit(2)
+                delay(250)
+                emit(3)
+            }.throttle(1000, choreographer.fakeClock).toList(output)
+        }
+
+        // Act
+        choreographer.advanceAndRun(0)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(500)
+        choreographer.advanceAndRun(250)
+        choreographer.advanceAndRun(249)
+
+        // Assert
+        assertThat(output).containsExactly(1)
+
+        // Act
+        choreographer.advanceAndRun(1)
+
+        // Assert
+        assertThat(output).containsExactly(1, 3)
+
+        // Cleanup
+        collectJob.cancel()
+    }
+
+    private fun createChoreographer(testScope: TestScope) = object {
+        val fakeClock = FakeSystemClock()
+
+        fun advanceAndRun(millis: Long) {
+            fakeClock.advanceTime(millis)
+            testScope.advanceTimeBy(millis)
+            testScope.runCurrent()
+        }
+    }
+}
+
 private fun <T> assertThatFlow(flow: Flow<T>) =
     object {
         suspend fun emitsExactly(vararg emissions: T) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
index d0420f7..729168a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
@@ -22,13 +22,17 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.NotificationManager;
+import android.os.UserHandle;
 
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.After;
 import org.junit.Before;
@@ -48,6 +52,7 @@
     private File mLeakDir;
     private File mLeakDump;
     private File mLeakHprof;
+    private UserTracker mUserTracker;
     private NotificationManager mNotificationManager;
 
     @Before
@@ -56,6 +61,9 @@
         mLeakDump = new File(mLeakDir, LeakReporter.LEAK_DUMP);
         mLeakHprof = new File(mLeakDir, LeakReporter.LEAK_HPROF);
 
+        mUserTracker = mock(UserTracker.class);
+        when(mUserTracker.getUserHandle()).thenReturn(
+                UserHandle.of(ActivityManager.getCurrentUser()));
         mNotificationManager = mock(NotificationManager.class);
         mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
 
@@ -65,7 +73,7 @@
             return null;
         }).when(mLeakDetector).dump(any(), any());
 
-        mLeakReporter = new LeakReporter(mContext, mLeakDetector, "test@example.com");
+        mLeakReporter = new LeakReporter(mContext, mUserTracker, mLeakDetector, "test@example.com");
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index 915ea1a..0663004 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -48,6 +48,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.RingerModeLiveData;
 import com.android.systemui.util.RingerModeTracker;
@@ -101,6 +102,8 @@
     @Mock
     private ActivityManager mActivityManager;
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private DumpManager mDumpManager;
 
 
@@ -113,6 +116,7 @@
         // Initial non-set value
         when(mRingerModeLiveData.getValue()).thenReturn(-1);
         when(mRingerModeInternalLiveData.getValue()).thenReturn(-1);
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
         // Enable group volume adjustments
         mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions,
@@ -124,7 +128,7 @@
                 mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager,
                 mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager,
                 mPackageManager, mWakefullnessLifcycle, mCaptioningManager, mKeyguardManager,
-                mActivityManager, mDumpManager, mCallback);
+                mActivityManager, mUserTracker, mDumpManager, mCallback);
         mVolumeController.setEnableDialogs(true, true);
     }
 
@@ -233,12 +237,13 @@
                 CaptioningManager captioningManager,
                 KeyguardManager keyguardManager,
                 ActivityManager activityManager,
+                UserTracker userTracker,
                 DumpManager dumpManager,
                 C callback) {
             super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager,
                     notificationManager, optionalVibrator, iAudioService, accessibilityManager,
                     packageManager, wakefulnessLifecycle, captioningManager, keyguardManager,
-                    activityManager, dumpManager);
+                    activityManager, userTracker, dumpManager);
             mCallbacks = callback;
 
             ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 0f9988c..d419095 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -16,8 +16,10 @@
 
 package com.android.systemui.volume;
 
+import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
 
+import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -28,6 +30,7 @@
 import android.app.KeyguardManager;
 import android.media.AudioManager;
 import android.os.SystemClock;
+import android.provider.DeviceConfig;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.InputDevice;
@@ -38,6 +41,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
@@ -50,6 +54,9 @@
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.util.DeviceConfigProxyFake;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -72,6 +79,8 @@
     View mDrawerVibrate;
     View mDrawerMute;
     View mDrawerNormal;
+    private DeviceConfigProxyFake mDeviceConfigProxy;
+    private FakeExecutor mExecutor;
 
     @Mock
     VolumeDialogController mVolumeDialogController;
@@ -100,6 +109,9 @@
 
         getContext().addMockSystemService(KeyguardManager.class, mKeyguard);
 
+        mDeviceConfigProxy = new DeviceConfigProxyFake();
+        mExecutor = new FakeExecutor(new FakeSystemClock());
+
         mDialog = new VolumeDialogImpl(
                 getContext(),
                 mVolumeDialogController,
@@ -110,6 +122,8 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
+                mDeviceConfigProxy,
+                mExecutor,
                 mDumpManager
             );
         mDialog.init(0, null);
@@ -128,6 +142,9 @@
                 VolumePrefs.SHOW_RINGER_TOAST_COUNT + 1);
 
         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
+
+        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false);
     }
 
     private State createShellState() {
@@ -297,6 +314,44 @@
                 AudioManager.RINGER_MODE_NORMAL, false);
     }
 
+    /**
+     * Ideally we would look at the ringer ImageView and check its assigned drawable id, but that
+     * API does not exist. So we do the next best thing; we check the cached icon id.
+     */
+    @Test
+    public void notificationVolumeSeparated_theRingerIconChanges() {
+        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false);
+
+        mExecutor.runAllReady(); // for the config change to take effect
+
+        // assert icon is new based on res id
+        assertEquals(mDialog.mVolumeRingerIconDrawableId,
+                R.drawable.ic_speaker_on);
+        assertEquals(mDialog.mVolumeRingerMuteIconDrawableId,
+                R.drawable.ic_speaker_mute);
+    }
+
+    @Test
+    public void notificationVolumeNotSeparated_theRingerIconRemainsTheSame() {
+        mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false);
+
+        mExecutor.runAllReady();
+
+        assertEquals(mDialog.mVolumeRingerIconDrawableId, R.drawable.ic_volume_ringer);
+        assertEquals(mDialog.mVolumeRingerMuteIconDrawableId, R.drawable.ic_volume_ringer_mute);
+    }
+
+    @Test
+    public void testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation() {
+        mDialog.dismissH(DISMISS_REASON_UNKNOWN);
+        // notifyVisible(false) should not be called immediately but only after the dismiss
+        // animation has ended.
+        verify(mVolumeDialogController, times(0)).notifyVisible(false);
+        mDialog.getDialogView().animate().cancel();
+    }
+
 /*
     @Test
     public void testContentDescriptions() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
new file mode 100644
index 0000000..b527861
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.wallet.controller
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.service.quickaccesswallet.WalletCard
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import java.util.ArrayList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
+
+    @Mock private lateinit var walletController: QuickAccessWalletController
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var featureFlags: FeatureFlags
+    @Mock private lateinit var mockContext: Context
+    @Captor private lateinit var broadcastReceiver: ArgumentCaptor<BroadcastReceiver>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(
+                broadcastDispatcher.broadcastFlow<List<String>?>(
+                    any(),
+                    isNull(),
+                    any(),
+                    any(),
+                    any()
+                )
+            )
+            .thenCallRealMethod()
+
+        whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
+            .thenReturn(true)
+
+        whenever(CARD_1.cardId).thenReturn(ID_1)
+        whenever(CARD_2.cardId).thenReturn(ID_2)
+        whenever(CARD_3.cardId).thenReturn(ID_3)
+    }
+
+    @Test
+    fun `state - has wallet cards - received contextual cards`() = runTest {
+        setUpWalletClient(listOf(CARD_1, CARD_2))
+        val latest =
+            collectLastValue(
+                createWalletContextualSuggestionsController(backgroundScope)
+                    .contextualSuggestionCards,
+            )
+
+        runCurrent()
+        verifyRegistered()
+        broadcastReceiver.value.onReceive(
+            mockContext,
+            createContextualCardsIntent(listOf(ID_1, ID_2))
+        )
+
+        assertThat(latest()).containsExactly(CARD_1, CARD_2)
+    }
+
+    @Test
+    fun `state - no wallet cards - received contextual cards`() = runTest {
+        setUpWalletClient(emptyList())
+        val latest =
+            collectLastValue(
+                createWalletContextualSuggestionsController(backgroundScope)
+                    .contextualSuggestionCards,
+            )
+
+        runCurrent()
+        verifyRegistered()
+        broadcastReceiver.value.onReceive(
+            mockContext,
+            createContextualCardsIntent(listOf(ID_1, ID_2))
+        )
+
+        assertThat(latest()).isEmpty()
+    }
+
+    @Test
+    fun `state - has wallet cards - no contextual cards`() = runTest {
+        setUpWalletClient(listOf(CARD_1, CARD_2))
+        val latest =
+            collectLastValue(
+                createWalletContextualSuggestionsController(backgroundScope)
+                    .contextualSuggestionCards,
+            )
+
+        runCurrent()
+        verifyRegistered()
+        broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList()))
+
+        assertThat(latest()).isEmpty()
+    }
+
+    @Test
+    fun `state - wallet cards error`() = runTest {
+        setUpWalletClient(shouldFail = true)
+        val latest =
+            collectLastValue(
+                createWalletContextualSuggestionsController(backgroundScope)
+                    .contextualSuggestionCards,
+            )
+
+        runCurrent()
+        verifyRegistered()
+        broadcastReceiver.value.onReceive(
+            mockContext,
+            createContextualCardsIntent(listOf(ID_1, ID_2))
+        )
+
+        assertThat(latest()).isEmpty()
+    }
+
+    @Test
+    fun `state - no contextual cards extra`() = runTest {
+        setUpWalletClient(listOf(CARD_1, CARD_2))
+        val latest =
+            collectLastValue(
+                createWalletContextualSuggestionsController(backgroundScope)
+                    .contextualSuggestionCards,
+            )
+
+        runCurrent()
+        verifyRegistered()
+        broadcastReceiver.value.onReceive(mockContext, Intent(INTENT_NAME))
+
+        assertThat(latest()).isEmpty()
+    }
+
+    @Test
+    fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest {
+        whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
+            .thenReturn(false)
+        setUpWalletClient(listOf(CARD_1, CARD_2))
+        val latest =
+            collectLastValue(
+                createWalletContextualSuggestionsController(backgroundScope)
+                    .contextualSuggestionCards,
+            )
+
+        runCurrent()
+        verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any())
+        assertThat(latest()).isNull()
+    }
+
+    private fun createWalletContextualSuggestionsController(
+        scope: CoroutineScope
+    ): WalletContextualSuggestionsController {
+        return WalletContextualSuggestionsController(
+            scope,
+            walletController,
+            broadcastDispatcher,
+            featureFlags
+        )
+    }
+
+    private fun verifyRegistered() {
+        verify(broadcastDispatcher)
+            .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any())
+    }
+
+    private fun createContextualCardsIntent(
+        ids: List<String> = emptyList(),
+    ): Intent {
+        val intent = Intent(INTENT_NAME)
+        intent.putStringArrayListExtra("cardIds", ArrayList(ids))
+        return intent
+    }
+
+    private fun setUpWalletClient(
+        cards: List<WalletCard> = emptyList(),
+        shouldFail: Boolean = false
+    ) {
+        whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
+            with(
+                invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback
+            ) {
+                if (shouldFail) {
+                    onWalletCardRetrievalError(mock())
+                } else {
+                    onWalletCardsRetrieved(GetWalletCardsResponse(cards, 0))
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val ID_1: String = "123"
+        private val CARD_1: WalletCard = mock()
+        private const val ID_2: String = "456"
+        private val CARD_2: WalletCard = mock()
+        private const val ID_3: String = "789"
+        private val CARD_3: WalletCard = mock()
+        private val INTENT_NAME: String = "WalletSuggestionsIntent"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
index 0fdcb95..31cce4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
@@ -32,13 +32,13 @@
 import static org.mockito.Mockito.when;
 import static org.mockito.hamcrest.MockitoHamcrest.intThat;
 
+import android.app.ActivityManager;
 import android.app.WallpaperManager;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.ColorSpace;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
-import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Surface;
@@ -49,6 +49,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -81,6 +82,8 @@
     private Surface mSurface;
     @Mock
     private Context mMockContext;
+    @Mock
+    private UserTracker mUserTracker;
 
     @Mock
     private Bitmap mWallpaperBitmap;
@@ -108,13 +111,16 @@
         when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888);
 
         // set up wallpaper manager
-        when(mWallpaperManager.getBitmapAsUser(eq(UserHandle.USER_CURRENT), anyBoolean()))
+        when(mWallpaperManager.getBitmapAsUser(eq(ActivityManager.getCurrentUser()), anyBoolean()))
                 .thenReturn(mWallpaperBitmap);
         when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager);
 
         // set up surface
         when(mSurfaceHolder.getSurface()).thenReturn(mSurface);
         doNothing().when(mSurface).hwuiDestroy();
+
+        // set up UserTracker
+        when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
     }
 
     @Test
@@ -170,7 +176,7 @@
     }
 
     private ImageWallpaper createImageWallpaper() {
-        return new ImageWallpaper(mFakeBackgroundExecutor) {
+        return new ImageWallpaper(mFakeBackgroundExecutor, mUserTracker) {
             @Override
             public Engine onCreateEngine() {
                 return new CanvasEngine() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 388c51f..bc33439 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -24,6 +24,7 @@
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -90,6 +91,8 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
@@ -228,6 +231,8 @@
     private BubbleEntry mBubbleEntryUser11;
     private BubbleEntry mBubbleEntry2User11;
 
+    private Intent mAppBubbleIntent;
+
     @Mock
     private ShellInit mShellInit;
     @Mock
@@ -283,6 +288,8 @@
 
     private TestableLooper mTestableLooper;
 
+    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -323,10 +330,13 @@
         mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry(
                 mNotificationTestHelper.createBubble(handle));
 
+        mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
+        mAppBubbleIntent.setPackage(mContext.getPackageName());
+
         mZenModeConfig.suppressedVisualEffects = 0;
         when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
 
-        mSysUiState = new SysUiState();
+        mSysUiState = new SysUiState(mDisplayTracker);
         mSysUiState.addCallback(sysUiFlags -> {
             mSysUiStateBubblesManageMenuExpanded =
                     (sysUiFlags
@@ -355,7 +365,8 @@
                         mock(Handler.class),
                         mock(NotifPipelineFlags.class),
                         mock(KeyguardNotificationVisibilityProvider.class),
-                        mock(UiEventLogger.class)
+                        mock(UiEventLogger.class),
+                        mock(UserTracker.class)
                 );
         when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
@@ -1630,6 +1641,62 @@
                 any(Bubble.class), anyBoolean(), anyBoolean());
     }
 
+    @Test
+    public void testShowOrHideAppBubble_addsAndExpand() {
+        assertThat(mBubbleController.isStackExpanded()).isFalse();
+        assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull();
+
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+
+        verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true),
+                /* showInShade= */ eq(false));
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+    }
+
+    @Test
+    public void testShowOrHideAppBubble_expandIfCollapsed() {
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleController.collapseStack();
+        assertThat(mBubbleController.isStackExpanded()).isFalse();
+
+        // Calling this while collapsed will expand the app bubble
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+        assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
+    }
+
+    @Test
+    public void testShowOrHideAppBubble_collapseIfSelected() {
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+
+        // Calling this while the app bubble is expanded should collapse the stack
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+        assertThat(mBubbleController.isStackExpanded()).isFalse();
+        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
+    }
+
+    @Test
+    public void testShowOrHideAppBubble_selectIfNotSelected() {
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey());
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+        assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
+    }
+
     /** Creates a bubble using the userId and package. */
     private Bubble createBubble(int userId, String pkg) {
         final UserHandle userHandle = new UserHandle(userId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index e5316bc8..ceee0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -24,6 +24,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
@@ -48,7 +49,8 @@
             Handler mainHandler,
             NotifPipelineFlags flags,
             KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            UserTracker userTracker) {
         super(contentResolver,
                 powerManager,
                 dreamManager,
@@ -61,7 +63,8 @@
                 mainHandler,
                 flags,
                 keyguardNotificationVisibilityProvider,
-                uiEventLogger);
+                uiEventLogger,
+                userTracker);
         mUseHeadsUp = true;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 7ae47b4..45489d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -29,6 +29,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.notetask.NoteTaskInitializer;
+import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -63,6 +64,7 @@
 @RunWith(AndroidJUnit4.class)
 public class WMShellTest extends SysuiTestCase {
     WMShell mWMShell;
+    FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Mock ShellInterface mShellInterface;
     @Mock CommandQueue mCommandQueue;
@@ -100,6 +102,7 @@
                 mProtoTracer,
                 mWakefulnessLifecycle,
                 mUserTracker,
+                mDisplayTracker,
                 mNoteTaskInitializer,
                 mSysUiMainExecutor
         );
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
index 3767fbe..3428553 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
@@ -40,24 +40,49 @@
 public class MemoryTrackingTestCase extends SysuiTestCase {
     private static File sFilesDir = null;
     private static String sLatestTestClassName = null;
+    private static int sHeapCount = 0;
+    private static File sLatestBaselineHeapFile = null;
 
-    @Before public void grabFilesDir() {
+    // Ideally, we would do this in @BeforeClass just once, but we need mContext to get the files
+    // dir, and that does not exist until @Before on each test method.
+    @Before
+    public void grabFilesDir() throws IOException {
+        // This should happen only once per suite
         if (sFilesDir == null) {
             sFilesDir = mContext.getFilesDir();
         }
-        sLatestTestClassName = getClass().getName();
+
+        // This will happen before the first test method in each class
+        if (sLatestTestClassName == null) {
+            sLatestTestClassName = getClass().getName();
+            sLatestBaselineHeapFile = dump("baseline" + (++sHeapCount), "before-test");
+        }
     }
 
     @AfterClass
     public static void dumpHeap() throws IOException {
+        File afterTestHeap = dump(sLatestTestClassName, "after-test");
+        if (sLatestBaselineHeapFile != null && afterTestHeap != null) {
+            Log.w("MEMORY", "To compare heap to baseline (use go/ahat):");
+            Log.w("MEMORY", "  adb pull " + sLatestBaselineHeapFile);
+            Log.w("MEMORY", "  adb pull " + afterTestHeap);
+            Log.w("MEMORY",
+                    "  java -jar ahat.jar --baseline " + sLatestBaselineHeapFile.getName() + " "
+                            + afterTestHeap.getName());
+        }
+        sLatestTestClassName = null;
+    }
+
+    private static File dump(String basename, String heapKind) throws IOException {
         if (sFilesDir == null) {
             Log.e("MEMORY", "Somehow no test cases??");
-            return;
+            return null;
         }
         mockitoTearDown();
-        Log.w("MEMORY", "about to dump heap");
-        File path = new File(sFilesDir, sLatestTestClassName + ".ahprof");
+        Log.w("MEMORY", "about to dump " + heapKind + " heap");
+        File path = new File(sFilesDir, basename + ".ahprof");
         Debug.dumpHprofData(path.getPath());
-        Log.w("MEMORY", "did it!  Location: " + path);
+        Log.w("MEMORY", "Success!  Location: " + path);
+        return path;
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index bf2235a..1bab997 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -132,8 +132,10 @@
 
     @After
     public void SysuiTeardown() {
-        InstrumentationRegistry.registerInstance(mRealInstrumentation,
-                InstrumentationRegistry.getArguments());
+        if (mRealInstrumentation != null) {
+            InstrumentationRegistry.registerInstance(mRealInstrumentation,
+                    InstrumentationRegistry.getArguments());
+        }
         if (TestableLooper.get(this) != null) {
             TestableLooper.get(this).processAllMessages();
             // Must remove static reference to this test object to prevent leak (b/261039202)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
index 990db77..f723a9e5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
@@ -23,14 +23,20 @@
     isUnlocked: Boolean = true,
     isShowingAlternateAuthOnUnlock: Boolean = false,
     interactionJankMonitor: InteractionJankMonitor = mock(InteractionJankMonitor::class.java),
+    isPredictiveBackQsDialogAnim: Boolean = false,
 ): DialogLaunchAnimator {
     return DialogLaunchAnimator(
-        FakeCallback(
-            isUnlocked = isUnlocked,
-            isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
-        ),
-        interactionJankMonitor,
-        fakeLaunchAnimator(),
+        callback =
+            FakeCallback(
+                isUnlocked = isUnlocked,
+                isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
+            ),
+        interactionJankMonitor = interactionJankMonitor,
+        featureFlags =
+            object : AnimationFeatureFlags {
+                override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
+            },
+        launchAnimator = fakeLaunchAnimator(),
         isForTesting = true,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
index 8176dd0..1bdee36 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
@@ -19,9 +19,10 @@
 import android.graphics.Rect
 
 class FakeOverlapDetector : OverlapDetector {
-    var shouldReturn: Boolean = false
+    var shouldReturn: Map<Int, Boolean> = mapOf()
 
     override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean {
-        return shouldReturn
+        return shouldReturn[touchData.pointerId]
+            ?: error("Unexpected PointerId not declared in TestCase currentPointers")
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/condition/SelfExecutingMonitor.java b/packages/SystemUI/tests/utils/src/com/android/systemui/condition/SelfExecutingMonitor.java
new file mode 100644
index 0000000..7ee05d0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/condition/SelfExecutingMonitor.java
@@ -0,0 +1,62 @@
+/*
+ * 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.condition;
+
+import androidx.annotation.NonNull;
+
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+/**
+ * {@link SelfExecutingMonitor} creates a monitor that independently executes its logic through
+ * a {@link FakeExecutor}, which is ran at when a subscription is added and removed.
+ */
+public class SelfExecutingMonitor extends Monitor {
+    private final FakeExecutor mExecutor;
+
+    /**
+     * Default constructor that allows specifying the FakeExecutor to use.
+     */
+    public SelfExecutingMonitor(FakeExecutor executor) {
+        super(executor);
+        mExecutor = executor;
+    }
+
+    @Override
+    public Subscription.Token addSubscription(@NonNull Subscription subscription) {
+        final Subscription.Token result = super.addSubscription(subscription);
+        mExecutor.runAllReady();
+        return result;
+    }
+
+    @Override
+    public void removeSubscription(@NonNull Subscription.Token token) {
+        super.removeSubscription(token);
+        mExecutor.runNextReady();
+    }
+
+    /**
+     * Creates a {@link SelfExecutingMonitor} with a self-managed {@link FakeExecutor}. Use only
+     * for cases where condition state only will be set at when a subscription is added.
+     */
+    public static SelfExecutingMonitor createInstance() {
+        final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+        final FakeExecutor mExecutor = new FakeExecutor(mFakeSystemClock);
+        return new SelfExecutingMonitor(mExecutor);
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt
index b7a8d2e..9b4f496 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt
@@ -18,6 +18,8 @@
 
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
@@ -25,16 +27,35 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 
-/** Collect [flow] in a new [Job] and return a getter for the last collected value. */
+/**
+ * Collect [flow] in a new [Job] and return a getter for the last collected value.
+ * ```
+ * fun myTest() = runTest {
+ *   // ...
+ *   val actual by collectLastValue(underTest.flow)
+ *   assertThat(actual).isEqualTo(expected)
+ * }
+ * ```
+ */
 fun <T> TestScope.collectLastValue(
     flow: Flow<T>,
     context: CoroutineContext = EmptyCoroutineContext,
     start: CoroutineStart = CoroutineStart.DEFAULT,
-): () -> T? {
+): FlowValue<T?> {
     var lastValue: T? = null
     backgroundScope.launch(context, start) { flow.collect { lastValue = it } }
-    return {
+    return FlowValueImpl {
         runCurrent()
         lastValue
     }
 }
+
+/** @see collectLastValue */
+interface FlowValue<T> : ReadOnlyProperty<Any?, T?> {
+    operator fun invoke(): T?
+}
+
+private class FlowValueImpl<T>(private val block: () -> T?) : FlowValue<T> {
+    override operator fun invoke(): T? = block()
+    override fun getValue(thisRef: Any?, property: KProperty<*>): T? = invoke()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/TestCoroutineSchedulerUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/TestCoroutineSchedulerUtils.kt
new file mode 100644
index 0000000..84e2a5c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/TestCoroutineSchedulerUtils.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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.coroutines
+
+import kotlin.time.Duration
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+
+/**
+ * Moves the virtual clock of this dispatcher forward by the specified [Duration].
+ *
+ * @see [TestCoroutineScheduler.advanceTimeBy]
+ */
+fun TestCoroutineScheduler.advanceTimeBy(duration: Duration) {
+    advanceTimeBy(duration.inWholeMilliseconds)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 6c82cef..b94f816e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -38,12 +38,6 @@
         }
     }
 
-    fun set(flag: DeviceConfigBooleanFlag, value: Boolean) {
-        if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
-            notifyFlagChanged(flag)
-        }
-    }
-
     fun set(flag: ResourceBooleanFlag, value: Boolean) {
         if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
             notifyFlagChanged(flag)
@@ -73,7 +67,7 @@
             listeners.forEach { listener ->
                 listener.onFlagChanged(
                     object : FlagListenable.FlagEvent {
-                        override val flagId = flag.id
+                        override val flagName = flag.name
                         override fun requestNoRestart() {}
                     }
                 )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
new file mode 100644
index 0000000..d4b1701
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
+
+class FakeBiometricSettingsRepository : BiometricSettingsRepository {
+
+    private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false)
+    override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow()
+
+    private val _isFaceEnrolled = MutableStateFlow(false)
+    override val isFaceEnrolled: Flow<Boolean>
+        get() = _isFaceEnrolled
+
+    private val _isFaceAuthEnabled = MutableStateFlow(false)
+    override val isFaceAuthenticationEnabled: Flow<Boolean>
+        get() = _isFaceAuthEnabled
+
+    private val _isStrongBiometricAllowed = MutableStateFlow(false)
+    override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow()
+
+    private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false)
+    override val isFingerprintEnabledByDevicePolicy =
+        _isFingerprintEnabledByDevicePolicy.asStateFlow()
+
+    override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
+        get() = flowOf(true)
+
+    fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
+        _isFingerprintEnrolled.value = isFingerprintEnrolled
+    }
+
+    fun setStrongBiometricAllowed(isStrongBiometricAllowed: Boolean) {
+        _isStrongBiometricAllowed.value = isStrongBiometricAllowed
+    }
+
+    fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) {
+        _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy
+    }
+
+    fun setFaceEnrolled(isFaceEnrolled: Boolean) {
+        _isFaceEnrolled.value = isFaceEnrolled
+    }
+
+    fun setIsFaceAuthEnabled(enabled: Boolean) {
+        _isFaceAuthEnabled.value = enabled
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
new file mode 100644
index 0000000..5641832
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.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.keyguard.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepository {
+    private val _isLockedOut = MutableStateFlow<Boolean>(false)
+    override val isLockedOut: StateFlow<Boolean> = _isLockedOut.asStateFlow()
+
+    fun setLockedOut(lockedOut: Boolean) {
+        _isLockedOut.value = lockedOut
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDevicePostureRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDevicePostureRepository.kt
new file mode 100644
index 0000000..914c786
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDevicePostureRepository.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.keyguard.data.repository
+
+import com.android.systemui.keyguard.shared.model.DevicePosture
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDevicePostureRepository : DevicePostureRepository {
+    private val _currentDevicePosture = MutableStateFlow(DevicePosture.UNKNOWN)
+    override val currentDevicePosture: Flow<DevicePosture>
+        get() = _currentDevicePosture
+
+    fun setCurrentPosture(posture: DevicePosture) {
+        _currentDevicePosture.value = posture
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt
new file mode 100644
index 0000000..9cdce20
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Fake implementation of [KeyguardRepository] */
+class FakeKeyguardBouncerRepository : KeyguardBouncerRepository {
+    private val _primaryBouncerVisible = MutableStateFlow(false)
+    override val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+    private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
+    override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+    override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+    private val _primaryBouncerHide = MutableStateFlow(false)
+    override val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+    private val _primaryBouncerStartingToHide = MutableStateFlow(false)
+    override val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+    private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
+    override val primaryBouncerStartingDisappearAnimation =
+        _primaryBouncerDisappearAnimation.asStateFlow()
+    private val _primaryBouncerScrimmed = MutableStateFlow(false)
+    override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+    private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
+    override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
+    private val _keyguardPosition = MutableStateFlow(0f)
+    override val keyguardPosition = _keyguardPosition.asStateFlow()
+    private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
+    override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+    private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
+    override val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+    private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
+    override val showMessage = _showMessage.asStateFlow()
+    private val _resourceUpdateRequests = MutableStateFlow(false)
+    override val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+    override val bouncerPromptReason = 0
+    override val bouncerErrorMessage: CharSequence? = null
+    private val _isAlternateBouncerVisible = MutableStateFlow(false)
+    override val alternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow()
+    override var lastAlternateBouncerVisibleTime: Long = 0L
+    private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false)
+    override val alternateBouncerUIAvailable = _isAlternateBouncerUIAvailable.asStateFlow()
+    private val _sideFpsShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val sideFpsShowing: StateFlow<Boolean> = _sideFpsShowing.asStateFlow()
+
+    override fun setPrimaryScrimmed(isScrimmed: Boolean) {
+        _primaryBouncerScrimmed.value = isScrimmed
+    }
+
+    override fun setPrimaryVisible(isVisible: Boolean) {
+        _primaryBouncerVisible.value = isVisible
+    }
+
+    override fun setAlternateVisible(isVisible: Boolean) {
+        _isAlternateBouncerVisible.value = isVisible
+    }
+
+    override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+        _isAlternateBouncerUIAvailable.value = isAvailable
+    }
+
+    override fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+        _primaryBouncerShow.value = keyguardBouncerModel
+    }
+
+    override fun setPrimaryShowingSoon(showingSoon: Boolean) {
+        _primaryBouncerShowingSoon.value = showingSoon
+    }
+
+    override fun setPrimaryHide(hide: Boolean) {
+        _primaryBouncerHide.value = hide
+    }
+
+    override fun setPrimaryStartingToHide(startingToHide: Boolean) {
+        _primaryBouncerStartingToHide.value = startingToHide
+    }
+
+    override fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+        _primaryBouncerDisappearAnimation.value = runnable
+    }
+
+    override fun setPanelExpansion(panelExpansion: Float) {
+        _panelExpansionAmount.value = panelExpansion
+    }
+
+    override fun setKeyguardPosition(keyguardPosition: Float) {
+        _keyguardPosition.value = keyguardPosition
+    }
+
+    override fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+        _resourceUpdateRequests.value = willUpdateResources
+    }
+
+    override fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+        _showMessage.value = bouncerShowMessageModel
+    }
+
+    override fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+        _keyguardAuthenticated.value = keyguardAuthenticated
+    }
+
+    override fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+        _isBackButtonEnabled.value = isBackButtonEnabled
+    }
+
+    override fun setSideFpsShowing(isShowing: Boolean) {
+        _sideFpsShowing.value = isShowing
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 39d2eca..194ed02 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -29,6 +29,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 
 /** Fake implementation of [KeyguardRepository] */
 class FakeKeyguardRepository : KeyguardRepository {
@@ -46,12 +47,18 @@
     private val _isKeyguardShowing = MutableStateFlow(false)
     override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
 
+    private val _isKeyguardUnlocked = MutableStateFlow(false)
+    override val isKeyguardUnlocked: Flow<Boolean> = _isKeyguardUnlocked
+
     private val _isKeyguardOccluded = MutableStateFlow(false)
     override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
 
     private val _isDozing = MutableStateFlow(false)
     override val isDozing: Flow<Boolean> = _isDozing
 
+    private val _isAodAvailable = MutableStateFlow(false)
+    override val isAodAvailable: Flow<Boolean> = _isAodAvailable
+
     private val _isDreaming = MutableStateFlow(false)
     override val isDreaming: Flow<Boolean> = _isDreaming
 
@@ -80,9 +87,6 @@
 
     private val _isUdfpsSupported = MutableStateFlow(false)
 
-    private val _isBouncerShowing = MutableStateFlow(false)
-    override val isBouncerShowing: Flow<Boolean> = _isBouncerShowing
-
     private val _isKeyguardGoingAway = MutableStateFlow(false)
     override val isKeyguardGoingAway: Flow<Boolean> = _isKeyguardGoingAway
 
@@ -98,6 +102,13 @@
     private val _biometricUnlockSource = MutableStateFlow<BiometricUnlockSource?>(null)
     override val biometricUnlockSource: Flow<BiometricUnlockSource?> = _biometricUnlockSource
 
+    private val _isQuickSettingsVisible = MutableStateFlow(false)
+    override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
+
+    override fun setQuickSettingsVisible(isVisible: Boolean) {
+        _isQuickSettingsVisible.value = isVisible
+    }
+
     override fun isKeyguardShowing(): Boolean {
         return _isKeyguardShowing.value
     }
@@ -126,6 +137,10 @@
         _isDozing.value = isDozing
     }
 
+    fun setAodAvailable(isAodAvailable: Boolean) {
+        _isAodAvailable.value = isAodAvailable
+    }
+
     fun setDreamingWithOverlay(isDreaming: Boolean) {
         _isDreamingWithOverlay.value = isDreaming
     }
@@ -138,10 +153,6 @@
         _wakefulnessModel.value = model
     }
 
-    fun setBouncerShowing(isShowing: Boolean) {
-        _isBouncerShowing.value = isShowing
-    }
-
     fun setBiometricUnlockState(state: BiometricUnlockModel) {
         _biometricUnlockState.tryEmit(state)
     }
@@ -162,6 +173,10 @@
         _dozeTransitionModel.value = model
     }
 
+    fun setStatusBarState(state: StatusBarState) {
+        _statusBarState.value = state
+    }
+
     override fun isUdfpsSupported(): Boolean {
         return _isUdfpsSupported.value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 6c44244..16442bb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -22,20 +22,22 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import java.util.UUID
+import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 
 /** Fake implementation of [KeyguardTransitionRepository] */
 class FakeKeyguardTransitionRepository : KeyguardTransitionRepository {
 
-    private val _transitions = MutableSharedFlow<TransitionStep>()
+    private val _transitions =
+        MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val transitions: SharedFlow<TransitionStep> = _transitions
 
     suspend fun sendTransitionStep(step: TransitionStep) {
         _transitions.emit(step)
     }
 
-    override fun startTransition(info: TransitionInfo): UUID? {
+    override fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean): UUID? {
         return null
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
new file mode 100644
index 0000000..1403cea
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.settings
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.view.Display
+import java.util.concurrent.Executor
+
+class FakeDisplayTracker constructor(val context: Context) : DisplayTracker {
+    val displayManager: DisplayManager = context.getSystemService(DisplayManager::class.java)!!
+    override var defaultDisplayId: Int = Display.DEFAULT_DISPLAY
+    override var allDisplays: Array<Display> = displayManager.displays
+
+    private val displayCallbacks: MutableList<DisplayTracker.Callback> = ArrayList()
+    private val brightnessCallbacks: MutableList<DisplayTracker.Callback> = ArrayList()
+    override fun addDisplayChangeCallback(callback: DisplayTracker.Callback, executor: Executor) {
+        displayCallbacks.add(callback)
+    }
+    override fun addBrightnessChangeCallback(
+        callback: DisplayTracker.Callback,
+        executor: Executor
+    ) {
+        brightnessCallbacks.add(callback)
+    }
+
+    override fun removeCallback(callback: DisplayTracker.Callback) {
+        displayCallbacks.remove(callback)
+        brightnessCallbacks.remove(callback)
+    }
+
+    fun setDefaultDisplay(displayId: Int) {
+        defaultDisplayId = displayId
+    }
+
+    fun setDisplays(displays: Array<Display>) {
+        allDisplays = displays
+    }
+
+    fun triggerOnDisplayAdded(displayId: Int) {
+        notifyCallbacks({ onDisplayAdded(displayId) }, displayCallbacks)
+    }
+
+    fun triggerOnDisplayRemoved(displayId: Int) {
+        notifyCallbacks({ onDisplayRemoved(displayId) }, displayCallbacks)
+    }
+
+    fun triggerOnDisplayChanged(displayId: Int) {
+        notifyCallbacks({ onDisplayChanged(displayId) }, displayCallbacks)
+    }
+
+    fun triggerOnDisplayBrightnessChanged(displayId: Int) {
+        notifyCallbacks({ onDisplayChanged(displayId) }, brightnessCallbacks)
+    }
+
+    private inline fun notifyCallbacks(
+        crossinline action: DisplayTracker.Callback.() -> Unit,
+        list: List<DisplayTracker.Callback>
+    ) {
+        list.forEach { it.action() }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index 0dd1fc7..4242c16 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -22,6 +22,7 @@
 import android.os.UserHandle
 import android.test.mock.MockContentResolver
 import com.android.systemui.util.mockito.mock
+import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 
 /** A fake [UserTracker] to be used in tests. */
@@ -66,8 +67,19 @@
         _userId = _userInfo.id
         _userHandle = UserHandle.of(_userId)
 
+        onUserChanging()
+        onUserChanged()
+    }
+
+    fun onUserChanging(userId: Int = _userId) {
         val copy = callbacks.toList()
-        copy.forEach { it.onUserChanged(_userId, userContext) }
+        val latch = CountDownLatch(copy.size)
+        copy.forEach { it.onUserChanging(userId, userContext, latch) }
+    }
+
+    fun onUserChanged(userId: Int = _userId) {
+        val copy = callbacks.toList()
+        copy.forEach { it.onUserChanged(userId, userContext) }
     }
 
     fun onProfileChanged() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
index 045e6f1..7bcad45 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar;
 
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
+
 import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -57,6 +59,7 @@
     private ShortcutInfo mShortcutInfo = null;
     private int mRankingAdjustment = 0;
     private boolean mIsBubble = false;
+    private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
 
     public RankingBuilder() {
     }
@@ -86,6 +89,7 @@
         mShortcutInfo = ranking.getConversationShortcutInfo();
         mRankingAdjustment = ranking.getRankingAdjustment();
         mIsBubble = ranking.isBubble();
+        mProposedImportance = ranking.getProposedImportance();
     }
 
     public Ranking build() {
@@ -114,7 +118,8 @@
                 mIsConversation,
                 mShortcutInfo,
                 mRankingAdjustment,
-                mIsBubble);
+                mIsBubble,
+                mProposedImportance);
         return ranking;
     }
 
@@ -214,6 +219,11 @@
         return this;
     }
 
+    public RankingBuilder setProposedImportance(@Importance int importance) {
+        mProposedImportance = importance;
+        return this;
+    }
+
     public RankingBuilder setUserSentiment(int userSentiment) {
         mUserSentiment = userSentiment;
         return this;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index ea5a302..1a8e244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -39,6 +39,10 @@
     private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
     override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
 
+    private val _userSwitchingInProgress = MutableStateFlow(false)
+    override val userSwitchingInProgress: Flow<Boolean>
+        get() = _userSwitchingInProgress
+
     override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
 
     private var _isGuestUserAutoCreated: Boolean = false
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
index 4a881a7..fd1b8e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
@@ -41,7 +41,7 @@
     }
 
     override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
-        return data.getOrDefault(key, defValues) as? MutableSet<String>?
+        return (data.getOrDefault(key, defValues) as? Set<String>?)?.toMutableSet()
     }
 
     override fun getInt(key: String, defValue: Int): Int {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
new file mode 100644
index 0000000..0c9ce0f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
@@ -0,0 +1,86 @@
+/*
+ * 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
+
+import android.content.DialogInterface
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.verify
+import org.mockito.stubbing.Stubber
+
+class FakeSystemUIDialogController {
+
+    val dialog: SystemUIDialog = mock()
+
+    private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf()
+
+    init {
+        saveListener(DialogInterface.BUTTON_POSITIVE)
+            .whenever(dialog)
+            .setPositiveButton(any(), any())
+        saveListener(DialogInterface.BUTTON_POSITIVE)
+            .whenever(dialog)
+            .setPositiveButton(any(), any(), any())
+
+        saveListener(DialogInterface.BUTTON_NEGATIVE)
+            .whenever(dialog)
+            .setNegativeButton(any(), any())
+        saveListener(DialogInterface.BUTTON_NEGATIVE)
+            .whenever(dialog)
+            .setNegativeButton(any(), any(), any())
+
+        saveListener(DialogInterface.BUTTON_NEUTRAL).whenever(dialog).setNeutralButton(any(), any())
+        saveListener(DialogInterface.BUTTON_NEUTRAL)
+            .whenever(dialog)
+            .setNeutralButton(any(), any(), any())
+    }
+
+    fun clickNegative() {
+        performClick(DialogInterface.BUTTON_NEGATIVE, "This dialog has no negative button")
+    }
+
+    fun clickPositive() {
+        performClick(DialogInterface.BUTTON_POSITIVE, "This dialog has no positive button")
+    }
+
+    fun clickNeutral() {
+        performClick(DialogInterface.BUTTON_NEUTRAL, "This dialog has no neutral button")
+    }
+
+    fun cancel() {
+        val captor = ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
+        verify(dialog).setOnCancelListener(captor.capture())
+        captor.value.onCancel(dialog)
+    }
+
+    private fun performClick(which: Int, errorMessage: String) {
+        clickListeners
+            .getOrElse(which) { throw IllegalAccessException(errorMessage) }
+            .onClick(dialog, which)
+    }
+
+    private fun saveListener(which: Int): Stubber = doAnswer {
+        val listener = it.getArgument<DialogInterface.OnClickListener>(1)
+        clickListeners[which] = listener
+        Unit
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
index e660e1f2..4b97316 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
@@ -23,6 +23,8 @@
 import android.os.UserHandle;
 import android.util.Pair;
 
+import com.android.systemui.settings.UserTracker;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -57,6 +59,11 @@
     }
 
     @Override
+    public UserTracker getUserTracker() {
+        return null;
+    }
+
+    @Override
     public void registerContentObserverForUser(Uri uri, boolean notifyDescendents,
             ContentObserver settingsObserver, int userHandle) {
         List<ContentObserver> observers;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
index 95b62a1..bdf1aff 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
@@ -49,7 +49,7 @@
     }
 
     @Override
-    public boolean isBouncerShowing() {
+    public boolean isPrimaryBouncerShowing() {
         return false;
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
index 33ef9cf..1baac84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
@@ -46,11 +46,6 @@
     }
 
     @Override
-    public boolean hasEmergencyCryptKeeperText() {
-        return false;
-    }
-
-    @Override
     public boolean isRadioOn() {
         return false;
     }
diff --git a/packages/SystemUI/tools/lint/baseline.xml b/packages/SystemUI/tools/lint/baseline.xml
index 9a2e320..301c9b8 100644
--- a/packages/SystemUI/tools/lint/baseline.xml
+++ b/packages/SystemUI/tools/lint/baseline.xml
@@ -337,17 +337,6 @@
     <issue
         id="Deprecated"
         message="`android:singleLine` is deprecated: Use `maxLines=&quot;1&quot;` instead"
-        errorLine1="        android:singleLine=&quot;true&quot;"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="res/layout/emergency_cryptkeeper_text.xml"
-            line="25"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="Deprecated"
-        message="`android:singleLine` is deprecated: Use `maxLines=&quot;1&quot;` instead"
         errorLine1="                android:singleLine=&quot;true&quot;"
         errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -88997,17 +88986,6 @@
         errorLine1="        android:paddingStart=&quot;6dp&quot;"
         errorLine2="        ~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="res/layout/emergency_cryptkeeper_text.xml"
-            line="24"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="RtlSymmetry"
-        message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry"
-        errorLine1="        android:paddingStart=&quot;6dp&quot;"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~">
-        <location
             file="res/layout/heads_up_status_bar_layout.xml"
             line="43"
             column="9"/>
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt
new file mode 100644
index 0000000..b395d9c
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.unfold
+
+import com.android.systemui.unfold.config.UnfoldTransitionConfig
+import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver
+import com.android.systemui.unfold.util.ATraceLoggerTransitionProgressListener
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+import javax.inject.Provider
+import javax.inject.Singleton
+
+/** Binds classes needed to provide unfold transition progresses to another process. */
+@Module
+class UnfoldRemoteModule {
+    @Provides
+    @Singleton
+    fun provideTransitionProvider(
+        config: UnfoldTransitionConfig,
+        traceListener: ATraceLoggerTransitionProgressListener,
+        remoteReceiverProvider: Provider<RemoteUnfoldTransitionReceiver>,
+    ): Optional<RemoteUnfoldTransitionReceiver> {
+        if (!config.isEnabled) {
+            return Optional.empty()
+        }
+        val remoteReceiver = remoteReceiverProvider.get()
+        remoteReceiver.addCallback(traceListener)
+        return Optional.of(remoteReceiver)
+    }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
index 5a868a4..068347c 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
@@ -22,8 +22,9 @@
 import android.os.Handler
 import android.view.IWindowManager
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
-import com.android.systemui.unfold.dagger.UnfoldBackground
 import com.android.systemui.unfold.dagger.UnfoldMain
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver
 import com.android.systemui.unfold.updates.FoldProvider
 import com.android.systemui.unfold.updates.RotationChangeProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
@@ -58,7 +59,7 @@
             @BindsInstance sensorManager: SensorManager,
             @BindsInstance @UnfoldMain handler: Handler,
             @BindsInstance @UnfoldMain executor: Executor,
-            @BindsInstance @UnfoldBackground backgroundExecutor: Executor,
+            @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor,
             @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
             @BindsInstance windowManager: IWindowManager,
             @BindsInstance contentResolver: ContentResolver = context.contentResolver
@@ -68,3 +69,38 @@
     val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
     val rotationChangeProvider: RotationChangeProvider
 }
+
+/**
+ * Generates a [RemoteTransitionProgress] usable to receive unfold transition progress from another
+ * process.
+ */
+@Singleton
+@Component(modules = [UnfoldRemoteModule::class])
+interface RemoteUnfoldSharedComponent {
+
+    @Component.Factory
+    interface Factory {
+        fun create(
+            @BindsInstance context: Context,
+            @BindsInstance config: UnfoldTransitionConfig,
+            @BindsInstance @UnfoldMain executor: Executor,
+            @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor,
+            @BindsInstance windowManager: IWindowManager,
+            @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
+        ): RemoteUnfoldSharedComponent
+    }
+
+    val remoteTransitionProgress: Optional<RemoteUnfoldTransitionReceiver>
+    val rotationChangeProvider: RotationChangeProvider
+}
+
+/**
+ * Usable to receive and propagate unfold transition progresses
+ *
+ * All unfold events received by [remoteReceiver] will be propagated to [localProvider].
+ * [remoteReceiver] is meant to receive events from a remote process (E.g. from a binder service).
+ */
+data class RemoteTransitionProgress(
+    val localProvider: UnfoldTransitionProgressProvider,
+    val remoteReceiver: UnfoldTransitionProgressProvider.TransitionProgressListener
+)
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
index 3fa5469..5ffc094 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
@@ -16,11 +16,10 @@
 
 package com.android.systemui.unfold
 
-import android.hardware.SensorManager
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
-import com.android.systemui.unfold.dagger.UnfoldBackground
 import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider
 import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
+import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
 import com.android.systemui.unfold.updates.DeviceFoldStateProvider
 import com.android.systemui.unfold.updates.FoldStateProvider
 import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider
@@ -34,55 +33,18 @@
 import dagger.Module
 import dagger.Provides
 import java.util.Optional
-import java.util.concurrent.Executor
+import javax.inject.Provider
 import javax.inject.Singleton
 
-@Module
+@Module(includes = [UnfoldSharedInternalModule::class])
 class UnfoldSharedModule {
     @Provides
     @Singleton
-    fun unfoldTransitionProgressProvider(
-        config: UnfoldTransitionConfig,
-        scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
-        tracingListener: ATraceLoggerTransitionProgressListener,
-        foldStateProvider: FoldStateProvider
-    ): Optional<UnfoldTransitionProgressProvider> =
-        if (!config.isEnabled) {
-            Optional.empty()
-        } else {
-            val baseProgressProvider =
-                if (config.isHingeAngleEnabled) {
-                    PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
-                } else {
-                    FixedTimingTransitionProgressProvider(foldStateProvider)
-                }
-            Optional.of(
-                scaleAwareProviderFactory.wrap(baseProgressProvider).apply {
-                    // Always present callback that logs animation beginning and end.
-                    addCallback(tracingListener)
-                }
-            )
-        }
-
-    @Provides
-    @Singleton
     fun provideFoldStateProvider(
         deviceFoldStateProvider: DeviceFoldStateProvider
     ): FoldStateProvider = deviceFoldStateProvider
 
     @Provides
-    fun hingeAngleProvider(
-        config: UnfoldTransitionConfig,
-        sensorManager: SensorManager,
-        @UnfoldBackground executor: Executor
-    ): HingeAngleProvider =
-        if (config.isHingeAngleEnabled) {
-            HingeSensorAngleProvider(sensorManager, executor)
-        } else {
-            EmptyHingeAngleProvider
-        }
-
-    @Provides
     @Singleton
     fun unfoldKeyguardVisibilityProvider(
         impl: UnfoldKeyguardVisibilityManagerImpl
@@ -94,3 +56,63 @@
         impl: UnfoldKeyguardVisibilityManagerImpl
     ): UnfoldKeyguardVisibilityManager = impl
 }
+
+/**
+ * Needed as methods inside must be public, but their parameters can be internal (and, a public
+ * method can't have internal parameters). Making the module internal and included in a public one
+ * fixes the issue.
+ */
+@Module
+internal class UnfoldSharedInternalModule {
+    @Provides
+    @Singleton
+    fun unfoldTransitionProgressProvider(
+        config: UnfoldTransitionConfig,
+        scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
+        tracingListener: ATraceLoggerTransitionProgressListener,
+        physicsBasedUnfoldTransitionProgressProvider:
+            Provider<PhysicsBasedUnfoldTransitionProgressProvider>,
+        fixedTimingTransitionProgressProvider: Provider<FixedTimingTransitionProgressProvider>,
+    ): Optional<UnfoldTransitionProgressProvider> {
+        if (!config.isEnabled) {
+            return Optional.empty()
+        }
+        val baseProgressProvider =
+            if (config.isHingeAngleEnabled) {
+                physicsBasedUnfoldTransitionProgressProvider.get()
+            } else {
+                fixedTimingTransitionProgressProvider.get()
+            }
+
+        return Optional.of(
+            scaleAwareProviderFactory.wrap(baseProgressProvider).apply {
+                // Always present callback that logs animation beginning and end.
+                addCallback(tracingListener)
+            }
+        )
+    }
+
+    @Provides
+    fun hingeAngleProvider(
+        config: UnfoldTransitionConfig,
+        hingeAngleSensorProvider: Provider<HingeSensorAngleProvider>
+    ): HingeAngleProvider {
+        return if (config.isHingeAngleEnabled) {
+            hingeAngleSensorProvider.get()
+        } else {
+            EmptyHingeAngleProvider
+        }
+    }
+
+    @Provides
+    @Singleton
+    fun provideProgressForwarder(
+            config: UnfoldTransitionConfig,
+            progressForwarder: Provider<UnfoldTransitionProgressForwarder>
+    ): Optional<UnfoldTransitionProgressForwarder> {
+        if (!config.isEnabled) {
+            return Optional.empty()
+        }
+        return Optional.of(progressForwarder.get())
+    }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
index a1ed178..8eb79df 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -37,29 +37,52 @@
  * This should **never** be called from sysui, as the object is already provided in that process.
  */
 fun createUnfoldSharedComponent(
-    context: Context,
-    config: UnfoldTransitionConfig,
-    screenStatusProvider: ScreenStatusProvider,
-    foldProvider: FoldProvider,
-    activityTypeProvider: CurrentActivityTypeProvider,
-    sensorManager: SensorManager,
-    mainHandler: Handler,
-    mainExecutor: Executor,
-    backgroundExecutor: Executor,
-    tracingTagPrefix: String,
-    windowManager: IWindowManager,
+        context: Context,
+        config: UnfoldTransitionConfig,
+        screenStatusProvider: ScreenStatusProvider,
+        foldProvider: FoldProvider,
+        activityTypeProvider: CurrentActivityTypeProvider,
+        sensorManager: SensorManager,
+        mainHandler: Handler,
+        mainExecutor: Executor,
+        singleThreadBgExecutor: Executor,
+        tracingTagPrefix: String,
+        windowManager: IWindowManager,
 ): UnfoldSharedComponent =
-    DaggerUnfoldSharedComponent.factory()
-        .create(
-            context,
-            config,
-            screenStatusProvider,
-            foldProvider,
-            activityTypeProvider,
-            sensorManager,
-            mainHandler,
-            mainExecutor,
-            backgroundExecutor,
-            tracingTagPrefix,
-            windowManager,
-        )
+        DaggerUnfoldSharedComponent.factory()
+                .create(
+                        context,
+                        config,
+                        screenStatusProvider,
+                        foldProvider,
+                        activityTypeProvider,
+                        sensorManager,
+                        mainHandler,
+                        mainExecutor,
+                        singleThreadBgExecutor,
+                        tracingTagPrefix,
+                        windowManager,
+                )
+
+/**
+ * Factory for [RemoteUnfoldSharedComponent].
+ *
+ * Wraps [DaggerRemoteUnfoldSharedComponent] (that is autogenerated), for better discoverability.
+ */
+fun createRemoteUnfoldSharedComponent(
+        context: Context,
+        config: UnfoldTransitionConfig,
+        mainExecutor: Executor,
+        singleThreadBgExecutor: Executor,
+        tracingTagPrefix: String,
+        windowManager: IWindowManager,
+        ): RemoteUnfoldSharedComponent =
+        DaggerRemoteUnfoldSharedComponent.factory()
+                .create(
+                        context,
+                        config,
+                        mainExecutor,
+                        singleThreadBgExecutor,
+                        windowManager,
+                        tracingTagPrefix,
+                )
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt
similarity index 89%
rename from packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt
rename to packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt
index 6074795..dcac531 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt
@@ -18,8 +18,7 @@
 
 /**
  * Alternative to [UiBackground] qualifier annotation in unfold module.
+ *
  * It is needed as we can't depend on SystemUI code in this module.
  */
-@Qualifier
-@Retention(AnnotationRetention.RUNTIME)
-annotation class UnfoldBackground
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnfoldSingleThreadBg
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
index fa59cb4..c437e5c 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
@@ -21,14 +21,15 @@
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
-import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
 import com.android.systemui.unfold.updates.FoldStateProvider
 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+import javax.inject.Inject
 
 /** Emits animation progress with fixed timing after unfolding */
-internal class FixedTimingTransitionProgressProvider(
-    private val foldStateProvider: FoldStateProvider
-) : UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
+internal class FixedTimingTransitionProgressProvider
+@Inject
+constructor(private val foldStateProvider: FoldStateProvider) :
+    UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
 
     private val animatorListener = AnimatorListener()
     private val animator =
@@ -57,12 +58,15 @@
     }
 
     override fun onFoldUpdate(@FoldUpdate update: Int) {
-        when (update) {
-            FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> animator.start()
-            FOLD_UPDATE_FINISH_CLOSED -> animator.cancel()
+        if (update == FOLD_UPDATE_FINISH_CLOSED) {
+             animator.cancel()
         }
     }
 
+    override fun onUnfoldedScreenAvailable() {
+        animator.start()
+    }
+
     override fun addCallback(listener: TransitionProgressListener) {
         listeners.add(listener)
     }
@@ -71,8 +75,6 @@
         listeners.remove(listener)
     }
 
-    override fun onHingeAngleUpdate(angle: Float) {}
-
     private object AnimationProgressProperty :
         FloatProperty<FixedTimingTransitionProgressProvider>("animation_progress") {
 
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/IUnfoldAnimation.aidl b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/IUnfoldAnimation.aidl
new file mode 100644
index 0000000..07a1db4
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/IUnfoldAnimation.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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.unfold.progress;
+
+
+import com.android.systemui.unfold.progress.IUnfoldTransitionListener;
+
+
+/**
+ * Interface exposed by System UI to allow remote process to register for unfold animation events.
+ */
+oneway interface IUnfoldAnimation {
+
+    /**
+     * Sets a listener for the animation.
+     *
+     * Only one listener is supported. If there are multiple, the earlier one will be overridden.
+     */
+    void setListener(in IUnfoldTransitionListener listener);
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/IUnfoldTransitionListener.aidl b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/IUnfoldTransitionListener.aidl
new file mode 100644
index 0000000..8f46b1b
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/IUnfoldTransitionListener.aidl
@@ -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.unfold.progress;
+
+
+/**
+ * Implemented by remote processes to receive unfold animation events from System UI.
+ */
+oneway interface IUnfoldTransitionListener {
+    /**
+    * Sent when unfold animation started.
+    */
+    void onTransitionStarted() = 1;
+
+    /**
+    * Sent when unfold animation progress changes.
+    */
+    void onTransitionProgress(float progress) = 2;
+
+    /**
+    * Sent when unfold animation finished.
+    */
+    void onTransitionFinished() = 3;
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index 074b1e1..d19b414 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -28,14 +28,14 @@
 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
-import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
 import com.android.systemui.unfold.updates.FoldStateProvider
 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
 import com.android.systemui.unfold.updates.name
+import javax.inject.Inject
 
 /** Maps fold updates to unfold transition progress using DynamicAnimation. */
-class PhysicsBasedUnfoldTransitionProgressProvider(
+class PhysicsBasedUnfoldTransitionProgressProvider @Inject constructor(
     private val foldStateProvider: FoldStateProvider
 ) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
 
@@ -77,21 +77,11 @@
 
     override fun onFoldUpdate(@FoldUpdate update: Int) {
         when (update) {
-            FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {
-                startTransition(startValue = 0f)
-
-                // Stop the animation if the device has already opened by the time when
-                // the display is available as we won't receive the full open event anymore
-                if (foldStateProvider.isFinishedOpening) {
-                    cancelTransition(endValue = 1f, animate = true)
-                }
-            }
             FOLD_UPDATE_FINISH_FULL_OPEN, FOLD_UPDATE_FINISH_HALF_OPEN -> {
                 // Do not cancel if we haven't started the transition yet.
                 // This could happen when we fully unfolded the device before the screen
                 // became available. In this case we start and immediately cancel the animation
-                // in FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE event handler, so we don't need to
-                // cancel it here.
+                // in onUnfoldedScreenAvailable event handler, so we don't need to cancel it here.
                 if (isTransitionRunning) {
                     cancelTransition(endValue = 1f, animate = true)
                 }
@@ -124,6 +114,16 @@
         }
     }
 
+    override fun onUnfoldedScreenAvailable() {
+        startTransition(startValue = 0f)
+
+        // Stop the animation if the device has already opened by the time when
+        // the display is available as we won't receive the full open event anymore
+        if (foldStateProvider.isFinishedOpening) {
+            cancelTransition(endValue = 1f, animate = true)
+        }
+    }
+
     private fun cancelTransition(endValue: Float, animate: Boolean) {
         if (isTransitionRunning && animate) {
             if (endValue == 1.0f && !isAnimatedCancelRunning) {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt
new file mode 100644
index 0000000..5e4bcc9
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.unfold.progress
+
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.dagger.UnfoldMain
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Receives unfold events from remote senders (System UI).
+ *
+ * A binder to an instance to this class (created with [RemoteUnfoldTransitionReceiver.asBinder])
+ * should be sent to the remote process providing events.
+ */
+class RemoteUnfoldTransitionReceiver
+@Inject
+constructor(@UnfoldMain private val executor: Executor) :
+    UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
+
+    private val listeners: MutableSet<TransitionProgressListener> = mutableSetOf()
+
+    override fun onTransitionStarted() {
+        executor.execute { listeners.forEach { it.onTransitionStarted() } }
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        executor.execute { listeners.forEach { it.onTransitionProgress(progress) } }
+    }
+
+    override fun onTransitionFinished() {
+        executor.execute { listeners.forEach { it.onTransitionFinished() } }
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners += listener
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners -= listener
+    }
+
+    override fun destroy() {
+        listeners.clear()
+    }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldTransitionProgressForwarder.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldTransitionProgressForwarder.kt
new file mode 100644
index 0000000..b654521
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldTransitionProgressForwarder.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.unfold.progress
+
+import android.os.RemoteException
+import android.util.Log
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import javax.inject.Inject
+
+/** Forwards received unfold events to [remoteListener], when present. */
+class UnfoldTransitionProgressForwarder @Inject constructor() :
+    TransitionProgressListener, IUnfoldAnimation.Stub() {
+
+    private var remoteListener: IUnfoldTransitionListener? = null
+
+    override fun onTransitionStarted() {
+        try {
+            Log.d(TAG, "onTransitionStarted")
+            remoteListener?.onTransitionStarted()
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Failed call onTransitionStarted", e)
+        }
+    }
+
+    override fun onTransitionFinished() {
+        try {
+            Log.d(TAG, "onTransitionFinished")
+            remoteListener?.onTransitionFinished()
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Failed call onTransitionFinished", e)
+        }
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        try {
+            remoteListener?.onTransitionProgress(progress)
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Failed call onTransitionProgress", e)
+        }
+    }
+
+    override fun setListener(listener: IUnfoldTransitionListener?) {
+        remoteListener = listener
+    }
+
+    companion object {
+        private val TAG = UnfoldTransitionProgressForwarder::class.java.simpleName
+    }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 5b45897..82fd225 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -54,6 +54,7 @@
     @FoldUpdate private var lastFoldUpdate: Int? = null
 
     @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
+    @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngleBeforeTransition: Float = 0f
 
     private val hingeAngleListener = HingeAngleListener()
     private val screenListener = ScreenStatusListener()
@@ -79,6 +80,7 @@
         screenStatusProvider.addCallback(screenListener)
         hingeAngleProvider.addCallback(hingeAngleListener)
         rotationChangeProvider.addCallback(rotationListener)
+        activityTypeProvider.init()
     }
 
     override fun stop() {
@@ -87,6 +89,7 @@
         hingeAngleProvider.removeCallback(hingeAngleListener)
         hingeAngleProvider.stop()
         rotationChangeProvider.removeCallback(rotationListener)
+        activityTypeProvider.uninit()
     }
 
     override fun addCallback(listener: FoldUpdatesListener) {
@@ -110,31 +113,45 @@
 
     private fun onHingeAngle(angle: Float) {
         if (DEBUG) {
-            Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle")
+            Log.d(
+                TAG,
+                "Hinge angle: $angle, " +
+                    "lastHingeAngle: $lastHingeAngle, " +
+                    "lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition"
+            )
             Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt())
         }
 
-        val isClosing = angle < lastHingeAngle
-        val closingThreshold = getClosingThreshold()
-        val closingThresholdMet = closingThreshold == null || angle < closingThreshold
+        val currentDirection =
+                if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
+        if (isTransitionInProgress && currentDirection != lastFoldUpdate) {
+            lastHingeAngleBeforeTransition = lastHingeAngle
+        }
+
+        val isClosing = angle < lastHingeAngleBeforeTransition
+        val transitionUpdate =
+                if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
+        val angleChangeSurpassedThreshold =
+            Math.abs(angle - lastHingeAngleBeforeTransition) > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
         val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
-        val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+        val eventNotAlreadyDispatched = lastFoldUpdate != transitionUpdate
         val screenAvailableEventSent = isUnfoldHandled
 
-        if (isClosing // hinge angle should be decreasing since last update
-                && closingThresholdMet // hinge angle is below certain threshold
-                && !closingEventDispatched  // we haven't sent closing event already
-                && !isFullyOpened // do not send closing event if we are in fully opened hinge
+        if (
+            angleChangeSurpassedThreshold && // Do not react immediately to small changes in angle
+                eventNotAlreadyDispatched && // we haven't sent transition event already
+                !isFullyOpened && // do not send transition event if we are in fully opened hinge
                                   // angle range as closing threshold could overlap this range
-                && screenAvailableEventSent // do not send closing event if we are still in
-                                            // the process of turning on the inner display
+                screenAvailableEventSent && // do not send transition event if we are still in the
+                                            // process of turning on the inner display
+                isClosingThresholdMet(angle) // hinge angle is below certain threshold.
         ) {
-            notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
+            notifyFoldUpdate(transitionUpdate, lastHingeAngle)
         }
 
         if (isTransitionInProgress) {
             if (isFullyOpened) {
-                notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
+                notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN, angle)
                 cancelTimeout()
             } else {
                 // The timeout will trigger some constant time after the last angle update.
@@ -146,6 +163,11 @@
         outputListeners.forEach { it.onHingeAngleUpdate(angle) }
     }
 
+    private fun isClosingThresholdMet(currentAngle: Float): Boolean {
+        val closingThreshold = getClosingThreshold()
+        return closingThreshold == null || currentAngle < closingThreshold
+    }
+
     /**
      * Fold animation should be started only after the threshold returned here.
      *
@@ -174,23 +196,29 @@
 
             if (isFolded) {
                 hingeAngleProvider.stop()
-                notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
+                notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED, lastHingeAngle)
                 cancelTimeout()
                 isUnfoldHandled = false
             } else {
-                notifyFoldUpdate(FOLD_UPDATE_START_OPENING)
+                notifyFoldUpdate(FOLD_UPDATE_START_OPENING, lastHingeAngle)
                 rescheduleAbortAnimationTimeout()
                 hingeAngleProvider.start()
             }
         }
     }
 
-    private fun notifyFoldUpdate(@FoldUpdate update: Int) {
+    private fun notifyFoldUpdate(@FoldUpdate update: Int, angle: Float) {
         if (DEBUG) {
             Log.d(TAG, update.name())
         }
+        val previouslyTransitioning = isTransitionInProgress
+
         outputListeners.forEach { it.onFoldUpdate(update) }
         lastFoldUpdate = update
+
+        if (previouslyTransitioning != isTransitionInProgress) {
+            lastHingeAngleBeforeTransition = angle
+        }
     }
 
     private fun rescheduleAbortAnimationTimeout() {
@@ -204,7 +232,8 @@
         handler.removeCallbacks(timeoutRunnable)
     }
 
-    private fun cancelAnimation(): Unit = notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
+    private fun cancelAnimation(): Unit =
+            notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN, lastHingeAngle)
 
     private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
 
@@ -216,7 +245,7 @@
             // receive 'folded' event. If SystemUI started when device is already folded it will
             // still receive 'folded' event on startup.
             if (!isFolded && !isUnfoldHandled) {
-                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) }
+                outputListeners.forEach { it.onUnfoldedScreenAvailable() }
                 isUnfoldHandled = true
             }
         }
@@ -252,7 +281,6 @@
     when (this) {
         FOLD_UPDATE_START_OPENING -> "START_OPENING"
         FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
-        FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
         FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN"
         FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN"
         FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
@@ -265,5 +293,8 @@
 /** Threshold after which we consider the device fully unfolded. */
 @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
 
+/** Threshold after which hinge angle updates are considered. This is to eliminate noise. */
+@VisibleForTesting const val HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES = 7.5f
+
 /** Fold animation on top of apps only when the angle exceeds this threshold. */
 @VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
index c7a8bf3..0af372f 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
@@ -31,8 +31,9 @@
     val isFinishedOpening: Boolean
 
     interface FoldUpdatesListener {
-        fun onHingeAngleUpdate(@FloatRange(from = 0.0, to = 180.0) angle: Float)
-        fun onFoldUpdate(@FoldUpdate update: Int)
+        @JvmDefault fun onHingeAngleUpdate(@FloatRange(from = 0.0, to = 180.0) angle: Float) {}
+        @JvmDefault fun onFoldUpdate(@FoldUpdate update: Int) {}
+        @JvmDefault fun onUnfoldedScreenAvailable() {}
     }
 
     @IntDef(
@@ -40,7 +41,6 @@
             [
                 FOLD_UPDATE_START_OPENING,
                 FOLD_UPDATE_START_CLOSING,
-                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE,
                 FOLD_UPDATE_FINISH_HALF_OPEN,
                 FOLD_UPDATE_FINISH_FULL_OPEN,
                 FOLD_UPDATE_FINISH_CLOSED])
@@ -50,7 +50,6 @@
 
 const val FOLD_UPDATE_START_OPENING = 0
 const val FOLD_UPDATE_START_CLOSING = 1
-const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 2
-const val FOLD_UPDATE_FINISH_HALF_OPEN = 3
-const val FOLD_UPDATE_FINISH_FULL_OPEN = 4
-const val FOLD_UPDATE_FINISH_CLOSED = 5
+const val FOLD_UPDATE_FINISH_HALF_OPEN = 2
+const val FOLD_UPDATE_FINISH_FULL_OPEN = 3
+const val FOLD_UPDATE_FINISH_CLOSED = 4
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
index 577137c..89fb12e 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
@@ -20,35 +20,43 @@
 import android.hardware.SensorManager
 import android.os.Trace
 import androidx.core.util.Consumer
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
 import java.util.concurrent.Executor
+import javax.inject.Inject
 
-internal class HingeSensorAngleProvider(
+internal class HingeSensorAngleProvider
+@Inject
+constructor(
     private val sensorManager: SensorManager,
-    private val executor: Executor
-) :
-    HingeAngleProvider {
+    @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor
+) : HingeAngleProvider {
 
     private val sensorListener = HingeAngleSensorListener()
     private val listeners: MutableList<Consumer<Float>> = arrayListOf()
     var started = false
 
-    override fun start() = executor.execute {
-        if (started) return@execute
-        Trace.beginSection("HingeSensorAngleProvider#start")
-        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
-        sensorManager.registerListener(
-            sensorListener,
-            sensor,
-            SensorManager.SENSOR_DELAY_FASTEST
-        )
-        Trace.endSection()
-        started = true
+    override fun start() {
+        singleThreadBgExecutor.execute {
+            if (started) return@execute
+            Trace.beginSection("HingeSensorAngleProvider#start")
+            val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
+            sensorManager.registerListener(
+                sensorListener,
+                sensor,
+                SensorManager.SENSOR_DELAY_FASTEST
+            )
+            Trace.endSection()
+
+            started = true
+        }
     }
 
-    override fun stop() = executor.execute {
-        if (!started) return@execute
-        sensorManager.unregisterListener(sensorListener)
-        started = false
+    override fun stop() {
+        singleThreadBgExecutor.execute {
+            if (!started) return@execute
+            sensorManager.unregisterListener(sensorListener)
+            started = false
+        }
     }
 
     override fun removeCallback(listener: Consumer<Float>) {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
index d0e6cdc..34e7c38 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
@@ -16,6 +16,11 @@
 
 interface CurrentActivityTypeProvider {
     val isHomeActivity: Boolean?
+
+    /** Starts listening for task updates. */
+    fun init() {}
+    /** Stop listening for task updates. */
+    fun uninit() {}
 }
 
 class EmptyCurrentActivityTypeProvider(override val isHomeActivity: Boolean? = null) :
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 1e9c3b7..c7a7959 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1842,6 +1842,12 @@
             AccessibilityServiceInfo accessibilityServiceInfo;
             try {
                 accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
+                if (!accessibilityServiceInfo.isWithinParcelableSize()) {
+                    Slog.e(LOG_TAG, "Skipping service "
+                            + accessibilityServiceInfo.getResolveInfo().getComponentInfo()
+                            + " because service info size is larger than safe parcelable limits.");
+                    continue;
+                }
                 if (userState.mCrashedServices.contains(serviceInfo.getComponentName())) {
                     // Restore the crashed attribute.
                     accessibilityServiceInfo.crashed = true;
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index 3fa0ab6..e6abc4c 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -391,7 +391,7 @@
     private boolean takeScreenshot() {
         ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
                 ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
-        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+        screenshotHelper.takeScreenshot(
                 WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
                 new Handler(Looper.getMainLooper()), null);
         return true;
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 308f360..9d91b97 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -817,7 +817,8 @@
             if (host != null) {
                 host.callbacks = null;
                 pruneHostLocked(host);
-                mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
+                mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(),
+                        false);
             }
         }
     }
@@ -888,12 +889,8 @@
             Host host = lookupHostLocked(id);
 
             if (host != null) {
-                try {
-                    mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
-                } catch (NullPointerException e) {
-                    Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e);
-                    throw e;
-                }
+                mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(),
+                        false);
             }
         }
     }
@@ -4345,14 +4342,15 @@
                     PendingHostUpdate.appWidgetRemoved(appWidgetId));
         }
 
-        public SparseArray<String> getWidgetUids() {
+        public SparseArray<String> getWidgetUidsIfBound() {
             final SparseArray<String> uids = new SparseArray<>();
             for (int i = widgets.size() - 1; i >= 0; i--) {
                 final Widget widget = widgets.get(i);
                 if (widget.provider == null) {
                     if (DEBUG) {
-                        Slog.e(TAG, "Widget with no provider " + widget.toString());
+                        Slog.d(TAG, "Widget with no provider " + widget.toString());
                     }
+                    continue;
                 }
                 final ProviderId providerId = widget.provider.id;
                 uids.put(providerId.uid, providerId.componentName.getPackageName());
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index db1a62c..31dcc8b 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -1173,6 +1173,11 @@
     }
 
     private final class Led {
+        // must match: config_notificationsBatteryLowBehavior in config.xml
+        static final int LOW_BATTERY_BEHAVIOR_DEFAULT = 0;
+        static final int LOW_BATTERY_BEHAVIOR_SOLID = 1;
+        static final int LOW_BATTERY_BEHAVIOR_FLASHING = 2;
+
         private final LogicalLight mBatteryLight;
 
         private final int mBatteryLowARGB;
@@ -1180,6 +1185,7 @@
         private final int mBatteryFullARGB;
         private final int mBatteryLedOn;
         private final int mBatteryLedOff;
+        private final int mBatteryLowBehavior;
 
         public Led(Context context, LightsManager lights) {
             mBatteryLight = lights.getLight(LightsManager.LIGHT_ID_BATTERY);
@@ -1196,6 +1202,8 @@
                     com.android.internal.R.integer.config_notificationsBatteryLedOff);
             mBatteryNearlyFullLevel = context.getResources().getInteger(
                     com.android.internal.R.integer.config_notificationsBatteryNearlyFullLevel);
+            mBatteryLowBehavior = context.getResources().getInteger(
+                    com.android.internal.R.integer.config_notificationsBatteryLowBehavior);
         }
 
         /**
@@ -1208,13 +1216,26 @@
             final int level = mHealthInfo.batteryLevel;
             final int status = mHealthInfo.batteryStatus;
             if (level < mLowBatteryWarningLevel) {
-                if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
-                    // Solid red when battery is charging
-                    mBatteryLight.setColor(mBatteryLowARGB);
-                } else {
-                    // Flash red when battery is low and not charging
-                    mBatteryLight.setFlashing(mBatteryLowARGB, LogicalLight.LIGHT_FLASH_TIMED,
-                            mBatteryLedOn, mBatteryLedOff);
+                switch (mBatteryLowBehavior) {
+                    case LOW_BATTERY_BEHAVIOR_SOLID:
+                        // Solid red when low battery
+                        mBatteryLight.setColor(mBatteryLowARGB);
+                        break;
+                    case LOW_BATTERY_BEHAVIOR_FLASHING:
+                        // Flash red when battery is low and not charging
+                        mBatteryLight.setFlashing(mBatteryLowARGB, LogicalLight.LIGHT_FLASH_TIMED,
+                                mBatteryLedOn, mBatteryLedOff);
+                        break;
+                    default:
+                        if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
+                            // Solid red when battery is charging
+                            mBatteryLight.setColor(mBatteryLowARGB);
+                        } else {
+                            // Flash red when battery is low and not charging
+                            mBatteryLight.setFlashing(mBatteryLowARGB,
+                                    LogicalLight.LIGHT_FLASH_TIMED, mBatteryLedOn, mBatteryLedOff);
+                        }
+                        break;
                 }
             } else if (status == BatteryManager.BATTERY_STATUS_CHARGING
                     || status == BatteryManager.BATTERY_STATUS_FULL) {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index ce9c067..4be8993 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3772,6 +3772,13 @@
         final boolean includeSharedProfile =
                 (flags & StorageManager.FLAG_INCLUDE_SHARED_PROFILE) != 0;
 
+        // When the caller is the app actually hosting external storage, we
+        // should never attempt to augment the actual storage volume state,
+        // otherwise we risk confusing it with race conditions as users go
+        // through various unlocked states
+        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
+                mMediaStoreAuthorityAppId);
+
         // Only Apps with MANAGE_EXTERNAL_STORAGE should call the API with includeSharedProfile
         if (includeSharedProfile) {
             try {
@@ -3784,8 +3791,13 @@
                 // Checking first entry in packagesFromUid is enough as using "sharedUserId"
                 // mechanism is rare and discouraged. Also, Apps that share same UID share the same
                 // permissions.
-                if (!mStorageManagerInternal.hasExternalStorageAccess(callingUid,
-                        packagesFromUid[0])) {
+                // Allowing Media Provider is an exception, Media Provider process should be allowed
+                // to query users across profiles, even without MANAGE_EXTERNAL_STORAGE access.
+                // Note that ordinarily Media provider process has the above permission, but if they
+                // are revoked, Storage Volume(s) should still be returned.
+                if (!callerIsMediaStore
+                        && !mStorageManagerInternal.hasExternalStorageAccess(callingUid,
+                                packagesFromUid[0])) {
                     throw new SecurityException("Only File Manager Apps permitted");
                 }
             } catch (RemoteException re) {
@@ -3798,13 +3810,6 @@
         // point
         final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM);
 
-        // When the caller is the app actually hosting external storage, we
-        // should never attempt to augment the actual storage volume state,
-        // otherwise we risk confusing it with race conditions as users go
-        // through various unlocked states
-        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
-                mMediaStoreAuthorityAppId);
-
         final boolean userIsDemo;
         final boolean userKeyUnlocked;
         final boolean storagePermission;
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 89447b4..f846741 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -3091,7 +3091,7 @@
                             }
                         }
 
-                        Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+                        Intent intent = result.getParcelable(AccountManager.KEY_INTENT, Intent.class);
                         if (intent != null && notifyOnAuthFailure && !customTokens) {
                             /*
                              * Make sure that the supplied intent is owned by the authenticator
@@ -3516,8 +3516,7 @@
             Bundle.setDefusable(result, true);
             mNumResults++;
             Intent intent = null;
-            if (result != null
-                    && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
+            if (result != null) {
                 if (!checkKeyIntent(
                         Binder.getCallingUid(),
                         result)) {
@@ -4886,8 +4885,10 @@
             	EventLog.writeEvent(0x534e4554, "250588548", authUid, "");
                 return false;
             }
-
             Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
+            if (intent == null) {
+                return true;
+            }
             // Explicitly set an empty ClipData to ensure that we don't offer to
             // promote any Uris contained inside for granting purposes
             if (intent.getClipData() == null) {
@@ -4937,8 +4938,12 @@
             Bundle simulateBundle = p.readBundle();
             p.recycle();
             Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
-            return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT,
-                Intent.class)));
+            Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT,
+                    Intent.class);
+            if (intent == null) {
+                return (simulateIntent == null);
+            }
+            return intent.filterEquals(simulateIntent);
         }
 
         private boolean isExportedSystemActivity(ActivityInfo activityInfo) {
@@ -5087,8 +5092,7 @@
                     }
                 }
             }
-            if (result != null
-                    && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
+            if (result != null) {
                 if (!checkKeyIntent(
                         Binder.getCallingUid(),
                         result)) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 9669c06..c36e070 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3420,6 +3420,11 @@
                             throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
                                     + className + " is not an isolatedProcess");
                         }
+                        if (!mAm.getPackageManagerInternal().isSameApp(callingPackage, callingUid,
+                                userId)) {
+                            throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
+                                    + "calling package not owned by calling UID ");
+                        }
                         // Run the service under the calling package's application.
                         ApplicationInfo aInfo = AppGlobals.getPackageManager().getApplicationInfo(
                                 callingPackage, ActivityManagerService.STOCK_PM_FLAGS, userId);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d48723a..5b77ed1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -12766,8 +12766,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;
@@ -14657,6 +14659,17 @@
                     throw new SecurityException(msg);
                 }
             }
+            if (!Build.IS_DEBUGGABLE && callingUid != ROOT_UID && callingUid != SHELL_UID
+                    && callingUid != SYSTEM_UID && !hasActiveInstrumentationLocked(callingPid)) {
+                // If it's not debug build and not called from root/shell/system uid, reject it.
+                final String msg = "Permission Denial: instrumentation test "
+                        + className + " from pid=" + callingPid + ", uid=" + callingUid
+                        + ", pkgName=" + getPackageNameByPid(callingPid)
+                        + " not allowed because it's not started from SHELL";
+                Slog.wtfQuiet(TAG, msg);
+                reportStartInstrumentationFailureLocked(watcher, className, msg);
+                throw new SecurityException(msg);
+            }
 
             boolean disableHiddenApiChecks = ai.usesNonSdkApi()
                     || (flags & INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS) != 0;
@@ -14879,6 +14892,29 @@
         }
     }
 
+    @GuardedBy("this")
+    private boolean hasActiveInstrumentationLocked(int pid) {
+        if (pid == 0) {
+            return false;
+        }
+        synchronized (mPidsSelfLocked) {
+            ProcessRecord process = mPidsSelfLocked.get(pid);
+            return process != null && process.getActiveInstrumentation() != null;
+        }
+    }
+
+    private String getPackageNameByPid(int pid) {
+        synchronized (mPidsSelfLocked) {
+            final ProcessRecord app = mPidsSelfLocked.get(pid);
+
+            if (app != null && app.info != null) {
+                return app.info.packageName;
+            }
+
+            return null;
+        }
+    }
+
     private boolean isCallerShell() {
         final int callingUid = Binder.getCallingUid();
         return callingUid == SHELL_UID || callingUid == ROOT_UID;
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 8439a7d..5c08afd 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -21,6 +21,7 @@
 import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE;
 
 import android.annotation.NonNull;
+import android.app.AlarmManager;
 import android.app.StatsManager;
 import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothActivityEnergyInfo;
@@ -356,6 +357,16 @@
         mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
         mStats.setPowerProfileLocked(mPowerProfile);
+
+        final boolean resetOnUnplugHighBatteryLevel = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel);
+        final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge);
+        mStats.setBatteryStatsConfig(
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
+                        .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
+                        .build());
         mStats.startTrackingSystemServerCpuTime();
 
         if (BATTERY_USAGE_STORE_ENABLED) {
@@ -386,6 +397,18 @@
             Slog.e(TAG, "Could not register INetworkManagement event observer " + e);
         }
 
+        final AlarmManager am = mContext.getSystemService(AlarmManager.class);
+        mHandler.post(() -> {
+            synchronized (mStats) {
+                mStats.setLongPlugInAlarmInterface(new AlarmInterface(am, () -> {
+                    synchronized (mStats) {
+                        if (mStats.isOnBattery()) return;
+                        mStats.maybeResetWhilePluggedInLocked();
+                    }
+                }));
+            }
+        });
+
         synchronized (mPowerStatsLock) {
             mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class);
             if (mPowerStatsInternal != null) {
@@ -2264,6 +2287,32 @@
         }
     }
 
+    final class AlarmInterface implements BatteryStatsImpl.AlarmInterface,
+            AlarmManager.OnAlarmListener {
+        private AlarmManager mAm;
+        private Runnable mOnAlarm;
+
+        AlarmInterface(AlarmManager am, Runnable onAlarm) {
+            mAm = am;
+            mOnAlarm = onAlarm;
+        }
+
+        @Override
+        public void schedule(long rtcTimeMs, long windowLengthMs) {
+            mAm.setWindow(AlarmManager.RTC, rtcTimeMs, windowLengthMs, TAG, this, mHandler);
+        }
+
+        @Override
+        public void cancel() {
+            mAm.cancel(this);
+        }
+
+        @Override
+        public void onAlarm() {
+            mOnAlarm.run();
+        }
+    }
+
     private static native int nativeWaitWakeup(ByteBuffer outBuffer);
 
     private void dumpHelp(PrintWriter pw) {
@@ -2450,7 +2499,8 @@
                 } else if ("--reset-all".equals(arg)) {
                     awaitCompletion();
                     synchronized (mStats) {
-                        mStats.resetAllStatsCmdLocked();
+                        mStats.resetAllStatsAndHistoryLocked(
+                                BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
                         mBatteryUsageStatsStore.removeAllSnapshots();
                         pw.println("Battery stats and history reset.");
                         noOutput = true;
@@ -2458,7 +2508,8 @@
                 } else if ("--reset".equals(arg)) {
                     awaitCompletion();
                     synchronized (mStats) {
-                        mStats.resetAllStatsCmdLocked();
+                        mStats.resetAllStatsAndHistoryLocked(
+                                BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
                         pw.println("Battery stats reset.");
                         noOutput = true;
                     }
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index e4f624d..263d19a 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -61,9 +61,7 @@
         { "include-filter": "com.android.server.am.MeasuredEnergySnapshotTest" },
         { "include-filter": "com.android.server.am.BatteryExternalStatsWorkerTest" }
       ]
-    }
-  ],
-  "presubmit-large": [
+    },
     {
       "name": "CtsUsageStatsTestCases",
       "file_patterns": [
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index 3584f16..ac78228 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -51,6 +51,7 @@
 import android.app.GameModeInfo;
 import android.app.GameState;
 import android.app.IGameManagerService;
+import android.app.IUidObserver;
 import android.app.compat.PackageOverride;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -118,6 +119,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Service to manage game related features.
@@ -171,40 +173,19 @@
     private final ArrayMap<String, GamePackageConfiguration> mOverrideConfigs = new ArrayMap<>();
     @Nullable
     private final GameServiceController mGameServiceController;
+    private final Object mUidObserverLock = new Object();
+    @VisibleForTesting
+    @Nullable
+    final UidObserver mUidObserver;
+    @GuardedBy("mUidObserverLock")
+    private final Set<Integer> mForegroundGameUids = new HashSet<>();
 
     public GameManagerService(Context context) {
         this(context, createServiceThread().getLooper());
     }
 
     GameManagerService(Context context, Looper looper) {
-        mContext = context;
-        mHandler = new SettingsHandler(looper);
-        mPackageManager = mContext.getPackageManager();
-        mUserManager = mContext.getSystemService(UserManager.class);
-        mPlatformCompat = IPlatformCompat.Stub.asInterface(
-                ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
-        mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
-        mSystemDir = new File(Environment.getDataDirectory(), "system");
-        mSystemDir.mkdirs();
-        FileUtils.setPermissions(mSystemDir.toString(),
-                FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH,
-                -1, -1);
-        mGameModeInterventionListFile = new AtomicFile(new File(mSystemDir,
-                                                     GAME_MODE_INTERVENTION_LIST_FILE_NAME));
-        FileUtils.setPermissions(mGameModeInterventionListFile.getBaseFile().getAbsolutePath(),
-                FileUtils.S_IRUSR | FileUtils.S_IWUSR
-                        | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
-                -1, -1);
-        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
-            mGameServiceController = new GameServiceController(
-                    context, BackgroundThread.getExecutor(),
-                    new GameServiceProviderSelectorImpl(
-                            context.getResources(),
-                            context.getPackageManager()),
-                    new GameServiceProviderInstanceFactoryImpl(context));
-        } else {
-            mGameServiceController = null;
-        }
+        this(context, looper, Environment.getDataDirectory());
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@@ -237,6 +218,14 @@
         } else {
             mGameServiceController = null;
         }
+        mUidObserver = new UidObserver();
+        try {
+            ActivityManager.getService().registerUidObserver(mUidObserver,
+                    ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
+                    ActivityManager.PROCESS_STATE_UNKNOWN, null);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Could not register UidObserver");
+        }
     }
 
     @Override
@@ -1874,4 +1863,66 @@
      * load dynamic library for frame rate overriding JNI calls
      */
     private static native void nativeSetOverrideFrameRate(int uid, float frameRate);
+
+    final class UidObserver extends IUidObserver.Stub {
+        @Override
+        public void onUidIdle(int uid, boolean disabled) {}
+
+        @Override
+        public void onUidGone(int uid, boolean disabled) {
+            synchronized (mUidObserverLock) {
+                disableGameMode(uid);
+            }
+        }
+
+        @Override
+        public void onUidActive(int uid) {}
+
+        @Override
+        public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
+            synchronized (mUidObserverLock) {
+                if (ActivityManager.isProcStateBackground(procState)) {
+                    disableGameMode(uid);
+                    return;
+                }
+
+                final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+                if (packages == null || packages.length == 0) {
+                    return;
+                }
+
+                final int userId = mContext.getUserId();
+                if (!Arrays.stream(packages).anyMatch(p -> isPackageGame(p, userId))) {
+                    return;
+                }
+
+                if (mForegroundGameUids.isEmpty()) {
+                    Slog.v(TAG, "Game power mode ON (process state was changed to foreground)");
+                    mPowerManagerInternal.setPowerMode(Mode.GAME, true);
+                }
+                mForegroundGameUids.add(uid);
+            }
+        }
+
+        private void disableGameMode(int uid) {
+            synchronized (mUidObserverLock) {
+                if (!mForegroundGameUids.contains(uid)) {
+                    return;
+                }
+                mForegroundGameUids.remove(uid);
+                if (!mForegroundGameUids.isEmpty()) {
+                    return;
+                }
+                Slog.v(TAG,
+                        "Game power mode OFF (process remomved or state changed to background)");
+                mPowerManagerInternal.setPowerMode(Mode.GAME, false);
+            }
+        }
+
+        @Override
+        public void onUidCachedChanged(int uid, boolean cached) {}
+
+        @Override
+        public void onUidProcAdjChanged(int uid) {}
+    }
 }
diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
index 4fd739ca..4b0ae1b 100644
--- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
+++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
@@ -16,6 +16,9 @@
 
 package com.android.server.app;
 
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -33,7 +36,6 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.games.CreateGameSessionRequest;
@@ -50,7 +52,6 @@
 import android.util.Slog;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost.SurfacePackage;
-import android.view.WindowManager;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -59,6 +60,7 @@
 import com.android.internal.infra.ServiceConnector.ServiceLifecycleCallbacks;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 import com.android.server.wm.WindowManagerInternal.TaskSystemBarsListener;
@@ -861,8 +863,6 @@
                 Slog.w(TAG, "Could not get bitmap for id: " + taskId);
                 callback.complete(GameScreenshotResult.createInternalErrorResult());
             } else {
-                final Bundle bundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(
-                        bitmap);
                 final RunningTaskInfo runningTaskInfo =
                         mGameTaskInfoProvider.getRunningTaskInfo(taskId);
                 if (runningTaskInfo == null) {
@@ -877,11 +877,17 @@
                         callback.complete(GameScreenshotResult.createSuccessResult());
                     }
                 };
-                mScreenshotHelper.provideScreenshot(bundle, crop, Insets.NONE, taskId,
-                        mUserHandle.getIdentifier(), gameSessionRecord.getComponentName(),
-                        WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
-                        BackgroundThread.getHandler(),
-                        completionConsumer);
+                ScreenshotRequest request = new ScreenshotRequest.Builder(
+                        TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+                        .setTopComponent(gameSessionRecord.getComponentName())
+                        .setTaskId(taskId)
+                        .setUserId(mUserHandle.getIdentifier())
+                        .setBitmap(bitmap)
+                        .setBoundsOnScreen(crop)
+                        .setInsets(Insets.NONE)
+                        .build();
+                mScreenshotHelper.takeScreenshot(
+                        request, BackgroundThread.getHandler(), completionConsumer);
             }
         });
     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index e1ae8d9..c59158b 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -56,6 +56,7 @@
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -82,6 +83,7 @@
 
     private final @NonNull AudioService mAudioService;
     private final @NonNull Context mContext;
+    private final @NonNull AudioSystemAdapter mAudioSystem;
 
     /** ID for Communication strategy retrieved form audio policy manager */
     private int mCommunicationStrategyId = -1;
@@ -156,12 +158,14 @@
     public static final long USE_SET_COMMUNICATION_DEVICE = 243827847L;
 
     //-------------------------------------------------------------------
-    /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
+    /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
+            @NonNull AudioSystemAdapter audioSystem) {
         mContext = context;
         mAudioService = service;
         mBtHelper = new BtHelper(this);
         mDeviceInventory = new AudioDeviceInventory(this);
         mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
+        mAudioSystem = audioSystem;
 
         init();
     }
@@ -170,12 +174,14 @@
      *  in system_server */
     AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
                       @NonNull AudioDeviceInventory mockDeviceInventory,
-                      @NonNull SystemServerAdapter mockSystemServer) {
+                      @NonNull SystemServerAdapter mockSystemServer,
+                      @NonNull AudioSystemAdapter audioSystem) {
         mContext = context;
         mAudioService = service;
         mBtHelper = new BtHelper(this);
         mDeviceInventory = mockDeviceInventory;
         mSystemServer = mockSystemServer;
+        mAudioSystem = audioSystem;
 
         init();
     }
@@ -429,6 +435,48 @@
         return device;
     }
 
+    private static final int[] VALID_COMMUNICATION_DEVICE_TYPES = {
+            AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+            AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+            AudioDeviceInfo.TYPE_WIRED_HEADSET,
+            AudioDeviceInfo.TYPE_USB_HEADSET,
+            AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+            AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+            AudioDeviceInfo.TYPE_HEARING_AID,
+            AudioDeviceInfo.TYPE_BLE_HEADSET,
+            AudioDeviceInfo.TYPE_USB_DEVICE,
+            AudioDeviceInfo.TYPE_BLE_SPEAKER,
+            AudioDeviceInfo.TYPE_LINE_ANALOG,
+            AudioDeviceInfo.TYPE_HDMI,
+            AudioDeviceInfo.TYPE_AUX_LINE
+    };
+
+    /*package */ static boolean isValidCommunicationDevice(AudioDeviceInfo device) {
+        for (int type : VALID_COMMUNICATION_DEVICE_TYPES) {
+            if (device.getType() == type) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /* package */ static List<AudioDeviceInfo> getAvailableCommunicationDevices() {
+        ArrayList<AudioDeviceInfo> commDevices = new ArrayList<>();
+        AudioDeviceInfo[] allDevices =
+                AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
+        for (AudioDeviceInfo device : allDevices) {
+            if (isValidCommunicationDevice(device)) {
+                commDevices.add(device);
+            }
+        }
+        return commDevices;
+    }
+
+    private @Nullable AudioDeviceInfo getCommunicationDeviceOfType(int type) {
+        return getAvailableCommunicationDevices().stream().filter(d -> d.getType() == type)
+                .findFirst().orElse(null);
+    }
+
     /**
      * Returns the device currently requested for communication use case.
      * @return AudioDeviceInfo the requested device for communication.
@@ -436,7 +484,29 @@
     /* package */ AudioDeviceInfo getCommunicationDevice() {
         synchronized (mDeviceStateLock) {
             updateActiveCommunicationDevice();
-            return mActiveCommunicationDevice;
+            AudioDeviceInfo device = mActiveCommunicationDevice;
+            // make sure we return a valid communication device (i.e. a device that is allowed by
+            // setCommunicationDevice()) for consistency.
+            if (device != null) {
+                // a digital dock is used instead of the speaker in speakerphone mode and should
+                // be reflected as such
+                if (device.getType() == AudioDeviceInfo.TYPE_DOCK) {
+                    device = getCommunicationDeviceOfType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
+                }
+            }
+            // Try to default to earpiece when current communication device is not valid. This can
+            // happen for instance if no call is active. If no earpiece device is available take the
+            // first valid communication device
+            if (device == null || !AudioDeviceBroker.isValidCommunicationDevice(device)) {
+                device = getCommunicationDeviceOfType(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+                if (device == null) {
+                    List<AudioDeviceInfo> commDevices = getAvailableCommunicationDevices();
+                    if (!commDevices.isEmpty()) {
+                        device = commDevices.get(0);
+                    }
+                }
+            }
+            return device;
         }
     }
 
@@ -450,7 +520,7 @@
             AudioAttributes attr =
                     AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType(
                             AudioSystem.STREAM_VOICE_CALL);
-            List<AudioDeviceAttributes> devices = AudioSystem.getDevicesForAttributes(
+            List<AudioDeviceAttributes> devices = mAudioSystem.getDevicesForAttributes(
                     attr, false /* forVolume */);
             if (devices.isEmpty()) {
                 if (mAudioService.isPlatformVoice()) {
@@ -913,8 +983,8 @@
 
     @GuardedBy("mDeviceStateLock")
     private void dispatchCommunicationDevice() {
-        int portId = (mActiveCommunicationDevice == null) ? 0
-                : mActiveCommunicationDevice.getId();
+        AudioDeviceInfo device = getCommunicationDevice();
+        int portId = device != null ? device.getId() : 0;
         if (portId == mCurCommunicationPortId) {
             return;
         }
@@ -931,6 +1001,7 @@
         mCommDevDispatchers.finishBroadcast();
     }
 
+
     //---------------------------------------------------------------------
     // Communication with (to) AudioService
     //TODO check whether the AudioService methods are candidates to move here
@@ -1229,7 +1300,7 @@
             Log.v(TAG, "onSetForceUse(useCase<" + useCase + ">, config<" + config + ">, fromA2dp<"
                     + fromA2dp + ">, eventSource<" + eventSource + ">)");
         }
-        AudioSystem.setForceUse(useCase, config);
+        mAudioSystem.setForceUse(useCase, config);
     }
 
     private void onSendBecomingNoisyIntent() {
@@ -1452,6 +1523,7 @@
                 case MSG_I_BT_SERVICE_DISCONNECTED_PROFILE:
                     if (msg.arg1 != BluetoothProfile.HEADSET) {
                         synchronized (mDeviceStateLock) {
+                            mBtHelper.onBtProfileDisconnected(msg.arg1);
                             mDeviceInventory.onBtProfileDisconnected(msg.arg1);
                         }
                     } else {
@@ -1878,9 +1950,9 @@
 
         if (preferredCommunicationDevice == null
                 || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
-            AudioSystem.setParameters("BT_SCO=off");
+            mAudioSystem.setParameters("BT_SCO=off");
         } else {
-            AudioSystem.setParameters("BT_SCO=on");
+            mAudioSystem.setParameters("BT_SCO=on");
         }
         if (preferredCommunicationDevice == null) {
             AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 593acd6..4d9714c 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -41,6 +41,7 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityThread;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -155,6 +156,7 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorManager;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.System;
 import android.service.notification.ZenModeConfig;
@@ -176,6 +178,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.EventLogTags;
@@ -183,6 +186,7 @@
 import com.android.server.SystemService;
 import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent;
 import com.android.server.audio.AudioServiceEvents.PhoneStateEvent;
+import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent;
 import com.android.server.audio.AudioServiceEvents.VolumeEvent;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
@@ -234,6 +238,7 @@
             AudioSystemAdapter.OnVolRangeInitRequestListener {
 
     private static final String TAG = "AS.AudioService";
+    private static final boolean CONFIG_DEFAULT_VAL = false;
 
     private final AudioSystemAdapter mAudioSystem;
     private final SystemServerAdapter mSystemServer;
@@ -988,6 +993,7 @@
      * @param looper Looper to use for the service's message handler. If this is null, an
      *               {@link AudioSystemThread} is created as the messaging thread instead.
      */
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public AudioService(Context context, AudioSystemAdapter audioSystem,
             SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper,
             AppOpsManager appOps) {
@@ -1027,8 +1033,12 @@
         mUseVolumeGroupAliases = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
 
-        mNotifAliasRing = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_alias_ring_notif_stream_types);
+        mNotifAliasRing = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
+
+        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+                ActivityThread.currentApplication().getMainExecutor(),
+                this::onDeviceConfigChange);
 
         // Initialize volume
         // Priority 1 - Android Property
@@ -1199,7 +1209,7 @@
         mUseFixedVolume = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_useFixedVolume);
 
-        mDeviceBroker = new AudioDeviceBroker(mContext, this);
+        mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem);
 
         mRecordMonitor = new RecordingActivityMonitor(mContext);
         mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true);
@@ -1246,6 +1256,36 @@
                 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
     }
 
+    private void initVolumeStreamStates() {
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+        synchronized (VolumeStreamState.class) {
+            for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+                VolumeStreamState streamState = mStreamStates[streamType];
+                int groupId = getVolumeGroupForStreamType(streamType);
+                if (groupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP
+                        && sVolumeGroupStates.indexOfKey(groupId) >= 0) {
+                    streamState.setVolumeGroupState(sVolumeGroupStates.get(groupId));
+                }
+            }
+        }
+    }
+
+    /**
+     * Separating notification volume from ring is NOT of aliasing the corresponding streams
+     * @param properties
+     */
+    private void onDeviceConfigChange(DeviceConfig.Properties properties) {
+        Set<String> changeSet = properties.getKeyset();
+        if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
+            boolean newNotifAliasRing = !properties.getBoolean(
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL);
+            if (mNotifAliasRing != newNotifAliasRing) {
+                mNotifAliasRing = newNotifAliasRing;
+                updateStreamVolumeAlias(true, TAG);
+            }
+        }
+    }
+
     /**
      * Called by handling of MSG_INIT_STREAMS_VOLUMES
      */
@@ -1259,6 +1299,8 @@
         // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it
         // relies on audio policy having correct ranges for volume indexes.
         mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+        // Link VGS on VSS
+        initVolumeStreamStates();
 
         // Call setRingerModeInt() to apply correct mute
         // state on streams affected by ringer mode.
@@ -1615,7 +1657,7 @@
 
         synchronized (mSettingsLock) {
             final int forDock = mDockAudioMediaEnabled ?
-                    AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE;
+                    AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE;
             mDeviceBroker.setForceUse_Async(AudioSystem.FOR_DOCK, forDock, "onAudioServerDied");
             sendEncodedSurroundMode(mContentResolver, "onAudioServerDied");
             sendEnabledSurroundFormats(mContentResolver, true);
@@ -2155,19 +2197,19 @@
                 AudioSystem.STREAM_ASSISTANT : AudioSystem.STREAM_MUSIC;
 
         if (mIsSingleVolume) {
-            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION;
+            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION.clone();
             dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
         } else if (mUseVolumeGroupAliases) {
-            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE;
+            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE.clone();
             dtmfStreamAlias = AudioSystem.STREAM_DTMF;
         } else {
             switch (mPlatformType) {
                 case AudioSystem.PLATFORM_VOICE:
-                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE;
+                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE.clone();
                     dtmfStreamAlias = AudioSystem.STREAM_RING;
                     break;
                 default:
-                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT;
+                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT.clone();
                     dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
             }
             if (!mNotifAliasRing) {
@@ -2236,9 +2278,10 @@
                 SENDMSG_QUEUE,
                 AudioSystem.FOR_DOCK,
                 mDockAudioMediaEnabled ?
-                        AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE,
+                        AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE,
                 new String("readDockAudioSettings"),
                 0);
+
     }
 
 
@@ -3307,15 +3350,7 @@
                 } else {
                     state = direction == AudioManager.ADJUST_MUTE;
                 }
-                for (int stream = 0; stream < mStreamStates.length; stream++) {
-                    if (streamTypeAlias == mStreamVolumeAlias[stream]) {
-                        if (!(readCameraSoundForced()
-                                    && (mStreamStates[stream].getStreamType()
-                                        == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
-                            mStreamStates[stream].mute(state);
-                        }
-                    }
-                }
+                muteAliasStreams(streamTypeAlias, state);
             } else if ((direction == AudioManager.ADJUST_RAISE) &&
                     !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
                 Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
@@ -3330,7 +3365,7 @@
                     // Unmute the stream if it was previously muted
                     if (direction == AudioManager.ADJUST_RAISE) {
                         // unmute immediately for volume up
-                        streamState.mute(false);
+                        muteAliasStreams(streamTypeAlias, false);
                     } else if (direction == AudioManager.ADJUST_LOWER) {
                         if (mIsSingleVolume) {
                             sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE,
@@ -3456,6 +3491,43 @@
         sendVolumeUpdate(streamType, oldIndex, newIndex, flags, device);
     }
 
+    /**
+     * Loops on aliasted stream, update the mute cache attribute of each
+     * {@see AudioService#VolumeStreamState}, and then apply the change.
+     * It prevents to unnecessary {@see AudioSystem#setStreamVolume} done for each stream
+     * and aliases before mute change changed and after.
+     */
+    private void muteAliasStreams(int streamAlias, boolean state) {
+        synchronized (VolumeStreamState.class) {
+            List<Integer> streamsToMute = new ArrayList<>();
+            for (int stream = 0; stream < mStreamStates.length; stream++) {
+                VolumeStreamState vss = mStreamStates[stream];
+                if (streamAlias == mStreamVolumeAlias[stream] && vss.isMutable()) {
+                    if (!(readCameraSoundForced()
+                            && (vss.getStreamType()
+                                    == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
+                        boolean changed = vss.mute(state, /* apply= */ false);
+                        if (changed) {
+                            streamsToMute.add(stream);
+                        }
+                    }
+                }
+            }
+            streamsToMute.forEach(streamToMute -> {
+                mStreamStates[streamToMute].doMute();
+                broadcastMuteSetting(streamToMute, state);
+            });
+        }
+    }
+
+    private void broadcastMuteSetting(int streamType, boolean isMuted) {
+        // Stream mute changed, fire the intent.
+        Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+        intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
+        intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, isMuted);
+        sendBroadcastToAll(intent);
+    }
+
     // Called after a delay when volume down is pressed while muted
     private void onUnmuteStream(int stream, int flags) {
         boolean wasMuted;
@@ -3560,8 +3632,19 @@
         return false;
     }
 
+    /**
+     * Update stream volume, ringer mode and mute status after a volume index change
+     * @param streamType
+     * @param index
+     * @param flags
+     * @param device the device for which the volume is changed
+     * @param caller
+     * @param hasModifyAudioSettings
+     * @param canChangeMute true if the origin of this event is one where the mute state should be
+     *                      updated following the change in volume index
+     */
     private void onSetStreamVolume(int streamType, int index, int flags, int device,
-            String caller, boolean hasModifyAudioSettings) {
+            String caller, boolean hasModifyAudioSettings, boolean canChangeMute) {
         final int stream = mStreamVolumeAlias[streamType];
         setStreamVolumeInt(stream, index, device, false, caller, hasModifyAudioSettings);
         // setting volume on ui sounds stream type also controls silent mode
@@ -3570,10 +3653,11 @@
             setRingerMode(getNewRingerMode(stream, index, flags),
                     TAG + ".onSetStreamVolume", false /*external*/);
         }
-        // setting non-zero volume for a muted stream unmutes the stream and vice versa,
+        // setting non-zero volume for a muted stream unmutes the stream and vice versa
         // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements
-        if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) {
-            mStreamStates[stream].mute(index == 0);
+        if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) && canChangeMute) {
+            // As adjustStreamVolume with muteAdjust flags mute/unmutes stream and aliased streams.
+            muteAliasStreams(stream, index == 0);
         }
     }
 
@@ -3629,29 +3713,27 @@
     }
 
 
-    /** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */
-    public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags,
+    /** @see AudioManager#setVolumeGroupVolumeIndex(int, int, int) */
+    public void setVolumeGroupVolumeIndex(int groupId, int index, int flags,
             String callingPackage, String attributionTag) {
         enforceModifyAudioRoutingPermission();
-        Objects.requireNonNull(attr, "attr must not be null");
-        final int volumeGroup = getVolumeGroupIdForAttributes(attr);
-        if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
-            Log.e(TAG, ": no volume group found for attributes " + attr.toString());
+        if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+            Log.e(TAG, ": no volume group found for id " + groupId);
             return;
         }
-        final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
+        VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
 
-        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(),
-                index/*val1*/, flags/*val2*/, callingPackage));
+        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, vgs.name(),
+                index, flags, callingPackage + ", user " + getCurrentUserId()));
 
         vgs.setVolumeIndex(index, flags);
 
         // For legacy reason, propagate to all streams associated to this volume group
-        for (final int groupedStream : vgs.getLegacyStreamTypes()) {
+        for (int groupedStream : vgs.getLegacyStreamTypes()) {
             try {
                 ensureValidStreamType(groupedStream);
             } catch (IllegalArgumentException e) {
-                Log.d(TAG, "volume group " + volumeGroup + " has internal streams (" + groupedStream
+                Log.d(TAG, "volume group " + groupId + " has internal streams (" + groupedStream
                         + "), do not change associated stream volume");
                 continue;
             }
@@ -3663,7 +3745,7 @@
 
     @Nullable
     private AudioVolumeGroup getAudioVolumeGroupById(int volumeGroupId) {
-        for (final AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) {
+        for (AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) {
             if (avg.getId() == volumeGroupId) {
                 return avg;
             }
@@ -3673,30 +3755,42 @@
         return null;
     }
 
-    /** @see AudioManager#getVolumeIndexForAttributes(attr) */
-    public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+    /** @see AudioManager#getVolumeGroupVolumeIndex(int) */
+    public int getVolumeGroupVolumeIndex(int groupId) {
         enforceModifyAudioRoutingPermission();
-        Objects.requireNonNull(attr, "attr must not be null");
-        final int volumeGroup = getVolumeGroupIdForAttributes(attr);
-        if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
-            throw new IllegalArgumentException("No volume group for attributes " + attr);
+        synchronized (VolumeStreamState.class) {
+            if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+                throw new IllegalArgumentException("No volume group for id " + groupId);
+            }
+            VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+            // Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero
+            // min but it mutable on permission condition.
+            return vgs.isMuted() ? 0 : vgs.getVolumeIndex();
         }
-        final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
-        return vgs.getVolumeIndex();
     }
 
-    /** @see AudioManager#getMaxVolumeIndexForAttributes(attr) */
-    public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+    /** @see AudioManager#getVolumeGroupMaxVolumeIndex(int) */
+    public int getVolumeGroupMaxVolumeIndex(int groupId) {
         enforceModifyAudioRoutingPermission();
-        Objects.requireNonNull(attr, "attr must not be null");
-        return AudioSystem.getMaxVolumeIndexForAttributes(attr);
+        synchronized (VolumeStreamState.class) {
+            if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+                throw new IllegalArgumentException("No volume group for id " + groupId);
+            }
+            VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+            return vgs.getMaxIndex();
+        }
     }
 
-    /** @see AudioManager#getMinVolumeIndexForAttributes(attr) */
-    public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+    /** @see AudioManager#getVolumeGroupMinVolumeIndex(int) */
+    public int getVolumeGroupMinVolumeIndex(int groupId) {
         enforceModifyAudioRoutingPermission();
-        Objects.requireNonNull(attr, "attr must not be null");
-        return AudioSystem.getMinVolumeIndexForAttributes(attr);
+        synchronized (VolumeStreamState.class) {
+            if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+                throw new IllegalArgumentException("No volume group for id " + groupId);
+            }
+            VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+            return vgs.getMinIndex();
+        }
     }
 
     /** @see AudioDeviceVolumeManager#setDeviceVolume(VolumeInfo, AudioDeviceAttributes)
@@ -3710,19 +3804,37 @@
         Objects.requireNonNull(ada);
         Objects.requireNonNull(callingPackage);
 
-        AudioService.sVolumeLogger.loglogi("setDeviceVolume" + " from:" + callingPackage + " "
-                + vi + " " + ada, TAG);
-
         if (!vi.hasStreamType()) {
             Log.e(TAG, "Unsupported non-stream type based VolumeInfo", new Exception());
             return;
         }
+
         int index = vi.getVolumeIndex();
         if (index == VolumeInfo.INDEX_NOT_SET && !vi.hasMuteCommand()) {
             throw new IllegalArgumentException(
                     "changing device volume requires a volume index or mute command");
         }
 
+        // force a cache clear to force reevaluating stream type to audio device selection
+        // that can interfere with the sending of the VOLUME_CHANGED_ACTION intent
+        // TODO change cache management to not rely only on invalidation, but on "do not trust"
+        //     moments when routing is in flux.
+        mAudioSystem.clearRoutingCache();
+
+        // log the current device that will be used when evaluating the sending of the
+        // 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, 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
         // for the device to min volume
@@ -3762,6 +3874,70 @@
                 callingPackage, /*attributionTag*/ null);
     }
 
+    /** @see AudioManager#adjustVolumeGroupVolume(int, int, int) */
+    public void adjustVolumeGroupVolume(int groupId, int direction, int flags,
+                                        String callingPackage) {
+        ensureValidDirection(direction);
+        if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+            Log.e(TAG, ": no volume group found for id " + groupId);
+            return;
+        }
+        VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+        // For compatibility reason, use stream API if group linked to a valid stream
+        boolean fallbackOnStream = false;
+        for (int stream : vgs.getLegacyStreamTypes()) {
+            try {
+                ensureValidStreamType(stream);
+            } catch (IllegalArgumentException e) {
+                Log.d(TAG, "volume group " + groupId + " has internal streams (" + stream
+                        + "), do not change associated stream volume");
+                continue;
+            }
+            // Note: Group and Stream does not share same convention, 0 is mute for stream,
+            // min index is acting as mute for Groups
+            if (vgs.isVssMuteBijective(stream)) {
+                adjustStreamVolume(stream, direction, flags, callingPackage);
+                if (isMuteAdjust(direction)) {
+                    // will be propagated to all aliased streams
+                    return;
+                }
+                fallbackOnStream = true;
+            }
+        }
+        if (fallbackOnStream) {
+            // Handled by at least one stream, will be propagated to group, bailing out.
+            return;
+        }
+        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_GROUP_VOL, vgs.name(),
+                direction, flags, callingPackage));
+        vgs.adjustVolume(direction, flags);
+    }
+
+    /** @see AudioManager#getLastAudibleVolumeGroupVolume(int) */
+    public int getLastAudibleVolumeGroupVolume(int groupId) {
+        enforceQueryStatePermission();
+        synchronized (VolumeStreamState.class) {
+            if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+                Log.e(TAG, ": no volume group found for id " + groupId);
+                return 0;
+            }
+            VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+            return vgs.getVolumeIndex();
+        }
+    }
+
+    /** @see AudioManager#isVolumeGroupMuted(int) */
+    public boolean isVolumeGroupMuted(int groupId) {
+        synchronized (VolumeStreamState.class) {
+            if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+                Log.e(TAG, ": no volume group found for id " + groupId);
+                return false;
+            }
+            VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+            return vgs.isMuted();
+        }
+    }
+
     /** @see AudioManager#setStreamVolume(int, int, int)
      * Part of service interface, check permissions here */
     public void setStreamVolumeWithAttribution(int streamType, int index, int flags,
@@ -3806,11 +3982,11 @@
             return;
         }
 
-        final AudioEventLogger.Event event = (device == null)
-                ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
-                    index/*val1*/, flags/*val2*/, callingPackage)
-                : new DeviceVolumeEvent(streamType, index, device, callingPackage);
-        sVolumeLogger.log(event);
+        if (device == null) {
+            // call was already logged in setDeviceVolume()
+            sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
+                    index/*val1*/, flags/*val2*/, callingPackage));
+        }
         setStreamVolume(streamType, index, flags, device,
                 callingPackage, callingPackage, attributionTag,
                 Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission());
@@ -4201,7 +4377,10 @@
                 mPendingVolumeCommand = new StreamVolumeCommand(
                                                     streamType, index, flags, device);
             } else {
-                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings);
+                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
+                        // ada is non-null when called from setDeviceVolume,
+                        // which shouldn't update the mute state
+                        ada == null /*canChangeMute*/);
                 index = mStreamStates[streamType].getIndex(device);
             }
         }
@@ -4211,32 +4390,11 @@
                 maybeSendSystemAudioStatusCommand(false);
             }
         }
-        sendVolumeUpdate(streamType, oldIndex, index, flags, device);
-    }
-
-
-
-    private int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) {
-        Objects.requireNonNull(attributes, "attributes must not be null");
-        int volumeGroupId = getVolumeGroupIdForAttributesInt(attributes);
-        if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
-            return volumeGroupId;
+        if (ada == null) {
+            // only non-null when coming here from setDeviceVolume
+            // TODO change test to check early if device is current device or not
+            sendVolumeUpdate(streamType, oldIndex, index, flags, device);
         }
-        // The default volume group is the one hosted by default product strategy, i.e.
-        // supporting Default Attributes
-        return getVolumeGroupIdForAttributesInt(AudioProductStrategy.getDefaultAttributes());
-    }
-
-    private int getVolumeGroupIdForAttributesInt(@NonNull AudioAttributes attributes) {
-        Objects.requireNonNull(attributes, "attributes must not be null");
-        for (final AudioProductStrategy productStrategy :
-                AudioProductStrategy.getAudioProductStrategies()) {
-            int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes);
-            if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
-                return volumeGroupId;
-            }
-        }
-        return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
     }
 
     private void dispatchAbsoluteVolumeChanged(int streamType, AbsoluteVolumeDeviceInfo deviceInfo,
@@ -4271,7 +4429,6 @@
         }
     }
 
-
     // No ringer or zen muted stream volumes can be changed unless it'll exit dnd
     private boolean volumeAdjustmentAllowedByDnd(int streamTypeAlias, int flags) {
         switch (mNm.getZenMode()) {
@@ -4963,7 +5120,7 @@
     }
 
     private void setRingerMode(int ringerMode, String caller, boolean external) {
-        if (mUseFixedVolume || mIsSingleVolume) {
+        if (mUseFixedVolume || mIsSingleVolume || mUseVolumeGroupAliases) {
             return;
         }
         if (caller == null || caller.length() == 0) {
@@ -5046,7 +5203,8 @@
             if (!shouldMute) {
                 // unmute
                 // ring and notifications volume should never be 0 when not silenced
-                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING) {
+                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING
+                        || mStreamVolumeAlias[streamType] == AudioSystem.STREAM_NOTIFICATION) {
                     synchronized (VolumeStreamState.class) {
                         final VolumeStreamState vss = mStreamStates[streamType];
                         for (int i = 0; i < vss.mIndexMap.size(); i++) {
@@ -5779,6 +5937,8 @@
             }
         }
 
+        readVolumeGroupsSettings(userSwitch);
+
         // apply new ringer mode before checking volume for alias streams so that streams
         // muted by ringer mode have the correct volume
         setRingerModeInt(getRingerModeInternal(), false);
@@ -5796,54 +5956,22 @@
             }
         }
 
-        readVolumeGroupsSettings();
-
         if (DEBUG_VOL) {
             Log.d(TAG, "Restoring device volume behavior");
         }
         restoreDeviceVolumeBehavior();
     }
 
-    private static final int[] VALID_COMMUNICATION_DEVICE_TYPES = {
-        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
-        AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
-        AudioDeviceInfo.TYPE_WIRED_HEADSET,
-        AudioDeviceInfo.TYPE_USB_HEADSET,
-        AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
-        AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
-        AudioDeviceInfo.TYPE_HEARING_AID,
-        AudioDeviceInfo.TYPE_BLE_HEADSET,
-        AudioDeviceInfo.TYPE_USB_DEVICE,
-        AudioDeviceInfo.TYPE_BLE_SPEAKER,
-        AudioDeviceInfo.TYPE_LINE_ANALOG,
-        AudioDeviceInfo.TYPE_HDMI,
-        AudioDeviceInfo.TYPE_AUX_LINE
-    };
-
-    private boolean isValidCommunicationDevice(AudioDeviceInfo device) {
-        for (int type : VALID_COMMUNICATION_DEVICE_TYPES) {
-            if (device.getType() == type) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     /** @see AudioManager#getAvailableCommunicationDevices(int) */
     public int[] getAvailableCommunicationDeviceIds() {
-        ArrayList<Integer> deviceIds = new ArrayList<>();
-        AudioDeviceInfo[] devices = AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
-        for (AudioDeviceInfo device : devices) {
-            if (isValidCommunicationDevice(device)) {
-                deviceIds.add(device.getId());
-            }
-        }
-        return deviceIds.stream().mapToInt(Integer::intValue).toArray();
+        List<AudioDeviceInfo> commDevices = AudioDeviceBroker.getAvailableCommunicationDevices();
+        return commDevices.stream().mapToInt(AudioDeviceInfo::getId).toArray();
     }
-        /**
-         * @see AudioManager#setCommunicationDevice(int)
-         * @see AudioManager#clearCommunicationDevice()
-         */
+
+    /**
+     * @see AudioManager#setCommunicationDevice(int)
+     * @see AudioManager#clearCommunicationDevice()
+     */
     public boolean setCommunicationDevice(IBinder cb, int portId) {
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -5852,9 +5980,10 @@
         if (portId != 0) {
             device = AudioManager.getDeviceForPortId(portId, AudioManager.GET_DEVICES_OUTPUTS);
             if (device == null) {
-                throw new IllegalArgumentException("invalid portID " + portId);
+                Log.w(TAG, "setCommunicationDevice: invalid portID " + portId);
+                return false;
             }
-            if (!isValidCommunicationDevice(device)) {
+            if (!AudioDeviceBroker.isValidCommunicationDevice(device)) {
                 throw new IllegalArgumentException("invalid device type " + device.getType());
             }
         }
@@ -5897,13 +6026,15 @@
 
     /** @see AudioManager#getCommunicationDevice() */
     public int getCommunicationDevice() {
+        int deviceId = 0;
         final long ident = Binder.clearCallingIdentity();
-        AudioDeviceInfo device = mDeviceBroker.getCommunicationDevice();
-        Binder.restoreCallingIdentity(ident);
-        if (device == null) {
-            return 0;
+        try {
+            AudioDeviceInfo device = mDeviceBroker.getCommunicationDevice();
+            deviceId = device != null ? device.getId() : 0;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
         }
-        return device.getId();
+        return deviceId;
     }
 
     /** @see AudioManager#addOnCommunicationDeviceChangedListener(
@@ -7275,6 +7406,7 @@
             try {
                 // if no valid attributes, this volume group is not controllable, throw exception
                 ensureValidAttributes(avg);
+                sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
             } catch (IllegalArgumentException e) {
                 // Volume Groups without attributes are not controllable through set/get volume
                 // using attributes. Do not append them.
@@ -7283,11 +7415,10 @@
                 }
                 continue;
             }
-            sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
         }
         for (int i = 0; i < sVolumeGroupStates.size(); i++) {
             final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
-            vgs.applyAllVolumes();
+            vgs.applyAllVolumes(/* userSwitch= */ false);
         }
     }
 
@@ -7300,14 +7431,22 @@
         }
     }
 
-    private void readVolumeGroupsSettings() {
-        if (DEBUG_VOL) {
-            Log.v(TAG, "readVolumeGroupsSettings");
-        }
-        for (int i = 0; i < sVolumeGroupStates.size(); i++) {
-            final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
-            vgs.readSettings();
-            vgs.applyAllVolumes();
+    private void readVolumeGroupsSettings(boolean userSwitch) {
+        synchronized (mSettingsLock) {
+            synchronized (VolumeStreamState.class) {
+                if (DEBUG_VOL) {
+                    Log.d(TAG, "readVolumeGroupsSettings userSwitch=" + userSwitch);
+                }
+                for (int i = 0; i < sVolumeGroupStates.size(); i++) {
+                    VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
+                    // as for STREAM_MUSIC, preserve volume from one user to the next.
+                    if (!(userSwitch && vgs.isMusic())) {
+                        vgs.clearIndexCache();
+                        vgs.readSettings();
+                    }
+                    vgs.applyAllVolumes(userSwitch);
+                }
+            }
         }
     }
 
@@ -7318,7 +7457,7 @@
         }
         for (int i = 0; i < sVolumeGroupStates.size(); i++) {
             final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
-            vgs.applyAllVolumes();
+            vgs.applyAllVolumes(false/*userSwitch*/);
         }
     }
 
@@ -7331,17 +7470,34 @@
         }
     }
 
+    private static boolean isCallStream(int stream) {
+        return stream == AudioSystem.STREAM_VOICE_CALL
+                || stream == AudioSystem.STREAM_BLUETOOTH_SCO;
+    }
+
+    private static int getVolumeGroupForStreamType(int stream) {
+        AudioAttributes attributes =
+                AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType(stream);
+        if (attributes.equals(new AudioAttributes.Builder().build())) {
+            return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+        }
+        return AudioProductStrategy.getVolumeGroupIdForAudioAttributes(
+                attributes, /* fallbackOnDefault= */ false);
+    }
+
     // NOTE: Locking order for synchronized objects related to volume management:
     //  1     mSettingsLock
-    //  2       VolumeGroupState.class
+    //  2       VolumeStreamState.class
     private class VolumeGroupState {
         private final AudioVolumeGroup mAudioVolumeGroup;
         private final SparseIntArray mIndexMap = new SparseIntArray(8);
         private int mIndexMin;
         private int mIndexMax;
-        private int mLegacyStreamType = AudioSystem.STREAM_DEFAULT;
+        private boolean mHasValidStreamType = false;
         private int mPublicStreamType = AudioSystem.STREAM_MUSIC;
         private AudioAttributes mAudioAttributes = AudioProductStrategy.getDefaultAttributes();
+        private boolean mIsMuted = false;
+        private final String mSettingName;
 
         // No API in AudioSystem to get a device from strategy or from attributes.
         // Need a valid public stream type to use current API getDeviceForStream
@@ -7355,20 +7511,22 @@
                 Log.v(TAG, "VolumeGroupState for " + avg.toString());
             }
             // mAudioAttributes is the default at this point
-            for (final AudioAttributes aa : avg.getAudioAttributes()) {
+            for (AudioAttributes aa : avg.getAudioAttributes()) {
                 if (!aa.equals(mAudioAttributes)) {
                     mAudioAttributes = aa;
                     break;
                 }
             }
-            final int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes();
+            int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes();
+            String streamSettingName = "";
             if (streamTypes.length != 0) {
                 // Uses already initialized MIN / MAX if a stream type is attached to group
-                mLegacyStreamType = streamTypes[0];
-                for (final int streamType : streamTypes) {
+                for (int streamType : streamTypes) {
                     if (streamType != AudioSystem.STREAM_DEFAULT
                             && streamType < AudioSystem.getNumStreamTypes()) {
                         mPublicStreamType = streamType;
+                        mHasValidStreamType = true;
+                        streamSettingName = System.VOLUME_SETTINGS_INT[mPublicStreamType];
                         break;
                     }
                 }
@@ -7378,10 +7536,10 @@
                 mIndexMin = AudioSystem.getMinVolumeIndexForAttributes(mAudioAttributes);
                 mIndexMax = AudioSystem.getMaxVolumeIndexForAttributes(mAudioAttributes);
             } else {
-                Log.e(TAG, "volume group: " + mAudioVolumeGroup.name()
+                throw new IllegalArgumentException("volume group: " + mAudioVolumeGroup.name()
                         + " has neither valid attributes nor valid stream types assigned");
-                return;
             }
+            mSettingName = !streamSettingName.isEmpty() ? streamSettingName : ("volume_" + name());
             // Load volume indexes from data base
             readSettings();
         }
@@ -7394,40 +7552,149 @@
             return mAudioVolumeGroup.name();
         }
 
+        /**
+         * Volume group with non null minimum index are considered as non mutable, thus
+         * bijectivity is broken with potential associated stream type.
+         * VOICE_CALL stream has minVolumeIndex > 0  but can be muted directly by an
+         * app that has MODIFY_PHONE_STATE permission.
+         */
+        private boolean isVssMuteBijective(int stream) {
+            return isStreamAffectedByMute(stream)
+                    && (getMinIndex() == (mStreamStates[stream].mIndexMin + 5) / 10)
+                    && (getMinIndex() == 0 || isCallStream(stream));
+        }
+
+        private boolean isMutable() {
+            return mIndexMin == 0 || (mHasValidStreamType && isVssMuteBijective(mPublicStreamType));
+        }
+        /**
+         * Mute/unmute the volume group
+         * @param muted the new mute state
+         */
+        @GuardedBy("AudioService.VolumeStreamState.class")
+        public boolean mute(boolean muted) {
+            if (!isMutable()) {
+                // Non mutable volume group
+                if (DEBUG_VOL) {
+                    Log.d(TAG, "invalid mute on unmutable volume group " + name());
+                }
+                return false;
+            }
+            boolean changed = (mIsMuted != muted);
+            // As for VSS, mute shall apply minIndex to all devices found in IndexMap and default.
+            if (changed) {
+                mIsMuted = muted;
+                applyAllVolumes(false /*userSwitch*/);
+            }
+            return changed;
+        }
+
+        public boolean isMuted() {
+            return mIsMuted;
+        }
+
+        public void adjustVolume(int direction, int flags) {
+            synchronized (VolumeStreamState.class) {
+                int device = getDeviceForVolume();
+                int previousIndex = getIndex(device);
+                if (isMuteAdjust(direction) && !isMutable()) {
+                    // Non mutable volume group
+                    if (DEBUG_VOL) {
+                        Log.d(TAG, "invalid mute on unmutable volume group " + name());
+                    }
+                    return;
+                }
+                switch (direction) {
+                    case AudioManager.ADJUST_TOGGLE_MUTE: {
+                        // Note: If muted by volume 0, unmute will restore volume 0.
+                        mute(!mIsMuted);
+                        break;
+                    }
+                    case AudioManager.ADJUST_UNMUTE:
+                        // Note: If muted by volume 0, unmute will restore volume 0.
+                        mute(false);
+                        break;
+                    case AudioManager.ADJUST_MUTE:
+                        // May be already muted by setvolume 0, prevent from setting same value
+                        if (previousIndex != 0) {
+                            // bypass persist
+                            mute(true);
+                        }
+                        mIsMuted = true;
+                        break;
+                    case AudioManager.ADJUST_RAISE:
+                        // As for stream, RAISE during mute will increment the index
+                        setVolumeIndex(Math.min(previousIndex + 1, mIndexMax),  device, flags);
+                        break;
+                    case AudioManager.ADJUST_LOWER:
+                        // For stream, ADJUST_LOWER on a muted VSS is a no-op
+                        // If we decide to unmute on ADJUST_LOWER, cannot fallback on
+                        // adjustStreamVolume for group associated to legacy stream type
+                        if (isMuted() && previousIndex != 0) {
+                            mute(false);
+                        } else {
+                            int newIndex = Math.max(previousIndex - 1, mIndexMin);
+                            setVolumeIndex(newIndex, device, flags);
+                        }
+                        break;
+                }
+            }
+        }
+
         public int getVolumeIndex() {
-            return getIndex(getDeviceForVolume());
+            synchronized (VolumeStreamState.class) {
+                return getIndex(getDeviceForVolume());
+            }
         }
 
         public void setVolumeIndex(int index, int flags) {
-            if (mUseFixedVolume) {
-                return;
+            synchronized (VolumeStreamState.class) {
+                if (mUseFixedVolume) {
+                    return;
+                }
+                setVolumeIndex(index, getDeviceForVolume(), flags);
             }
-            setVolumeIndex(index, getDeviceForVolume(), flags);
         }
 
+        @GuardedBy("AudioService.VolumeStreamState.class")
         private void setVolumeIndex(int index, int device, int flags) {
-            // Set the volume index
-            setVolumeIndexInt(index, device, flags);
-
-            // Update local cache
-            mIndexMap.put(device, index);
-
-            // update data base - post a persist volume group msg
-            sendMsg(mAudioHandler,
-                    MSG_PERSIST_VOLUME_GROUP,
-                    SENDMSG_QUEUE,
-                    device,
-                    0,
-                    this,
-                    PERSIST_DELAY);
+            // Update cache & persist (muted by volume 0 shall be persisted)
+            updateVolumeIndex(index, device);
+            // setting non-zero volume for a muted stream unmutes the stream and vice versa,
+            boolean changed = mute(index == 0);
+            if (!changed) {
+                // Set the volume index only if mute operation is a no-op
+                index = getValidIndex(index);
+                setVolumeIndexInt(index, device, flags);
+            }
         }
 
+        @GuardedBy("AudioService.VolumeStreamState.class")
+        public void updateVolumeIndex(int index, int device) {
+            // Filter persistency if already exist and the index has not changed
+            if (mIndexMap.indexOfKey(device) < 0 || mIndexMap.get(device) != index) {
+                // Update local cache
+                mIndexMap.put(device, getValidIndex(index));
+
+                // update data base - post a persist volume group msg
+                sendMsg(mAudioHandler,
+                        MSG_PERSIST_VOLUME_GROUP,
+                        SENDMSG_QUEUE,
+                        device,
+                        0,
+                        this,
+                        PERSIST_DELAY);
+            }
+        }
+
+        @GuardedBy("AudioService.VolumeStreamState.class")
         private void setVolumeIndexInt(int index, int device, int flags) {
             // Reflect mute state of corresponding stream by forcing index to 0 if muted
             // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
             // This allows RX path muting by the audio HAL only when explicitly muted but not when
             // index is just set to 0 to repect BT requirements
-            if (mStreamStates[mPublicStreamType].isFullyMuted()) {
+            if (mHasValidStreamType && isVssMuteBijective(mPublicStreamType)
+                    && mStreamStates[mPublicStreamType].isFullyMuted()) {
                 index = 0;
             } else if (mPublicStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0) {
                 index = 1;
@@ -7436,18 +7703,16 @@
             AudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, device);
         }
 
-        public int getIndex(int device) {
-            synchronized (VolumeGroupState.class) {
-                int index = mIndexMap.get(device, -1);
-                // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
-                return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
-            }
+        @GuardedBy("AudioService.VolumeStreamState.class")
+        private int getIndex(int device) {
+            int index = mIndexMap.get(device, -1);
+            // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
+            return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
         }
 
-        public boolean hasIndexForDevice(int device) {
-            synchronized (VolumeGroupState.class) {
-                return (mIndexMap.get(device, -1) != -1);
-            }
+        @GuardedBy("AudioService.VolumeStreamState.class")
+        private boolean hasIndexForDevice(int device) {
+            return (mIndexMap.get(device, -1) != -1);
         }
 
         public int getMaxIndex() {
@@ -7458,55 +7723,108 @@
             return mIndexMin;
         }
 
-        private boolean isValidLegacyStreamType() {
-            return (mLegacyStreamType != AudioSystem.STREAM_DEFAULT)
-                    && (mLegacyStreamType < mStreamStates.length);
+        private boolean isValidStream(int stream) {
+            return (stream != AudioSystem.STREAM_DEFAULT) && (stream < mStreamStates.length);
         }
 
-        public void applyAllVolumes() {
-            synchronized (VolumeGroupState.class) {
-                int deviceForStream = AudioSystem.DEVICE_NONE;
-                int volumeIndexForStream = 0;
-                if (isValidLegacyStreamType()) {
-                    // Prevent to apply settings twice when group is associated to public stream
-                    deviceForStream = getDeviceForStream(mLegacyStreamType);
-                    volumeIndexForStream = getStreamVolume(mLegacyStreamType);
-                }
+        public boolean isMusic() {
+            return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC;
+        }
+
+        public void applyAllVolumes(boolean userSwitch) {
+            String caller = "from vgs";
+            synchronized (VolumeStreamState.class) {
                 // apply device specific volumes first
-                int index;
                 for (int i = 0; i < mIndexMap.size(); i++) {
-                    final int device = mIndexMap.keyAt(i);
+                    int device = mIndexMap.keyAt(i);
+                    int index = mIndexMap.valueAt(i);
+                    boolean synced = false;
                     if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
-                        index = mIndexMap.valueAt(i);
-                        if (device == deviceForStream && volumeIndexForStream == index) {
-                            continue;
+                        for (int stream : getLegacyStreamTypes()) {
+                            if (isValidStream(stream)) {
+                                boolean streamMuted = mStreamStates[stream].mIsMuted;
+                                int deviceForStream = getDeviceForStream(stream);
+                                int indexForStream =
+                                        (mStreamStates[stream].getIndex(deviceForStream) + 5) / 10;
+                                if (device == deviceForStream) {
+                                    if (indexForStream == index && (isMuted() == streamMuted)
+                                            && isVssMuteBijective(stream)) {
+                                        synced = true;
+                                        continue;
+                                    }
+                                    if (indexForStream != index) {
+                                        mStreamStates[stream].setIndex(index * 10, device, caller,
+                                                true /*hasModifyAudioSettings*/);
+                                    }
+                                    if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+                                        mStreamStates[stream].mute(isMuted());
+                                    }
+                                }
+                            }
                         }
-                        if (DEBUG_VOL) {
-                            Log.v(TAG, "applyAllVolumes: restore index " + index + " for group "
-                                    + mAudioVolumeGroup.name() + " and device "
-                                    + AudioSystem.getOutputDeviceName(device));
+                        if (!synced) {
+                            if (DEBUG_VOL) {
+                                Log.d(TAG, "applyAllVolumes: apply index " + index + ", group "
+                                        + mAudioVolumeGroup.name() + " and device "
+                                        + AudioSystem.getOutputDeviceName(device));
+                            }
+                            setVolumeIndexInt(isMuted() ? 0 : index, device, 0 /*flags*/);
                         }
-                        setVolumeIndexInt(index, device, 0 /*flags*/);
                     }
                 }
                 // apply default volume last: by convention , default device volume will be used
                 // by audio policy manager if no explicit volume is present for a given device type
-                index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
-                if (DEBUG_VOL) {
-                    Log.v(TAG, "applyAllVolumes: restore default device index " + index
-                            + " for group " + mAudioVolumeGroup.name());
-                }
-                if (isValidLegacyStreamType()) {
-                    int defaultStreamIndex = (mStreamStates[mLegacyStreamType]
-                            .getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10;
-                    if (defaultStreamIndex == index) {
-                        return;
+                int index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
+                boolean synced = false;
+                int deviceForVolume = getDeviceForVolume();
+                boolean forceDeviceSync = userSwitch && (mIndexMap.indexOfKey(deviceForVolume) < 0);
+                for (int stream : getLegacyStreamTypes()) {
+                    if (isValidStream(stream)) {
+                        boolean streamMuted = mStreamStates[stream].mIsMuted;
+                        int defaultStreamIndex = (mStreamStates[stream].getIndex(
+                                        AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10;
+                        if (forceDeviceSync) {
+                            mStreamStates[stream].setIndex(index * 10, deviceForVolume, caller,
+                                    true /*hasModifyAudioSettings*/);
+                        }
+                        if (defaultStreamIndex == index && (isMuted() == streamMuted)
+                                && isVssMuteBijective(stream)) {
+                            synced = true;
+                            continue;
+                        }
+                        if (defaultStreamIndex != index) {
+                            mStreamStates[stream].setIndex(
+                                    index * 10, AudioSystem.DEVICE_OUT_DEFAULT, caller,
+                                    true /*hasModifyAudioSettings*/);
+                        }
+                        if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+                            mStreamStates[stream].mute(isMuted());
+                        }
                     }
                 }
-                setVolumeIndexInt(index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/);
+                if (!synced) {
+                    if (DEBUG_VOL) {
+                        Log.d(TAG, "applyAllVolumes: apply default device index " + index
+                                + ", group " + mAudioVolumeGroup.name());
+                    }
+                    setVolumeIndexInt(
+                            isMuted() ? 0 : index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/);
+                }
+                if (forceDeviceSync) {
+                    if (DEBUG_VOL) {
+                        Log.d(TAG, "applyAllVolumes: forceDeviceSync index " + index
+                                + ", device " + AudioSystem.getOutputDeviceName(deviceForVolume)
+                                + ", group " + mAudioVolumeGroup.name());
+                    }
+                    setVolumeIndexInt(isMuted() ? 0 : index, deviceForVolume, 0);
+                }
             }
         }
 
+        public void clearIndexCache() {
+            mIndexMap.clear();
+        }
+
         private void persistVolumeGroup(int device) {
             if (mUseFixedVolume) {
                 return;
@@ -7515,21 +7833,19 @@
                 Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device) + " for group "
                         + mAudioVolumeGroup.name()
                         + ", device " + AudioSystem.getOutputDeviceName(device)
-                        + " and User=" + ActivityManager.getCurrentUser());
+                        + " and User=" + getCurrentUserId());
             }
             boolean success = mSettings.putSystemIntForUser(mContentResolver,
                     getSettingNameForDevice(device),
                     getIndex(device),
-                    UserHandle.USER_CURRENT);
+                    isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
             if (!success) {
                 Log.e(TAG, "persistVolumeGroup failed for group " +  mAudioVolumeGroup.name());
             }
         }
 
         public void readSettings() {
-            synchronized (VolumeGroupState.class) {
-                // First clear previously loaded (previous user?) settings
-                mIndexMap.clear();
+            synchronized (VolumeStreamState.class) {
                 // force maximum volume on all streams if fixed volume property is set
                 if (mUseFixedVolume) {
                     mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
@@ -7544,7 +7860,8 @@
                     int index;
                     String name = getSettingNameForDevice(device);
                     index = mSettings.getSystemIntForUser(
-                            mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT);
+                            mContentResolver, name, defaultIndex,
+                            isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
                     if (index == -1) {
                         continue;
                     }
@@ -7555,13 +7872,14 @@
                     if (DEBUG_VOL) {
                         Log.v(TAG, "readSettings: found stored index " + getValidIndex(index)
                                  + " for group " + mAudioVolumeGroup.name() + ", device: " + name
-                                 + ", User=" + ActivityManager.getCurrentUser());
+                                 + ", User=" + getCurrentUserId());
                     }
                     mIndexMap.put(device, getValidIndex(index));
                 }
             }
         }
 
+        @GuardedBy("AudioService.VolumeStreamState.class")
         private int getValidIndex(int index) {
             if (index < mIndexMin) {
                 return mIndexMin;
@@ -7572,15 +7890,17 @@
         }
 
         public @NonNull String getSettingNameForDevice(int device) {
-            final String suffix = AudioSystem.getOutputDeviceName(device);
+            String suffix = AudioSystem.getOutputDeviceName(device);
             if (suffix.isEmpty()) {
-                return mAudioVolumeGroup.name();
+                return mSettingName;
             }
-            return mAudioVolumeGroup.name() + "_" + AudioSystem.getOutputDeviceName(device);
+            return mSettingName + "_" + AudioSystem.getOutputDeviceName(device);
         }
 
         private void dump(PrintWriter pw) {
             pw.println("- VOLUME GROUP " + mAudioVolumeGroup.name() + ":");
+            pw.print("   Muted: ");
+            pw.println(mIsMuted);
             pw.print("   Min: ");
             pw.println(mIndexMin);
             pw.print("   Max: ");
@@ -7590,9 +7910,9 @@
                 if (i > 0) {
                     pw.print(", ");
                 }
-                final int device = mIndexMap.keyAt(i);
+                int device = mIndexMap.keyAt(i);
                 pw.print(Integer.toHexString(device));
-                final String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
+                String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
                         : AudioSystem.getOutputDeviceName(device);
                 if (!deviceName.isEmpty()) {
                     pw.print(" (");
@@ -7605,7 +7925,7 @@
             pw.println();
             pw.print("   Devices: ");
             int n = 0;
-            final int devices = getDeviceForVolume();
+            int devices = getDeviceForVolume();
             for (int device : AudioSystem.DEVICE_OUT_ALL_SET) {
                 if ((devices & device) == device) {
                     if (n++ > 0) {
@@ -7614,6 +7934,10 @@
                     pw.print(AudioSystem.getOutputDeviceName(device));
                 }
             }
+            pw.println();
+            pw.print("   Streams: ");
+            Arrays.stream(getLegacyStreamTypes())
+                    .forEach(stream -> pw.print(AudioSystem.streamToString(stream) + " "));
         }
     }
 
@@ -7625,13 +7949,14 @@
     //  4       VolumeStreamState.class
     private class VolumeStreamState {
         private final int mStreamType;
+        private VolumeGroupState mVolumeGroupState = null;
         private int mIndexMin;
         // min index when user doesn't have permission to change audio settings
         private int mIndexMinNoPerm;
         private int mIndexMax;
 
-        private boolean mIsMuted;
-        private boolean mIsMutedInternally;
+        private boolean mIsMuted = false;
+        private boolean mIsMutedInternally = false;
         private String mVolumeIndexSettingName;
         @NonNull private Set<Integer> mObservedDeviceSet = new TreeSet<>();
 
@@ -7689,6 +8014,15 @@
         }
 
         /**
+         * Associate a {@link volumeGroupState} on the {@link VolumeStreamState}.
+         * <p> It helps to synchronize the index, mute attributes on the maching
+         * {@link volumeGroupState}
+         * @param volumeGroupState matching the {@link VolumeStreamState}
+         */
+        public void setVolumeGroupState(VolumeGroupState volumeGroupState) {
+            mVolumeGroupState = volumeGroupState;
+        }
+        /**
          * Update the minimum index that can be used without MODIFY_AUDIO_SETTINGS permission
          * @param index minimum index expressed in "UI units", i.e. no 10x factor
          */
@@ -7946,6 +8280,9 @@
                 }
             }
             if (changed) {
+                // If associated to volume group, update group cache
+                updateVolumeGroupIndex(device, /* forceMuteState= */ false);
+
                 oldIndex = (oldIndex + 5) / 10;
                 index = (index + 5) / 10;
                 // log base stream changes to the event log
@@ -7963,6 +8300,8 @@
                     mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
                     mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
                             mStreamVolumeAlias[mStreamType]);
+                    AudioService.sVolumeLogger.log(new VolChangedBroadcastEvent(
+                            mStreamType, mStreamVolumeAlias[mStreamType], index));
                     sendBroadcastToAll(mVolumeChanged);
                 }
             }
@@ -8046,6 +8385,29 @@
             }
         }
 
+        // If associated to volume group, update group cache
+        private void updateVolumeGroupIndex(int device, boolean forceMuteState) {
+            synchronized (VolumeStreamState.class) {
+                if (mVolumeGroupState != null) {
+                    int groupIndex = (getIndex(device) + 5) / 10;
+                    if (DEBUG_VOL) {
+                        Log.d(TAG, "updateVolumeGroupIndex for stream " + mStreamType
+                                + ", muted=" + mIsMuted + ", device=" + device + ", index="
+                                + getIndex(device) + ", group " + mVolumeGroupState.name()
+                                + " Muted=" + mVolumeGroupState.isMuted() + ", Index=" + groupIndex
+                                + ", forceMuteState=" + forceMuteState);
+                    }
+                    mVolumeGroupState.updateVolumeIndex(groupIndex, device);
+                    // Only propage mute of stream when applicable
+                    if (isMutable()) {
+                        // For call stream, align mute only when muted, not when index is set to 0
+                        mVolumeGroupState.mute(
+                                forceMuteState ? mIsMuted : groupIndex == 0 || mIsMuted);
+                    }
+                }
+            }
+        }
+
         /**
          * Mute/unmute the stream
          * @param state the new mute state
@@ -8054,27 +8416,10 @@
         public boolean mute(boolean state) {
             boolean changed = false;
             synchronized (VolumeStreamState.class) {
-                if (state != mIsMuted) {
-                    changed = true;
-                    mIsMuted = state;
-
-                    // Set the new mute volume. This propagates the values to
-                    // the audio system, otherwise the volume won't be changed
-                    // at the lower level.
-                    sendMsg(mAudioHandler,
-                            MSG_SET_ALL_VOLUMES,
-                            SENDMSG_QUEUE,
-                            0,
-                            0,
-                            this, 0);
-                }
+                changed = mute(state, true);
             }
             if (changed) {
-                // Stream mute changed, fire the intent.
-                Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
-                intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType);
-                intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, state);
-                sendBroadcastToAll(intent);
+                broadcastMuteSetting(mStreamType, state);
             }
             return changed;
         }
@@ -8106,6 +8451,50 @@
             return mIsMuted || mIsMutedInternally;
         }
 
+
+        private boolean isMutable() {
+            return isStreamAffectedByMute(mStreamType)
+                    && (mIndexMin == 0 || isCallStream(mStreamType));
+        }
+
+        /**
+         * Mute/unmute the stream
+         * @param state the new mute state
+         * @param apply true to propagate to HW, or false just to update the cache. May be needed
+         * to mute a stream and its aliases as applyAllVolume will force settings to aliases.
+         * It prevents unnecessary calls to {@see AudioSystem#setStreamVolume}
+         * @return true if the mute state was changed
+         */
+        public boolean mute(boolean state, boolean apply) {
+            synchronized (VolumeStreamState.class) {
+                boolean changed = state != mIsMuted;
+                if (changed) {
+                    mIsMuted = state;
+                    if (apply) {
+                        doMute();
+                    }
+                }
+                return changed;
+            }
+        }
+
+        public void doMute() {
+            synchronized (VolumeStreamState.class) {
+                // If associated to volume group, update group cache
+                updateVolumeGroupIndex(getDeviceForStream(mStreamType), /* forceMuteState= */ true);
+
+                // Set the new mute volume. This propagates the values to
+                // the audio system, otherwise the volume won't be changed
+                // at the lower level.
+                sendMsg(mAudioHandler,
+                        MSG_SET_ALL_VOLUMES,
+                        SENDMSG_QUEUE,
+                        0,
+                        0,
+                        this, 0);
+            }
+        }
+
         public int getStreamType() {
             return mStreamType;
         }
@@ -8175,6 +8564,9 @@
             pw.println();
             pw.print("   Devices: ");
             pw.print(AudioSystem.deviceSetToString(getDeviceSetForStream(mStreamType)));
+            pw.println();
+            pw.print("   Volume Group: ");
+            pw.println(mVolumeGroupState != null ? mVolumeGroupState.name() : "n/a");
         }
     }
 
@@ -9841,7 +10233,7 @@
     private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000;
 
     private static final String ACTION_CHECK_MUSIC_ACTIVE =
-            AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
+            "com.android.server.audio.action.CHECK_MUSIC_ACTIVE";
     private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
 
     private int safeMediaVolumeIndex(int device) {
@@ -9917,7 +10309,8 @@
                                   mPendingVolumeCommand.mIndex,
                                   mPendingVolumeCommand.mFlags,
                                   mPendingVolumeCommand.mDevice,
-                                  callingPackage, true /*hasModifyAudioSettings*/);
+                                  callingPackage, true /*hasModifyAudioSettings*/,
+                                  true /*canChangeMute*/);
                 mPendingVolumeCommand = null;
             }
         }
@@ -10136,7 +10529,7 @@
     static final int LOG_NB_EVENTS_PHONE_STATE = 20;
     static final int LOG_NB_EVENTS_DEVICE_CONNECTION = 50;
     static final int LOG_NB_EVENTS_FORCE_USE = 20;
-    static final int LOG_NB_EVENTS_VOLUME = 40;
+    static final int LOG_NB_EVENTS_VOLUME = 100;
     static final int LOG_NB_EVENTS_DYN_POLICY = 10;
     static final int LOG_NB_EVENTS_SPATIAL = 30;
 
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 30a9e0a7..36908dc 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -17,7 +17,6 @@
 package com.android.server.audio;
 
 import android.annotation.NonNull;
-import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.AudioSystem;
@@ -147,20 +146,45 @@
         }
     }
 
+    static final class VolChangedBroadcastEvent extends AudioEventLogger.Event {
+        final int mStreamType;
+        final int mAliasStreamType;
+        final int mIndex;
+
+        VolChangedBroadcastEvent(int stream, int alias, int index) {
+            mStreamType = stream;
+            mAliasStreamType = alias;
+            mIndex = index;
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("sending VOLUME_CHANGED stream:")
+                    .append(AudioSystem.streamToString(mStreamType))
+                    .append(" index:").append(mIndex)
+                    .append(" alias:").append(AudioSystem.streamToString(mAliasStreamType))
+                    .toString();
+        }
+    }
+
     static final class DeviceVolumeEvent extends AudioEventLogger.Event {
         final int mStream;
         final int mVolIndex;
         final String mDeviceNativeType;
         final String mDeviceAddress;
         final String mCaller;
+        final int mDeviceForStream;
+        final boolean mSkipped;
 
         DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device,
-                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")
@@ -175,12 +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).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();
         }
     }
 
@@ -196,6 +226,7 @@
         static final int VOL_SET_GROUP_VOL = 8;
         static final int VOL_MUTE_STREAM_INT = 9;
         static final int VOL_SET_LE_AUDIO_VOL = 10;
+        static final int VOL_ADJUST_GROUP_VOL = 11;
 
         final int mOp;
         final int mStream;
@@ -203,7 +234,6 @@
         final int mVal2;
         final String mCaller;
         final String mGroupName;
-        final AudioAttributes mAudioAttributes;
 
         /** used for VOL_ADJUST_VOL_UID,
          *           VOL_ADJUST_SUGG_VOL,
@@ -216,7 +246,6 @@
             mVal2 = val2;
             mCaller = caller;
             mGroupName = null;
-            mAudioAttributes = null;
             logMetricEvent();
         }
 
@@ -229,7 +258,6 @@
             mStream = -1;
             mCaller = null;
             mGroupName = null;
-            mAudioAttributes = null;
             logMetricEvent();
         }
 
@@ -242,7 +270,6 @@
             mStream = -1;
             mCaller = null;
             mGroupName = null;
-            mAudioAttributes = null;
             logMetricEvent();
         }
 
@@ -255,7 +282,6 @@
             // unused
             mCaller = null;
             mGroupName = null;
-            mAudioAttributes = null;
             logMetricEvent();
         }
 
@@ -268,19 +294,18 @@
             // unused
             mCaller = null;
             mGroupName = null;
-            mAudioAttributes = null;
             logMetricEvent();
         }
 
-        /** used for VOL_SET_GROUP_VOL */
-        VolumeEvent(int op, AudioAttributes aa, String group, int index, int flags, String caller) {
+        /** used for VOL_SET_GROUP_VOL,
+         *           VOL_ADJUST_GROUP_VOL */
+        VolumeEvent(int op, String group, int index, int flags, String caller) {
             mOp = op;
             mStream = -1;
             mVal1 = index;
             mVal2 = flags;
             mCaller = caller;
             mGroupName = group;
-            mAudioAttributes = aa;
             logMetricEvent();
         }
 
@@ -292,7 +317,6 @@
             mVal2 = 0;
             mCaller = null;
             mGroupName = null;
-            mAudioAttributes = null;
             logMetricEvent();
         }
 
@@ -334,6 +358,15 @@
                             .record();
                     return;
                 }
+                case VOL_ADJUST_GROUP_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down")
+                            .set(MediaMetrics.Property.EVENT, "adjustVolumeGroupVolume")
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.GROUP, mGroupName)
+                            .record();
+                    return;
                 case VOL_SET_STREAM_VOL:
                     new MediaMetrics.Item(mMetricsId)
                             .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
@@ -385,7 +418,6 @@
                     return;
                 case VOL_SET_GROUP_VOL:
                     new MediaMetrics.Item(mMetricsId)
-                            .set(MediaMetrics.Property.ATTRIBUTES, mAudioAttributes.toString())
                             .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                             .set(MediaMetrics.Property.EVENT, "setVolumeIndexForAttributes")
                             .set(MediaMetrics.Property.FLAGS, mVal2)
@@ -411,6 +443,13 @@
                             .append(" flags:0x").append(Integer.toHexString(mVal2))
                             .append(") from ").append(mCaller)
                             .toString();
+                case VOL_ADJUST_GROUP_VOL:
+                    return new StringBuilder("adjustVolumeGroupVolume(group:")
+                            .append(mGroupName)
+                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
+                            .append(" flags:0x").append(Integer.toHexString(mVal2))
+                            .append(") from ").append(mCaller)
+                            .toString();
                 case VOL_ADJUST_STREAM_VOL:
                     return new StringBuilder("adjustStreamVolume(stream:")
                             .append(AudioSystem.streamToString(mStream))
@@ -459,8 +498,7 @@
                             .append(" stream:").append(AudioSystem.streamToString(mStream))
                             .toString();
                 case VOL_SET_GROUP_VOL:
-                    return new StringBuilder("setVolumeIndexForAttributes(attr:")
-                            .append(mAudioAttributes.toString())
+                    return new StringBuilder("setVolumeIndexForAttributes(group:")
                             .append(" group: ").append(mGroupName)
                             .append(" index:").append(mVal1)
                             .append(" flags:0x").append(Integer.toHexString(mVal2))
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index c3754eb..a17b4bf 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -29,8 +29,12 @@
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -60,8 +64,12 @@
     private String[] mMethodNames = {"getDevicesForAttributes"};
 
     private static final boolean USE_CACHE_FOR_GETDEVICES = true;
+    private static final Object sDeviceCacheLock = new Object();
+    @GuardedBy("sDeviceCacheLock")
     private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
             mDevicesForAttrCache;
+    @GuardedBy("sDeviceCacheLock")
+    private long mDevicesForAttributesCacheClearTimeMs = System.currentTimeMillis();
     private int[] mMethodCacheHit;
     private static final Object sRoutingListenerLock = new Object();
     @GuardedBy("sRoutingListenerLock")
@@ -105,6 +113,13 @@
         }
     }
 
+    public void clearRoutingCache() {
+        if (DEBUG_CACHE) {
+            Log.d(TAG, "---- routing cache clear (from java) ----------");
+        }
+        invalidateRoutingCache();
+    }
+
     /**
      * Implementation of AudioSystem.VolumeRangeInitRequestCallback
      */
@@ -140,9 +155,11 @@
             AudioSystem.setRoutingCallback(sSingletonDefaultAdapter);
             AudioSystem.setVolumeRangeInitRequestCallback(sSingletonDefaultAdapter);
             if (USE_CACHE_FOR_GETDEVICES) {
-                sSingletonDefaultAdapter.mDevicesForAttrCache =
-                        new ConcurrentHashMap<>(AudioSystem.getNumStreamTypes());
-                sSingletonDefaultAdapter.mMethodCacheHit = new int[NB_MEASUREMENTS];
+                synchronized (sDeviceCacheLock) {
+                    sSingletonDefaultAdapter.mDevicesForAttrCache =
+                            new ConcurrentHashMap<>(AudioSystem.getNumStreamTypes());
+                    sSingletonDefaultAdapter.mMethodCacheHit = new int[NB_MEASUREMENTS];
+                }
             }
             if (ENABLE_GETDEVICES_STATS) {
                 sSingletonDefaultAdapter.mMethodCallCounter = new int[NB_MEASUREMENTS];
@@ -156,8 +173,9 @@
         if (DEBUG_CACHE) {
             Log.d(TAG, "---- clearing cache ----------");
         }
-        if (mDevicesForAttrCache != null) {
-            synchronized (mDevicesForAttrCache) {
+        synchronized (sDeviceCacheLock) {
+            if (mDevicesForAttrCache != null) {
+                mDevicesForAttributesCacheClearTimeMs = System.currentTimeMillis();
                 mDevicesForAttrCache.clear();
             }
         }
@@ -186,7 +204,7 @@
         if (USE_CACHE_FOR_GETDEVICES) {
             ArrayList<AudioDeviceAttributes> res;
             final Pair<AudioAttributes, Boolean> key = new Pair(attributes, forVolume);
-            synchronized (mDevicesForAttrCache) {
+            synchronized (sDeviceCacheLock) {
                 res = mDevicesForAttrCache.get(key);
                 if (res == null) {
                     // result from AudioSystem guaranteed non-null, but could be invalid
@@ -337,6 +355,7 @@
      * @return
      */
     public int setParameters(String keyValuePairs) {
+        invalidateRoutingCache();
         return AudioSystem.setParameters(keyValuePairs);
     }
 
@@ -500,23 +519,31 @@
      */
     public void dump(PrintWriter pw) {
         pw.println("\nAudioSystemAdapter:");
-        pw.println(" mDevicesForAttrCache:");
-        if (mDevicesForAttrCache != null) {
-            for (Map.Entry<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
-                    entry : mDevicesForAttrCache.entrySet()) {
-                final AudioAttributes attributes = entry.getKey().first;
-                try {
-                    final int stream = attributes.getVolumeControlStream();
-                    pw.println("\t" + attributes + " forVolume: " + entry.getKey().second
-                            + " stream: "
-                            + AudioSystem.STREAM_NAMES[stream] + "(" + stream + ")");
-                    for (AudioDeviceAttributes devAttr : entry.getValue()) {
-                        pw.println("\t\t" + devAttr);
+        final DateTimeFormatter formatter = DateTimeFormatter
+                .ofPattern("MM-dd HH:mm:ss:SSS")
+                .withLocale(Locale.US)
+                .withZone(ZoneId.systemDefault());
+        synchronized (sDeviceCacheLock) {
+            pw.println(" last cache clear time: " + formatter.format(
+                    Instant.ofEpochMilli(mDevicesForAttributesCacheClearTimeMs)));
+            pw.println(" mDevicesForAttrCache:");
+            if (mDevicesForAttrCache != null) {
+                for (Map.Entry<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
+                        entry : mDevicesForAttrCache.entrySet()) {
+                    final AudioAttributes attributes = entry.getKey().first;
+                    try {
+                        final int stream = attributes.getVolumeControlStream();
+                        pw.println("\t" + attributes + " forVolume: " + entry.getKey().second
+                                + " stream: "
+                                + AudioSystem.STREAM_NAMES[stream] + "(" + stream + ")");
+                        for (AudioDeviceAttributes devAttr : entry.getValue()) {
+                            pw.println("\t\t" + devAttr);
+                        }
+                    } catch (IllegalArgumentException e) {
+                        // dump could fail if attributes do not map to a stream.
+                        pw.println("\t dump failed for attributes: " + attributes);
+                        Log.e(TAG, "dump failed", e);
                     }
-                } catch (IllegalArgumentException e) {
-                    // dump could fail if attributes do not map to a stream.
-                    pw.println("\t dump failed for attributes: " + attributes);
-                    Log.e(TAG, "dump failed", e);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 6cd42f8..691ce93 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -279,7 +279,11 @@
         }
         AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
                 AudioServiceEvents.VolumeEvent.VOL_SET_AVRCP_VOL, index));
-        mA2dp.setAvrcpAbsoluteVolume(index);
+        try {
+            mA2dp.setAvrcpAbsoluteVolume(index);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception while changing abs volume", e);
+        }
     }
 
     /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec int getA2dpCodec(
@@ -287,7 +291,12 @@
         if (mA2dp == null) {
             return AudioSystem.AUDIO_FORMAT_DEFAULT;
         }
-        final BluetoothCodecStatus btCodecStatus = mA2dp.getCodecStatus(device);
+        BluetoothCodecStatus btCodecStatus = null;
+        try {
+            btCodecStatus = mA2dp.getCodecStatus(device);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception while getting status of " + device, e);
+        }
         if (btCodecStatus == null) {
             return AudioSystem.AUDIO_FORMAT_DEFAULT;
         }
@@ -421,7 +430,11 @@
         }
         AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
                 AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex));
-        mLeAudio.setVolume(volume);
+        try {
+            mLeAudio.setVolume(volume);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception while setting LE volume", e);
+        }
     }
 
     /*package*/ synchronized void setHearingAidVolume(int index, int streamType,
@@ -447,7 +460,11 @@
             AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
                     AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
         }
-        mHearingAid.setVolume(gainDB);
+        try {
+            mHearingAid.setVolume(gainDB);
+        } catch (Exception e) {
+            Log.i(TAG, "Exception while setting hearing aid volume", e);
+        }
     }
 
     /*package*/ synchronized void onBroadcastScoConnectionState(int state) {
@@ -472,7 +489,7 @@
     }
 
     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void resetBluetoothSco() {
         mScoAudioState = SCO_STATE_INACTIVE;
         broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
@@ -487,6 +504,35 @@
         mBluetoothHeadset = null;
     }
 
+    //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void onBtProfileDisconnected(int profile) {
+        switch (profile) {
+            case BluetoothProfile.A2DP:
+                mA2dp = null;
+                break;
+            case BluetoothProfile.HEARING_AID:
+                mHearingAid = null;
+                break;
+            case BluetoothProfile.LE_AUDIO:
+                mLeAudio = null;
+                break;
+
+            case BluetoothProfile.A2DP_SINK:
+            case BluetoothProfile.LE_AUDIO_BROADCAST:
+                // shouldn't be received here as profile doesn't involve BtHelper
+                Log.e(TAG, "onBtProfileDisconnected: Not a profile handled by BtHelper "
+                        + BluetoothProfile.getProfileName(profile));
+                break;
+
+            default:
+                // Not a valid profile to disconnect
+                Log.e(TAG, "onBtProfileDisconnected: Not a valid profile to disconnect "
+                        + BluetoothProfile.getProfileName(profile));
+                break;
+        }
+    }
+
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
         if (profile == BluetoothProfile.HEADSET) {
             onHeadsetProfileConnected((BluetoothHeadset) proxy);
@@ -518,7 +564,7 @@
     }
 
     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void onHeadsetProfileConnected(BluetoothHeadset headset) {
         // Discard timeout message
         mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
@@ -672,7 +718,6 @@
                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
                     switch(profile) {
                         case BluetoothProfile.A2DP:
-                        case BluetoothProfile.A2DP_SINK:
                         case BluetoothProfile.HEADSET:
                         case BluetoothProfile.HEARING_AID:
                         case BluetoothProfile.LE_AUDIO:
@@ -682,6 +727,10 @@
                             mDeviceBroker.postBtProfileConnected(profile, proxy);
                             break;
 
+                        case BluetoothProfile.A2DP_SINK:
+                            // no A2DP sink functionality handled by BtHelper
+                        case BluetoothProfile.LE_AUDIO_BROADCAST:
+                            // no broadcast functionality handled by BtHelper
                         default:
                             break;
                     }
@@ -690,14 +739,19 @@
 
                     switch (profile) {
                         case BluetoothProfile.A2DP:
-                        case BluetoothProfile.A2DP_SINK:
                         case BluetoothProfile.HEADSET:
                         case BluetoothProfile.HEARING_AID:
                         case BluetoothProfile.LE_AUDIO:
-                        case BluetoothProfile.LE_AUDIO_BROADCAST:
+                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                                    "BT profile service: disconnecting "
+                                        + BluetoothProfile.getProfileName(profile) + " profile"));
                             mDeviceBroker.postBtProfileDisconnected(profile);
                             break;
 
+                        case BluetoothProfile.A2DP_SINK:
+                            // no A2DP sink functionality handled by BtHelper
+                        case BluetoothProfile.LE_AUDIO_BROADCAST:
+                            // no broadcast functionality handled by BtHelper
                         default:
                             break;
                     }
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 54be4bb..1862942 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -58,13 +58,15 @@
 public final class PlaybackActivityMonitor
         implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer {
 
-    public static final String TAG = "AudioService.PlaybackActivityMonitor";
+    public static final String TAG = "AS.PlayActivityMonitor";
 
     /*package*/ static final boolean DEBUG = false;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3;
+    /*package*/ static final int VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID = 4;
 
+    // ducking settings for a "normal duck" at -14dB
     private static final VolumeShaper.Configuration DUCK_VSHAPE =
             new VolumeShaper.Configuration.Builder()
                 .setId(VOLUME_SHAPER_SYSTEM_DUCK_ID)
@@ -78,6 +80,22 @@
                 .build();
     private static final VolumeShaper.Configuration DUCK_ID =
             new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_DUCK_ID);
+
+    // ducking settings for a "strong duck" at -35dB (attenuation factor of 0.017783)
+    private static final VolumeShaper.Configuration STRONG_DUCK_VSHAPE =
+            new VolumeShaper.Configuration.Builder()
+                .setId(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID)
+                .setCurve(new float[] { 0.f, 1.f } /* times */,
+                        new float[] { 1.f, 0.017783f } /* volumes */)
+                .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
+                .setDuration(MediaFocusControl.getFocusRampTimeMs(
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+                        new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION)
+                                .build()))
+                    .build();
+    private static final VolumeShaper.Configuration STRONG_DUCK_ID =
+            new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID);
+
     private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
             new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
                     .createIfNeeded()
@@ -659,11 +677,23 @@
             // add the players eligible for ducking to the list, and duck them
             // (if apcsToDuck is empty, this will at least mark this uid as ducked, so when
             //  players of the same uid start, they will be ducked by DuckingManager.checkDuck())
-            mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck);
+            mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck, reqCausesStrongDuck(winner));
         }
         return true;
     }
 
+    private boolean reqCausesStrongDuck(FocusRequester requester) {
+        if (requester.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) {
+            return false;
+        }
+        final int reqUsage = requester.getAudioAttributes().getUsage();
+        if ((reqUsage == AudioAttributes.USAGE_ASSISTANT)
+                || (reqUsage == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)) {
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public void restoreVShapedPlayers(@NonNull FocusRequester winner) {
         if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); }
@@ -939,10 +969,11 @@
     private static final class DuckingManager {
         private final HashMap<Integer, DuckedApp> mDuckers = new HashMap<Integer, DuckedApp>();
 
-        synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck) {
+        synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck,
+                boolean requestCausesStrongDuck) {
             if (DEBUG) {  Log.v(TAG, "DuckingManager: duckUid() uid:"+ uid); }
             if (!mDuckers.containsKey(uid)) {
-                mDuckers.put(uid, new DuckedApp(uid));
+                mDuckers.put(uid, new DuckedApp(uid, requestCausesStrongDuck));
             }
             final DuckedApp da = mDuckers.get(uid);
             for (AudioPlaybackConfiguration apc : apcsToDuck) {
@@ -989,10 +1020,13 @@
 
         private static final class DuckedApp {
             private final int mUid;
+            /** determines whether ducking is done with DUCK_VSHAPE or STRONG_DUCK_VSHAPE */
+            private final boolean mUseStrongDuck;
             private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>();
 
-            DuckedApp(int uid) {
+            DuckedApp(int uid, boolean useStrongDuck) {
                 mUid = uid;
+                mUseStrongDuck = useStrongDuck;
             }
 
             void dump(PrintWriter pw) {
@@ -1013,9 +1047,9 @@
                     return;
                 }
                 try {
-                    sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG));
+                    sEventLogger.log((new DuckEvent(apc, skipRamp, mUseStrongDuck)).printLog(TAG));
                     apc.getPlayerProxy().applyVolumeShaper(
-                            DUCK_VSHAPE,
+                            mUseStrongDuck ? STRONG_DUCK_VSHAPE : DUCK_VSHAPE,
                             skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
                     mDuckedPlayers.add(piid);
                 } catch (Exception e) {
@@ -1031,7 +1065,7 @@
                             sEventLogger.log((new AudioEventLogger.StringEvent("unducking piid:"
                                     + piid)).printLog(TAG));
                             apc.getPlayerProxy().applyVolumeShaper(
-                                    DUCK_ID,
+                                    mUseStrongDuck ? STRONG_DUCK_ID : DUCK_ID,
                                     VolumeShaper.Operation.REVERSE);
                         } catch (Exception e) {
                             Log.e(TAG, "Error unducking player piid:" + piid + " uid:" + mUid, e);
@@ -1146,13 +1180,17 @@
     }
 
     static final class DuckEvent extends VolumeShaperEvent {
+        final boolean mUseStrongDuck;
+
         @Override
         String getVSAction() {
-            return "ducking";
+            return mUseStrongDuck ? "ducking (strong)" : "ducking";
         }
 
-        DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+        DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, boolean useStrongDuck)
+        {
             super(apc, skipRamp);
+            mUseStrongDuck = useStrongDuck;
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
index 57ea812..1924f3c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
@@ -96,7 +96,11 @@
         @Override
         public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
             Slog.d(TAG, "Remove onClientFinished: " + clientMonitor + ", success: " + success);
-            mCallback.onClientFinished(InternalCleanupClient.this, success);
+            if (mUnknownHALTemplates.isEmpty()) {
+                mCallback.onClientFinished(InternalCleanupClient.this, success);
+            } else {
+                startCleanupUnknownHalTemplates();
+            }
         }
     };
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 05e83da..7d390415 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,15 +16,11 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.TaskStackListener;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
-import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
 import android.hardware.biometrics.common.ICancellationSignal;
@@ -92,7 +88,6 @@
     private long mSideFpsLastAcquireStartTime;
     private Runnable mAuthSuccessRunnable;
     private final Clock mClock;
-    private boolean mDidFinishSfps;
 
     FingerprintAuthenticationClient(
             @NonNull Context context,
@@ -198,9 +193,8 @@
 
     @Override
     protected void handleLifecycleAfterAuth(boolean authenticated) {
-        if (authenticated && !mDidFinishSfps) {
+        if (authenticated) {
             mCallback.onClientFinished(this, true /* success */);
-            mDidFinishSfps = true;
         }
     }
 
@@ -210,13 +204,11 @@
         return false;
     }
 
-    public void handleAuthenticate(
+    @Override
+    public void onAuthenticated(
             BiometricAuthenticator.Identifier identifier,
             boolean authenticated,
             ArrayList<Byte> token) {
-        if (authenticated && mSensorProps.isAnySidefpsType()) {
-            Slog.i(TAG, "(sideFPS): No power press detected, sending auth");
-        }
         super.onAuthenticated(identifier, authenticated, token);
         if (authenticated) {
             mState = STATE_STOPPED;
@@ -227,77 +219,25 @@
     }
 
     @Override
-    public void onAuthenticated(
-            BiometricAuthenticator.Identifier identifier,
-            boolean authenticated,
-            ArrayList<Byte> token) {
-
-        mHandler.post(
-                () -> {
-                    long delay = 0;
-                    if (authenticated && mSensorProps.isAnySidefpsType()) {
-                        delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
-
-                        if (mSideFpsLastAcquireStartTime != -1) {
-                            delay = Math.max(0,
-                                    delay - (mClock.millis() - mSideFpsLastAcquireStartTime));
-                        }
-
-                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps "
-                                + "waiting for power until: " + delay + "ms");
-                    }
-
-                    if (mHandler.hasMessages(MESSAGE_FINGER_UP)) {
-                        Slog.i(TAG, "Finger up detected, sending auth");
-                        delay = 0;
-                    }
-
-                    mAuthSuccessRunnable =
-                            () -> handleAuthenticate(identifier, authenticated, token);
-                    mHandler.postDelayed(
-                            mAuthSuccessRunnable,
-                            MESSAGE_AUTH_SUCCESS,
-                            delay);
-                });
-    }
-
-    @Override
     public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) {
         // For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off
         // for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired
         mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
         super.onAcquired(acquiredInfo, vendorCode);
-        if (mSensorProps.isAnySidefpsType()) {
-            if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
-                mSideFpsLastAcquireStartTime = mClock.millis();
-            }
-            final boolean shouldLookForVendor =
-                    mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR;
-            final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage;
-            final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage;
-            final boolean ignorePowerPress =
-                    acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch);
-
-            if (ignorePowerPress) {
-                Slog.d(TAG, "(sideFPS) onFingerUp");
-                mHandler.post(() -> {
-                    if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) {
-                        Slog.d(TAG, "(sideFPS) skipping wait for power");
-                        mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
-                        mHandler.post(mAuthSuccessRunnable);
-                    } else {
-                        mHandler.postDelayed(() -> {
-                        }, MESSAGE_FINGER_UP, mFingerUpIgnoresPower);
-                    }
-                });
-            }
-        }
-
     }
 
     @Override
     public void onError(int errorCode, int vendorCode) {
-        super.onError(errorCode, vendorCode);
+        if (getContext().getResources().getBoolean(R.bool.config_powerPressMapping)
+                && errorCode == BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR
+                && vendorCode == getContext().getResources()
+                .getInteger(R.integer.config_powerPressCode)) {
+            // Translating vendor code to internal code
+            super.onError(BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED,
+                    0 /* vendorCode */);
+        } else {
+            super.onError(errorCode, vendorCode);
+        }
 
         if (errorCode == BiometricFingerprintConstants.FINGERPRINT_ERROR_BAD_CALIBRATION) {
             BiometricNotificationUtils.showBadCalibrationNotification(getContext());
@@ -488,22 +428,5 @@
     }
 
     @Override
-    public void onPowerPressed() {
-        if (mSensorProps.isAnySidefpsType()) {
-            Slog.i(TAG, "(sideFPS): onPowerPressed");
-            mHandler.post(() -> {
-                if (mDidFinishSfps) {
-                    return;
-                }
-                Slog.i(TAG, "(sideFPS): finishing auth");
-                // Ignore auths after a power has been detected
-                mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
-                // Do not call onError() as that will send an additional callback to coex.
-                mDidFinishSfps = true;
-                onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
-                stopHalOperation();
-                mSensorOverlays.hide(getSensorId());
-            });
-        }
-    }
+    public void onPowerPressed() { }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 612d906..14b19fb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -37,6 +37,7 @@
 import android.util.Slog;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.internal.R;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -143,7 +144,17 @@
             }
         });
         mCallback.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
-        super.onAcquired(acquiredInfo, vendorCode);
+
+        if (getContext().getResources().getBoolean(R.bool.config_powerPressMapping)
+                && acquiredInfo == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR
+                && vendorCode == getContext().getResources()
+                .getInteger(R.integer.config_powerPressCode)) {
+            // Translating vendor code to internal code
+            super.onAcquired(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED,
+                    0 /* vendorCode */);
+        } else {
+            super.onAcquired(acquiredInfo, vendorCode);
+        }
     }
 
     @Override
@@ -270,8 +281,5 @@
     }
 
     @Override
-    public void onPowerPressed() {
-        onAcquired(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED,
-                0 /* vendorCode */);
-    }
+    public void onPowerPressed() {}
 }
diff --git a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
index 3a4aaa7..1f82961 100644
--- a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
+++ b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
@@ -31,7 +31,9 @@
 import android.net.metrics.NetworkMetrics;
 import android.net.metrics.WakeupEvent;
 import android.net.metrics.WakeupStats;
+import android.os.BatteryStatsInternal;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -44,6 +46,7 @@
 import com.android.internal.util.RingBuffer;
 import com.android.internal.util.TokenBucket;
 import com.android.net.module.util.BaseNetdEventListener;
+import com.android.server.LocalServices;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
 
 import java.io.PrintWriter;
@@ -74,7 +77,7 @@
     // TODO: dedup this String constant with the one used in
     // ConnectivityService#wakeupModifyInterface().
     @VisibleForTesting
-    static final String WAKEUP_EVENT_IFACE_PREFIX = "iface:";
+    static final String WAKEUP_EVENT_PREFIX_DELIM = ":";
 
     // Array of aggregated DNS and connect events sent by netd, grouped by net id.
     @GuardedBy("this")
@@ -278,17 +281,14 @@
     @Override
     public synchronized void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader,
             byte[] dstHw, String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs) {
-        String iface = prefix.replaceFirst(WAKEUP_EVENT_IFACE_PREFIX, "");
-        final long timestampMs;
-        if (timestampNs > 0) {
-            timestampMs = timestampNs / NANOS_PER_MS;
-        } else {
-            timestampMs = System.currentTimeMillis();
+        final String[] prefixParts = prefix.split(WAKEUP_EVENT_PREFIX_DELIM);
+        if (prefixParts.length != 2) {
+            throw new IllegalArgumentException("Prefix " + prefix
+                    + " required in format <nethandle>:<interface>");
         }
 
-        WakeupEvent event = new WakeupEvent();
-        event.iface = iface;
-        event.timestampMs = timestampMs;
+        final WakeupEvent event = new WakeupEvent();
+        event.iface = prefixParts[1];
         event.uid = uid;
         event.ethertype = ethertype;
         event.dstHwAddr = MacAddress.fromBytes(dstHw);
@@ -297,11 +297,25 @@
         event.ipNextHeader = ipNextHeader;
         event.srcPort = srcPort;
         event.dstPort = dstPort;
+        if (timestampNs > 0) {
+            event.timestampMs = timestampNs / NANOS_PER_MS;
+        } else {
+            event.timestampMs = System.currentTimeMillis();
+        }
         addWakeupEvent(event);
 
-        String dstMac = event.dstHwAddr.toString();
+        final BatteryStatsInternal bsi = LocalServices.getService(BatteryStatsInternal.class);
+        if (bsi != null) {
+            final long netHandle = Long.parseLong(prefixParts[0]);
+            final long elapsedMs = SystemClock.elapsedRealtime() + event.timestampMs
+                    - System.currentTimeMillis();
+            bsi.noteCpuWakingNetworkPacket(Network.fromNetworkHandle(netHandle), elapsedMs,
+                    event.uid);
+        }
+
+        final String dstMac = event.dstHwAddr.toString();
         FrameworkStatsLog.write(FrameworkStatsLog.PACKET_WAKEUP_OCCURRED,
-                uid, iface, ethertype, dstMac, srcIp, dstIp, ipNextHeader, srcPort, dstPort);
+                uid, event.iface, ethertype, dstMac, srcIp, dstIp, ipNextHeader, srcPort, dstPort);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java b/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java
index 5c4e2f3..c876a8b 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.text.TextUtils;
+import android.util.Slog;
 
 import com.android.server.policy.DeviceStatePolicyImpl;
 
@@ -92,11 +93,16 @@
 
             try {
                 return (DeviceStatePolicy.Provider) Class.forName(name).newInstance();
-            } catch (ReflectiveOperationException | ClassCastException e) {
+            } catch (ClassCastException e) {
                 throw new IllegalStateException("Couldn't instantiate class " + name
                         + " for config_deviceSpecificDeviceStatePolicyProvider:"
                         + " make sure it has a public zero-argument constructor"
-                        + " and implements DeviceStatePolicy.Provider", e);
+                        + " and implements DeviceStatePolicy.Provider");
+            } catch (ReflectiveOperationException e) {
+                Slog.e("DeviceStatePolicy", "Couldn't instantiate class " + name
+                        + " for config_deviceSpecificDeviceStatePolicyProvider:"
+                        + " using default provider", e);
+                return new DeviceStatePolicy.DefaultProvider();
             }
         }
     }
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 9dd2f84..b9ca57e 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -76,6 +76,10 @@
         return layout;
     }
 
+    int size() {
+        return mLayoutMap.size();
+    }
+
     private Layout createLayout(int state) {
         if (mLayoutMap.contains(state)) {
             Slog.e(TAG, "Attempted to create a second layout for state " + state);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index fa812c1..c36c5b6 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -135,6 +135,8 @@
  *      </thermalThrottling>
  *
  *      <refreshRate>
+ *        <defaultRefreshRateInHbmHdr>75</defaultRefreshRateInHbmHdr>
+ *        <defaultRefreshRateInHbmSunlight>75</defaultRefreshRateInHbmSunlight>
  *        <lowerBlockingZoneConfigs>
  *          <defaultRefreshRate>75</defaultRefreshRate>
  *          <blockingZoneThreshold>
@@ -402,6 +404,9 @@
     private static final String STABLE_ID_SUFFIX_FORMAT = "id_%d";
     private static final String NO_SUFFIX_FORMAT = "%d";
     private static final long STABLE_FLAG = 1L << 62;
+    private static final int DEFAULT_PEAK_REFRESH_RATE = 0;
+    private static final int DEFAULT_REFRESH_RATE = 60;
+    private static final int DEFAULT_REFRESH_RATE_IN_HBM = 0;
     private static final int DEFAULT_LOW_REFRESH_RATE = 60;
     private static final int DEFAULT_HIGH_REFRESH_RATE = 0;
     private static final int[] DEFAULT_BRIGHTNESS_THRESHOLDS = new int[]{};
@@ -570,17 +575,38 @@
      * using higher refresh rates, even if display modes with higher refresh rates are available
      * from hardware composer. Only has an effect if the value is non-zero.
      */
-    private int mDefaultHighRefreshRate = DEFAULT_HIGH_REFRESH_RATE;
+    private int mDefaultPeakRefreshRate = DEFAULT_PEAK_REFRESH_RATE;
 
     /**
      * The default refresh rate for a given device. This value sets the higher default
      * refresh rate. If the hardware composer on the device supports display modes with
      * a higher refresh rate than the default value specified here, the framework may use those
      * higher refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling
-     * setFrameRate(). We have historically allowed fallback to mDefaultHighRefreshRate if
-     * mDefaultLowRefreshRate is set to 0, but this is not supported anymore.
+     * setFrameRate(). We have historically allowed fallback to mDefaultPeakRefreshRate if
+     * mDefaultRefreshRate is set to 0, but this is not supported anymore.
      */
-    private int mDefaultLowRefreshRate = DEFAULT_LOW_REFRESH_RATE;
+    private int mDefaultRefreshRate = DEFAULT_REFRESH_RATE;
+
+    /**
+     * Default refresh rate while the device has high brightness mode enabled for HDR.
+     */
+    private int mDefaultRefreshRateInHbmHdr = DEFAULT_REFRESH_RATE_IN_HBM;
+
+    /**
+     * Default refresh rate while the device has high brightness mode enabled for Sunlight.
+     */
+    private int mDefaultRefreshRateInHbmSunlight = DEFAULT_REFRESH_RATE_IN_HBM;
+    /**
+     * Default refresh rate in the high zone defined by brightness and ambient thresholds.
+     * If non-positive, then the refresh rate is unchanged even if thresholds are configured.
+     */
+    private int mDefaultHighBlockingZoneRefreshRate = DEFAULT_HIGH_REFRESH_RATE;
+
+    /**
+     * Default refresh rate in the zone defined by brightness and ambient thresholds.
+     * If non-positive, then the refresh rate is unchanged even if thresholds are configured.
+     */
+    private int mDefaultLowBlockingZoneRefreshRate = DEFAULT_LOW_REFRESH_RATE;
 
     /**
      * The display uses different gamma curves for different refresh rates. It's hard for panel
@@ -1295,15 +1321,44 @@
     /**
      * @return Default peak refresh rate of the associated display
      */
-    public int getDefaultHighRefreshRate() {
-        return mDefaultHighRefreshRate;
+    public int getDefaultPeakRefreshRate() {
+        return mDefaultPeakRefreshRate;
     }
 
     /**
      * @return Default refresh rate of the associated display
      */
-    public int getDefaultLowRefreshRate() {
-        return mDefaultLowRefreshRate;
+    public int getDefaultRefreshRate() {
+        return mDefaultRefreshRate;
+    }
+
+    /**
+     * @return Default refresh rate while the device has high brightness mode enabled for HDR.
+     */
+    public int getDefaultRefreshRateInHbmHdr() {
+        return mDefaultRefreshRateInHbmHdr;
+    }
+
+    /**
+     * @return Default refresh rate while the device has high brightness mode enabled because of
+     * high lux.
+     */
+    public int getDefaultRefreshRateInHbmSunlight() {
+        return mDefaultRefreshRateInHbmSunlight;
+    }
+
+    /**
+     * @return Default refresh rate in the higher blocking zone of the associated display
+     */
+    public int getDefaultHighBlockingZoneRefreshRate() {
+        return mDefaultHighBlockingZoneRefreshRate;
+    }
+
+    /**
+     * @return Default refresh rate in the lower blocking zone of the associated display
+     */
+    public int getDefaultLowBlockingZoneRefreshRate() {
+        return mDefaultLowBlockingZoneRefreshRate;
     }
 
     /**
@@ -1441,8 +1496,12 @@
                 + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable
                 + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable
                 + "\n"
-                + ", mDefaultRefreshRate= " + mDefaultLowRefreshRate
-                + ", mDefaultPeakRefreshRate= " + mDefaultHighRefreshRate
+                + ", mDefaultLowBlockingZoneRefreshRate= " + mDefaultLowBlockingZoneRefreshRate
+                + ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate
+                + ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate
+                + ", mDefaultRefreshRate= " + mDefaultRefreshRate
+                + ", mDefaultRefreshRateInHbmHdr= " + mDefaultRefreshRateInHbmHdr
+                + ", mDefaultRefreshRateInHbmSunlight= " + mDefaultRefreshRateInHbmSunlight
                 + ", mLowDisplayBrightnessThresholds= "
                 + Arrays.toString(mLowDisplayBrightnessThresholds)
                 + ", mLowAmbientBrightnessThresholds= "
@@ -1756,10 +1815,52 @@
         BlockingZoneConfig higherBlockingZoneConfig =
                 (refreshRateConfigs == null) ? null
                         : refreshRateConfigs.getHigherBlockingZoneConfigs();
+        loadPeakDefaultRefreshRate(refreshRateConfigs);
+        loadDefaultRefreshRate(refreshRateConfigs);
+        loadDefaultRefreshRateInHbm(refreshRateConfigs);
         loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig);
         loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig);
     }
 
+    private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
+        if (refreshRateConfigs == null || refreshRateConfigs.getDefaultPeakRefreshRate() == null) {
+            mDefaultPeakRefreshRate = mContext.getResources().getInteger(
+                R.integer.config_defaultPeakRefreshRate);
+        } else {
+            mDefaultPeakRefreshRate =
+                refreshRateConfigs.getDefaultPeakRefreshRate().intValue();
+        }
+    }
+
+    private void loadDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
+        if (refreshRateConfigs == null || refreshRateConfigs.getDefaultRefreshRate() == null) {
+            mDefaultRefreshRate = mContext.getResources().getInteger(
+                R.integer.config_defaultRefreshRate);
+        } else {
+            mDefaultRefreshRate =
+                refreshRateConfigs.getDefaultRefreshRate().intValue();
+        }
+    }
+
+    private void loadDefaultRefreshRateInHbm(RefreshRateConfigs refreshRateConfigs) {
+        if (refreshRateConfigs != null
+                && refreshRateConfigs.getDefaultRefreshRateInHbmHdr() != null) {
+            mDefaultRefreshRateInHbmHdr = refreshRateConfigs.getDefaultRefreshRateInHbmHdr()
+                    .intValue();
+        } else {
+            mDefaultRefreshRateInHbmHdr = mContext.getResources().getInteger(
+                    R.integer.config_defaultRefreshRateInHbmHdr);
+        }
+
+        if (refreshRateConfigs != null
+                && refreshRateConfigs.getDefaultRefreshRateInHbmSunlight() != null) {
+            mDefaultRefreshRateInHbmSunlight =
+                    refreshRateConfigs.getDefaultRefreshRateInHbmSunlight().intValue();
+        } else {
+            mDefaultRefreshRateInHbmSunlight = mContext.getResources().getInteger(
+                R.integer.config_defaultRefreshRateInHbmSunlight);
+        }
+    }
 
     /**
      * Loads the refresh rate configurations pertaining to the upper blocking zones.
@@ -1784,10 +1885,10 @@
     private void loadHigherBlockingZoneDefaultRefreshRate(
                 BlockingZoneConfig upperBlockingZoneConfig) {
         if (upperBlockingZoneConfig == null) {
-            mDefaultHighRefreshRate = mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_defaultPeakRefreshRate);
+            mDefaultHighBlockingZoneRefreshRate = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_fixedRefreshRateInHighZone);
         } else {
-            mDefaultHighRefreshRate =
+            mDefaultHighBlockingZoneRefreshRate =
                 upperBlockingZoneConfig.getDefaultRefreshRate().intValue();
         }
     }
@@ -1799,10 +1900,10 @@
     private void loadLowerBlockingZoneDefaultRefreshRate(
                 BlockingZoneConfig lowerBlockingZoneConfig) {
         if (lowerBlockingZoneConfig == null) {
-            mDefaultLowRefreshRate = mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_defaultRefreshRate);
+            mDefaultLowBlockingZoneRefreshRate = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_defaultRefreshRateInZone);
         } else {
-            mDefaultLowRefreshRate =
+            mDefaultLowBlockingZoneRefreshRate =
                 lowerBlockingZoneConfig.getDefaultRefreshRate().intValue();
         }
     }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 1864328..73f8572 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -104,6 +104,7 @@
 import android.provider.Settings;
 import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.IntArray;
@@ -256,6 +257,13 @@
     final SparseArray<Pair<IVirtualDevice, DisplayWindowPolicyController>>
             mDisplayWindowPolicyControllers = new SparseArray<>();
 
+    /**
+     *  Map of every display device {@link HighBrightnessModeMetadata}s indexed by
+     *  {@link DisplayDevice#mUniqueId}.
+     */
+    public final ArrayMap<String, HighBrightnessModeMetadata> mHighBrightnessModeMetadataMap =
+            new ArrayMap<>();
+
     // List of all currently registered display adapters.
     private final ArrayList<DisplayAdapter> mDisplayAdapters = new ArrayList<DisplayAdapter>();
 
@@ -426,6 +434,8 @@
     private boolean mIsDocked;
     private boolean mIsDreaming;
 
+    private boolean mBootCompleted = false;
+
     private final BroadcastReceiver mIdleModeReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -565,6 +575,12 @@
                 }
             }
         } else if (phase == PHASE_BOOT_COMPLETED) {
+            synchronized (mSyncRoot) {
+                mBootCompleted = true;
+                for (int i = 0; i < mDisplayPowerControllers.size(); i++) {
+                    mDisplayPowerControllers.valueAt(i).onBootCompleted();
+                }
+            }
             mDisplayModeDirector.onBootCompleted();
             mLogicalDisplayMapper.onBootCompleted();
         }
@@ -1535,6 +1551,7 @@
         final int displayId = display.getDisplayIdLocked();
         final boolean isDefault = displayId == Display.DEFAULT_DISPLAY;
         configureColorModeLocked(display, device);
+
         if (!mAreUserDisabledHdrTypesAllowed) {
             display.setUserDisabledHdrTypes(mUserDisabledHdrTypes);
         }
@@ -1588,7 +1605,16 @@
 
         DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
         if (dpc != null) {
-            dpc.onDisplayChanged();
+            final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+            if (device == null) {
+                Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: "
+                        + display.getDisplayIdLocked());
+                return;
+            }
+
+            final String uniqueId = device.getUniqueId();
+            HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId);
+            dpc.onDisplayChanged(hbmMetadata);
         }
     }
 
@@ -1645,7 +1671,15 @@
         final int displayId = display.getDisplayIdLocked();
         final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
         if (dpc != null) {
-            dpc.onDisplayChanged();
+            final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+            if (device == null) {
+                Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: "
+                        + display.getDisplayIdLocked());
+                return;
+            }
+            final String uniqueId = device.getUniqueId();
+            HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId);
+            dpc.onDisplayChanged(hbmMetadata);
         }
     }
 
@@ -2631,6 +2665,27 @@
         mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked);
     }
 
+    @VisibleForTesting
+    HighBrightnessModeMetadata getHighBrightnessModeMetadata(LogicalDisplay display) {
+        final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+        if (device == null) {
+            Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: "
+                    + display.getDisplayIdLocked());
+            return null;
+        }
+
+        final String uniqueId = device.getUniqueId();
+
+        if (mHighBrightnessModeMetadataMap.containsKey(uniqueId)) {
+            return mHighBrightnessModeMetadataMap.get(uniqueId);
+        }
+
+        // HBM Time info not present. Create a new one for this physical display.
+        HighBrightnessModeMetadata hbmInfo = new HighBrightnessModeMetadata();
+        mHighBrightnessModeMetadataMap.put(uniqueId, hbmInfo);
+        return hbmInfo;
+    }
+
     private void addDisplayPowerControllerLocked(LogicalDisplay display) {
         if (mPowerHandler == null) {
             // initPowerManagement has not yet been called.
@@ -2642,10 +2697,18 @@
 
         final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore,
                 display, mSyncRoot);
+
+        // If display already has a HighBrightnessModeMetadata mapping, use that.
+        // Or create a new one and use that.
+        // We also need to pass a mapping of the HighBrightnessModeTimeInfoMap to
+        // displayPowerController, so the hbm info can be correctly associated
+        // with the corresponding displaydevice.
+        HighBrightnessModeMetadata hbmMetadata = getHighBrightnessModeMetadata(display);
+
         final DisplayPowerController displayPowerController = new DisplayPowerController(
                 mContext, mDisplayPowerCallbacks, mPowerHandler, mSensorManager,
                 mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
-                () -> handleBrightnessChange(display));
+                () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted);
         mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController);
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index aafba5a..6c7edb6 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);
+        }
     }
 
     /**
@@ -561,7 +570,7 @@
 
     /**
      * Sets the display mode switching type.
-     * @param newType
+     * @param newType new mode switching type
      */
     public void setModeSwitchingType(@DisplayManager.SwitchingType int newType) {
         synchronized (mLock) {
@@ -678,6 +687,18 @@
         notifyDesiredDisplayModeSpecsChangedLocked();
     }
 
+    @GuardedBy("mLock")
+    private float getMaxRefreshRateLocked(int displayId) {
+        Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
+        float maxRefreshRate = 0f;
+        for (Display.Mode mode : modes) {
+            if (mode.getRefreshRate() > maxRefreshRate) {
+                maxRefreshRate = mode.getRefreshRate();
+            }
+        }
+        return maxRefreshRate;
+    }
+
     private void notifyDesiredDisplayModeSpecsChangedLocked() {
         if (mDesiredDisplayModeSpecsListener != null
                 && !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
@@ -996,25 +1017,29 @@
         // of low priority voters. It votes [0, max(PEAK, MIN)]
         public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 7;
 
+        // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
+        // rate to max value (same as for PRIORITY_UDFPS) on lock screen
+        public static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
+
         // LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on.
-        public static final int PRIORITY_LOW_POWER_MODE = 8;
+        public static final int PRIORITY_LOW_POWER_MODE = 9;
 
         // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
         // higher priority voters' result is a range, it will fix the rate to a single choice.
         // It's used to avoid refresh rate switches in certain conditions which may result in the
         // user seeing the display flickering when the switches occur.
-        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 9;
+        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 10;
 
         // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
-        public static final int PRIORITY_SKIN_TEMPERATURE = 10;
+        public static final int PRIORITY_SKIN_TEMPERATURE = 11;
 
         // The proximity sensor needs the refresh rate to be locked in order to function, so this is
         // set to a high priority.
-        public static final int PRIORITY_PROXIMITY = 11;
+        public static final int PRIORITY_PROXIMITY = 12;
 
         // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
         // to function, so this needs to be the highest priority of all votes.
-        public static final int PRIORITY_UDFPS = 12;
+        public static final int PRIORITY_UDFPS = 13;
 
         // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
         // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -1117,6 +1142,8 @@
                     return "PRIORITY_USER_SETTING_MIN_REFRESH_RATE";
                 case PRIORITY_USER_SETTING_PEAK_REFRESH_RATE:
                     return "PRIORITY_USER_SETTING_PEAK_REFRESH_RATE";
+                case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
+                    return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE";
                 default:
                     return Integer.toString(priority);
             }
@@ -1169,7 +1196,7 @@
             mDefaultRefreshRate =
                     (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
                         R.integer.config_defaultRefreshRate)
-                        : (float) displayDeviceConfig.getDefaultLowRefreshRate();
+                        : (float) displayDeviceConfig.getDefaultRefreshRate();
         }
 
         public void observe() {
@@ -1256,7 +1283,7 @@
                 defaultPeakRefreshRate =
                         (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
                                 R.integer.config_defaultPeakRefreshRate)
-                                : (float) displayDeviceConfig.getDefaultHighRefreshRate();
+                                : (float) displayDeviceConfig.getDefaultPeakRefreshRate();
             }
             mDefaultPeakRefreshRate = defaultPeakRefreshRate;
         }
@@ -1612,8 +1639,26 @@
             return mHighAmbientBrightnessThresholds;
         }
 
+        /**
+         * @return the refresh rate to lock to when in a high brightness zone
+         */
+        @VisibleForTesting
+        int getRefreshRateInHighZone() {
+            return mRefreshRateInHighZone;
+        }
+
+        /**
+         * @return the refresh rate to lock to when in a low brightness zone
+         */
+        @VisibleForTesting
+        int getRefreshRateInLowZone() {
+            return mRefreshRateInLowZone;
+        }
+
         private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
                 boolean attemptLoadingFromDeviceConfig) {
+            loadRefreshRateInHighZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
+            loadRefreshRateInLowZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
             mLowDisplayBrightnessThresholds = loadBrightnessThresholds(
                     () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(),
                     () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(),
@@ -1634,6 +1679,44 @@
             }
         }
 
+        private void loadRefreshRateInLowZone(DisplayDeviceConfig displayDeviceConfig,
+                boolean attemptLoadingFromDeviceConfig) {
+            int refreshRateInLowZone =
+                    (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+                        R.integer.config_defaultRefreshRateInZone)
+                        : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate();
+            if (attemptLoadingFromDeviceConfig) {
+                try {
+                    refreshRateInLowZone = mDeviceConfig.getInt(
+                        DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                        DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
+                        refreshRateInLowZone);
+                } catch (Exception exception) {
+                    // Do nothing
+                }
+            }
+            mRefreshRateInLowZone = refreshRateInLowZone;
+        }
+
+        private void loadRefreshRateInHighZone(DisplayDeviceConfig displayDeviceConfig,
+                boolean attemptLoadingFromDeviceConfig) {
+            int refreshRateInHighZone =
+                    (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+                        R.integer.config_fixedRefreshRateInHighZone) : displayDeviceConfig
+                        .getDefaultHighBlockingZoneRefreshRate();
+            if (attemptLoadingFromDeviceConfig) {
+                try {
+                    refreshRateInHighZone = mDeviceConfig.getInt(
+                        DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                        DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
+                        refreshRateInHighZone);
+                } catch (Exception exception) {
+                    // Do nothing
+                }
+            }
+            mRefreshRateInHighZone = refreshRateInHighZone;
+        }
+
         private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
                 boolean attemptLoadingFromDeviceConfig) {
             mHighDisplayBrightnessThresholds = loadBrightnessThresholds(
@@ -1687,14 +1770,6 @@
         }
 
         /**
-         * @return the refresh to lock to when in a low brightness zone
-         */
-        @VisibleForTesting
-        int getRefreshRateInLowZone() {
-            return mRefreshRateInLowZone;
-        }
-
-        /**
          * @return the display brightness thresholds for the low brightness zones
          */
         @VisibleForTesting
@@ -1739,8 +1814,17 @@
                 mHighAmbientBrightnessThresholds = highAmbientBrightnessThresholds;
             }
 
-            mRefreshRateInLowZone = mDeviceConfigDisplaySettings.getRefreshRateInLowZone();
-            mRefreshRateInHighZone = mDeviceConfigDisplaySettings.getRefreshRateInHighZone();
+            final int refreshRateInLowZone = mDeviceConfigDisplaySettings
+                    .getRefreshRateInLowZone();
+            if (refreshRateInLowZone != -1) {
+                mRefreshRateInLowZone = refreshRateInLowZone;
+            }
+
+            final int refreshRateInHighZone = mDeviceConfigDisplaySettings
+                    .getRefreshRateInHighZone();
+            if (refreshRateInHighZone != -1) {
+                mRefreshRateInHighZone = refreshRateInHighZone;
+            }
 
             restartObserver();
             mDeviceConfigDisplaySettings.startListening();
@@ -1794,6 +1878,10 @@
             restartObserver();
         }
 
+        /**
+         * Used to reload the lower blocking zone refresh rate in case of changes in the
+         * DeviceConfig properties.
+         */
         public void onDeviceConfigRefreshRateInLowZoneChanged(int refreshRate) {
             if (refreshRate != mRefreshRateInLowZone) {
                 mRefreshRateInLowZone = refreshRate;
@@ -1817,6 +1905,10 @@
             restartObserver();
         }
 
+        /**
+         * Used to reload the higher blocking zone refresh rate in case of changes in the
+         * DeviceConfig properties.
+         */
         public void onDeviceConfigRefreshRateInHighZoneChanged(int refreshRate) {
             if (refreshRate != mRefreshRateInHighZone) {
                 mRefreshRateInHighZone = refreshRate;
@@ -2264,6 +2356,7 @@
 
     private class UdfpsObserver extends IUdfpsHbmListener.Stub {
         private final SparseBooleanArray mLocalHbmEnabled = new SparseBooleanArray();
+        private final SparseBooleanArray mAuthenticationPossible = new SparseBooleanArray();
 
         public void observe() {
             StatusBarManagerInternal statusBar =
@@ -2289,25 +2382,28 @@
 
         private void updateHbmStateLocked(int displayId, boolean enabled) {
             mLocalHbmEnabled.put(displayId, enabled);
-            updateVoteLocked(displayId);
+            updateVoteLocked(displayId, enabled, Vote.PRIORITY_UDFPS);
         }
 
-        private void updateVoteLocked(int displayId) {
+        @Override
+        public void onAuthenticationPossible(int displayId, boolean isPossible) {
+            synchronized (mLock) {
+                mAuthenticationPossible.put(displayId, isPossible);
+                updateVoteLocked(displayId, isPossible,
+                        Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+            }
+        }
+
+        @GuardedBy("mLock")
+        private void updateVoteLocked(int displayId, boolean enabled, int votePriority) {
             final Vote vote;
-            if (mLocalHbmEnabled.get(displayId)) {
-                Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
-                float maxRefreshRate = 0f;
-                for (Display.Mode mode : modes) {
-                    if (mode.getRefreshRate() > maxRefreshRate) {
-                        maxRefreshRate = mode.getRefreshRate();
-                    }
-                }
+            if (enabled) {
+                float maxRefreshRate = DisplayModeDirector.this.getMaxRefreshRateLocked(displayId);
                 vote = Vote.forRefreshRates(maxRefreshRate, maxRefreshRate);
             } else {
                 vote = null;
             }
-
-            DisplayModeDirector.this.updateVoteLocked(displayId, Vote.PRIORITY_UDFPS, vote);
+            DisplayModeDirector.this.updateVoteLocked(displayId, votePriority, vote);
         }
 
         void dumpLocked(PrintWriter pw) {
@@ -2318,6 +2414,13 @@
                 final String enabled = mLocalHbmEnabled.valueAt(i) ? "enabled" : "disabled";
                 pw.println("      Display " + displayId + ": " + enabled);
             }
+            pw.println("    mAuthenticationPossible: ");
+            for (int i = 0; i < mAuthenticationPossible.size(); i++) {
+                final int displayId = mAuthenticationPossible.keyAt(i);
+                final String isPossible = mAuthenticationPossible.valueAt(i) ? "possible"
+                        : "impossible";
+                pw.println("      Display " + displayId + ": " + isPossible);
+            }
         }
     }
 
@@ -2440,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();
@@ -2460,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
@@ -2664,15 +2781,10 @@
         }
 
         public int getRefreshRateInLowZone() {
-            int defaultRefreshRateInZone = mContext.getResources().getInteger(
-                    R.integer.config_defaultRefreshRateInZone);
-
-            int refreshRate = mDeviceConfig.getInt(
+            return mDeviceConfig.getInt(
                     DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
-                    defaultRefreshRateInZone);
+                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1);
 
-            return refreshRate;
         }
 
         /*
@@ -2694,37 +2806,39 @@
         }
 
         public int getRefreshRateInHighZone() {
-            int defaultRefreshRateInZone = mContext.getResources().getInteger(
-                    R.integer.config_fixedRefreshRateInHighZone);
-
-            int refreshRate = mDeviceConfig.getInt(
+            return mDeviceConfig.getInt(
                     DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
                     DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
-                    defaultRefreshRateInZone);
-
-            return refreshRate;
+                    -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);
-
-            return refreshRate;
-        }
-
-        public int getRefreshRateInHbmHdr() {
-            final int defaultRefreshRateInHbmHdr =
-                    mContext.getResources().getInteger(R.integer.config_defaultRefreshRateInHbmHdr);
-
-            final int refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+        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,
-                    defaultRefreshRateInHbmHdr);
+                    refreshRate);
+            } catch (NullPointerException e) {
+                // Do Nothing
+            }
+            return refreshRate;
+        }
 
+        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;
         }
 
@@ -2750,32 +2864,42 @@
 
             int[] lowDisplayBrightnessThresholds = getLowDisplayBrightnessThresholds();
             int[] lowAmbientBrightnessThresholds = getLowAmbientBrightnessThresholds();
-            int refreshRateInLowZone = getRefreshRateInLowZone();
+            final int refreshRateInLowZone = getRefreshRateInLowZone();
 
             mHandler.obtainMessage(MSG_LOW_BRIGHTNESS_THRESHOLDS_CHANGED,
                     new Pair<>(lowDisplayBrightnessThresholds, lowAmbientBrightnessThresholds))
                     .sendToTarget();
-            mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone, 0)
+
+            if (refreshRateInLowZone != -1) {
+                mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone)
                     .sendToTarget();
+            }
 
             int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds();
             int[] highAmbientBrightnessThresholds = getHighAmbientBrightnessThresholds();
-            int refreshRateInHighZone = getRefreshRateInHighZone();
+            final int refreshRateInHighZone = getRefreshRateInHighZone();
 
             mHandler.obtainMessage(MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED,
                     new Pair<>(highDisplayBrightnessThresholds, highAmbientBrightnessThresholds))
                     .sendToTarget();
-            mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone, 0)
+
+            if (refreshRateInHighZone != -1) {
+                mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone)
+                    .sendToTarget();
+            }
+
+            synchronized (mLock) {
+                final int refreshRateInHbmSunlight =
+                        getRefreshRateInHbmSunlight(mDefaultDisplayDeviceConfig);
+                mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED,
+                        refreshRateInHbmSunlight, 0)
                     .sendToTarget();
 
-            final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight();
-            mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED,
-                    refreshRateInHbmSunlight, 0)
+                final int refreshRateInHbmHdr =
+                        getRefreshRateInHbmHdr(mDefaultDisplayDeviceConfig);
+                mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED, refreshRateInHbmHdr, 0)
                     .sendToTarget();
-
-            final int refreshRateInHbmHdr = getRefreshRateInHbmHdr();
-            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/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 54189bf7..c2bd83a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -132,6 +132,8 @@
     private static final int MSG_UPDATE_RBC = 11;
     private static final int MSG_BRIGHTNESS_RAMP_DONE = 12;
     private static final int MSG_STATSD_HBM_BRIGHTNESS = 13;
+    private static final int MSG_SWITCH_USER = 14;
+    private static final int MSG_BOOT_COMPLETED = 15;
 
     private static final int PROXIMITY_UNKNOWN = -1;
     private static final int PROXIMITY_NEGATIVE = 0;
@@ -391,6 +393,7 @@
     private float[] mNitsRange;
 
     private final HighBrightnessModeController mHbmController;
+    private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
 
     private final BrightnessThrottler mBrightnessThrottler;
 
@@ -504,6 +507,8 @@
     private boolean mIsEnabled;
     private boolean mIsInTransition;
 
+    private boolean mBootCompleted;
+
     /**
      * Creates the display power controller.
      */
@@ -511,7 +516,8 @@
             DisplayPowerCallbacks callbacks, Handler handler,
             SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay,
             BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
-            Runnable onBrightnessChangeRunnable) {
+            Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata,
+            boolean bootCompleted) {
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
         final String displayIdStr = "[" + mDisplayId + "]";
@@ -521,6 +527,7 @@
         mSuspendBlockerIdProxPositive = displayIdStr + "prox positive";
         mSuspendBlockerIdProxNegative = displayIdStr + "prox negative";
         mSuspendBlockerIdProxDebounce = displayIdStr + "prox debounce";
+        mHighBrightnessModeMetadata = hbmMetadata;
 
         mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
         mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
@@ -652,6 +659,7 @@
         mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
 
+        mBootCompleted = bootCompleted;
     }
 
     private void applyReduceBrightColorsSplineAdjustment() {
@@ -701,6 +709,11 @@
     }
 
     public void onSwitchUser(@UserIdInt int newUserId) {
+        Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, newUserId);
+        mHandler.sendMessage(msg);
+    }
+
+    private void handleOnSwitchUser(@UserIdInt int newUserId) {
         handleSettingsChange(true /* userSwitch */);
         handleBrightnessModeChange();
         if (mBrightnessTracker != null) {
@@ -793,7 +806,7 @@
      * of each display need to be properly reflected in AutomaticBrightnessController.
      */
     @GuardedBy("DisplayManagerService.mSyncRoot")
-    public void onDisplayChanged() {
+    public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata) {
         final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
         if (device == null) {
             Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: "
@@ -815,7 +828,7 @@
                 mUniqueDisplayId = uniqueId;
                 mDisplayStatsId = mUniqueDisplayId.hashCode();
                 mDisplayDeviceConfig = config;
-                loadFromDisplayDeviceConfig(token, info);
+                loadFromDisplayDeviceConfig(token, info, hbmMetadata);
 
                 /// Since the underlying display-device changed, we really don't know the
                 // last command that was sent to change it's state. Lets assume it is unknown so
@@ -872,7 +885,8 @@
         }
     }
 
-    private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info) {
+    private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
+                                             HighBrightnessModeMetadata hbmMetadata) {
         // All properties that depend on the associated DisplayDevice and the DDC must be
         // updated here.
         loadBrightnessRampRates();
@@ -885,6 +899,7 @@
                     mBrightnessRampIncreaseMaxTimeMillis,
                     mBrightnessRampDecreaseMaxTimeMillis);
         }
+        mHbmController.setHighBrightnessModeMetadata(hbmMetadata);
         mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId,
                 mDisplayDeviceConfig.getHighBrightnessModeData(),
                 new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
@@ -1302,7 +1317,7 @@
 
         if (mScreenOffBrightnessSensorController != null) {
             mScreenOffBrightnessSensorController.setLightSensorEnabled(mUseAutoBrightness
-                    && (state == Display.STATE_OFF || (state == Display.STATE_DOZE
+                    && mIsEnabled && (state == Display.STATE_OFF || (state == Display.STATE_DOZE
                     && !mAllowAutoBrightnessWhileDozingConfig)));
         }
 
@@ -1360,7 +1375,7 @@
 
         // Initialize things the first time the power state is changed.
         if (mustInitialize) {
-            initialize(state);
+            initialize(readyToUpdateDisplayState() ? state : Display.STATE_UNKNOWN);
         }
 
         // Animate the screen state change unless already animating.
@@ -1965,7 +1980,7 @@
                     if (mAutomaticBrightnessController != null) {
                         mAutomaticBrightnessController.update();
                     }
-                }, mContext);
+                }, mHighBrightnessModeMetadata, mContext);
     }
 
     private BrightnessThrottler createBrightnessThrottlerLocked() {
@@ -2040,7 +2055,8 @@
                 }
             }
 
-            if (!reportOnly && mPowerState.getScreenState() != state) {
+            if (!reportOnly && mPowerState.getScreenState() != state
+                    && readyToUpdateDisplayState()) {
                 Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
                 // TODO(b/153319140) remove when we can get this from the above trace invocation
                 SystemProperties.set("debug.tracing.screen_state", String.valueOf(state));
@@ -2487,6 +2503,10 @@
         mBrightnessSetting.setBrightness(brightnessValue);
     }
 
+    void onBootCompleted() {
+        mHandler.obtainMessage(MSG_BOOT_COMPLETED).sendToTarget();
+    }
+
     private void updateScreenBrightnessSetting(float brightnessValue) {
         if (!isValidBrightnessValue(brightnessValue)
                 || brightnessValue == mCurrentScreenBrightnessSetting) {
@@ -2632,6 +2652,17 @@
         }
     };
 
+    /**
+     * Indicates whether the display state is ready to update. If this is the default display, we
+     * want to update it right away so that we can draw the boot animation on it. If it is not
+     * the default display, drawing the boot animation on it would look incorrect, so we need
+     * to wait until boot is completed.
+     * @return True if the display state is ready to update
+     */
+    private boolean readyToUpdateDisplayState() {
+        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
+    }
+
     public void dump(final PrintWriter pw) {
         synchronized (mLock) {
             pw.println();
@@ -3163,6 +3194,15 @@
                 case MSG_STATSD_HBM_BRIGHTNESS:
                     logHbmBrightnessStats(Float.intBitsToFloat(msg.arg1), msg.arg2);
                     break;
+
+                case MSG_SWITCH_USER:
+                    handleOnSwitchUser(msg.arg1);
+                    break;
+
+                case MSG_BOOT_COMPLETED:
+                    mBootCompleted = true;
+                    updatePowerState();
+                    break;
             }
         }
     }
diff --git a/services/core/java/com/android/server/display/HbmEvent.java b/services/core/java/com/android/server/display/HbmEvent.java
new file mode 100644
index 0000000..5675e2f
--- /dev/null
+++ b/services/core/java/com/android/server/display/HbmEvent.java
@@ -0,0 +1,46 @@
+/*
+ * 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.server.display;
+
+
+/**
+ * Represents an event in which High Brightness Mode was enabled.
+ */
+class HbmEvent {
+    private long mStartTimeMillis;
+    private long mEndTimeMillis;
+
+    HbmEvent(long startTimeMillis, long endTimeMillis) {
+        this.mStartTimeMillis = startTimeMillis;
+        this.mEndTimeMillis = endTimeMillis;
+    }
+
+    public long getStartTimeMillis() {
+        return mStartTimeMillis;
+    }
+
+    public long getEndTimeMillis() {
+        return mEndTimeMillis;
+    }
+
+    @Override
+    public String toString() {
+        return "HbmEvent: {startTimeMillis:" + mStartTimeMillis + ", endTimeMillis: "
+                + mEndTimeMillis + "}, total: "
+                + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]";
+    }
+}
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 0b9d4de..ac32d53 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -42,8 +42,8 @@
 import com.android.server.display.DisplayManagerService.Clock;
 
 import java.io.PrintWriter;
+import java.util.ArrayDeque;
 import java.util.Iterator;
-import java.util.LinkedList;
 
 /**
  * Controls the status of high-brightness mode for devices that support it. This class assumes that
@@ -105,30 +105,24 @@
     private int mHbmStatsState = FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF;
 
     /**
-     * If HBM is currently running, this is the start time for the current HBM session.
+     * If HBM is currently running, this is the start time and set of all events,
+     * for the current HBM session.
      */
-    private long mRunningStartTimeMillis = -1;
-
-    /**
-     * List of previous HBM-events ordered from most recent to least recent.
-     * Meant to store only the events that fall into the most recent
-     * {@link mHbmData.timeWindowMillis}.
-     */
-    private LinkedList<HbmEvent> mEvents = new LinkedList<>();
+    private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null;
 
     HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
             String displayUniqueId, float brightnessMin, float brightnessMax,
             HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
-            Runnable hbmChangeCallback, Context context) {
+            Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) {
         this(new Injector(), handler, width, height, displayToken, displayUniqueId, brightnessMin,
-            brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, context);
+            brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, hbmMetadata, context);
     }
 
     @VisibleForTesting
     HighBrightnessModeController(Injector injector, Handler handler, int width, int height,
             IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax,
             HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
-            Runnable hbmChangeCallback, Context context) {
+            Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) {
         mInjector = injector;
         mContext = context;
         mClock = injector.getClock();
@@ -137,6 +131,7 @@
         mBrightnessMin = brightnessMin;
         mBrightnessMax = brightnessMax;
         mHbmChangeCallback = hbmChangeCallback;
+        mHighBrightnessModeMetadata = hbmMetadata;
         mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
         mSettingsObserver = new SettingsObserver(mHandler);
         mRecalcRunnable = this::recalculateTimeAllowance;
@@ -222,19 +217,22 @@
 
         // If we are starting or ending a high brightness mode session, store the current
         // session in mRunningStartTimeMillis, or the old one in mEvents.
-        final boolean wasHbmDrainingAvailableTime = mRunningStartTimeMillis != -1;
+        final long runningStartTime = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+        final boolean wasHbmDrainingAvailableTime = runningStartTime != -1;
         final boolean shouldHbmDrainAvailableTime = mBrightness > mHbmData.transitionPoint
                 && !mIsHdrLayerPresent;
         if (wasHbmDrainingAvailableTime != shouldHbmDrainAvailableTime) {
             final long currentTime = mClock.uptimeMillis();
             if (shouldHbmDrainAvailableTime) {
-                mRunningStartTimeMillis = currentTime;
+                mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime);
             } else {
-                mEvents.addFirst(new HbmEvent(mRunningStartTimeMillis, currentTime));
-                mRunningStartTimeMillis = -1;
+                final HbmEvent hbmEvent = new HbmEvent(runningStartTime, currentTime);
+                mHighBrightnessModeMetadata.addHbmEvent(hbmEvent);
+                mHighBrightnessModeMetadata.setRunningStartTimeMillis(-1);
 
                 if (DEBUG) {
-                    Slog.d(TAG, "New HBM event: " + mEvents.getFirst());
+                    Slog.d(TAG, "New HBM event: "
+                            + mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst());
                 }
             }
         }
@@ -260,6 +258,10 @@
         mSettingsObserver.stopObserving();
     }
 
+    void setHighBrightnessModeMetadata(HighBrightnessModeMetadata hbmInfo) {
+        mHighBrightnessModeMetadata = hbmInfo;
+    }
+
     void resetHbmData(int width, int height, IBinder displayToken, String displayUniqueId,
             HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg) {
         mWidth = width;
@@ -316,20 +318,22 @@
         pw.println("  mBrightnessMax=" + mBrightnessMax);
         pw.println("  remainingTime=" + calculateRemainingTime(mClock.uptimeMillis()));
         pw.println("  mIsTimeAvailable= " + mIsTimeAvailable);
-        pw.println("  mRunningStartTimeMillis=" + TimeUtils.formatUptime(mRunningStartTimeMillis));
+        pw.println("  mRunningStartTimeMillis="
+                + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
         pw.println("  mIsThermalStatusWithinLimit=" + mIsThermalStatusWithinLimit);
         pw.println("  mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
         pw.println("  width*height=" + mWidth + "*" + mHeight);
         pw.println("  mEvents=");
         final long currentTime = mClock.uptimeMillis();
         long lastStartTime = currentTime;
-        if (mRunningStartTimeMillis != -1) {
-            lastStartTime = dumpHbmEvent(pw, new HbmEvent(mRunningStartTimeMillis, currentTime));
+        long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+        if (runningStartTimeMillis != -1) {
+            lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
         }
-        for (HbmEvent event : mEvents) {
-            if (lastStartTime > event.endTimeMillis) {
+        for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
+            if (lastStartTime > event.getEndTimeMillis()) {
                 pw.println("    event: [normal brightness]: "
-                        + TimeUtils.formatDuration(lastStartTime - event.endTimeMillis));
+                        + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
             }
             lastStartTime = dumpHbmEvent(pw, event);
         }
@@ -338,12 +342,12 @@
     }
 
     private long dumpHbmEvent(PrintWriter pw, HbmEvent event) {
-        final long duration = event.endTimeMillis - event.startTimeMillis;
+        final long duration = event.getEndTimeMillis() - event.getStartTimeMillis();
         pw.println("    event: ["
-                + TimeUtils.formatUptime(event.startTimeMillis) + ", "
-                + TimeUtils.formatUptime(event.endTimeMillis) + "] ("
+                + TimeUtils.formatUptime(event.getStartTimeMillis()) + ", "
+                + TimeUtils.formatUptime(event.getEndTimeMillis()) + "] ("
                 + TimeUtils.formatDuration(duration) + ")");
-        return event.startTimeMillis;
+        return event.getStartTimeMillis();
     }
 
     private boolean isCurrentlyAllowed() {
@@ -372,13 +376,15 @@
 
         // First, lets see how much time we've taken for any currently running
         // session of HBM.
-        if (mRunningStartTimeMillis > 0) {
-            if (mRunningStartTimeMillis > currentTime) {
+        long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+        if (runningStartTimeMillis > 0) {
+            if (runningStartTimeMillis > currentTime) {
                 Slog.e(TAG, "Start time set to the future. curr: " + currentTime
-                        + ", start: " + mRunningStartTimeMillis);
-                mRunningStartTimeMillis = currentTime;
+                        + ", start: " + runningStartTimeMillis);
+                mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime);
+                runningStartTimeMillis = currentTime;
             }
-            timeAlreadyUsed = currentTime - mRunningStartTimeMillis;
+            timeAlreadyUsed = currentTime - runningStartTimeMillis;
         }
 
         if (DEBUG) {
@@ -387,18 +393,19 @@
 
         // Next, lets iterate through the history of previous sessions and add those times.
         final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis;
-        Iterator<HbmEvent> it = mEvents.iterator();
+        Iterator<HbmEvent> it = mHighBrightnessModeMetadata.getHbmEventQueue().iterator();
         while (it.hasNext()) {
             final HbmEvent event = it.next();
 
             // If this event ended before the current Timing window, discard forever and ever.
-            if (event.endTimeMillis < windowstartTimeMillis) {
+            if (event.getEndTimeMillis() < windowstartTimeMillis) {
                 it.remove();
                 continue;
             }
 
-            final long startTimeMillis = Math.max(event.startTimeMillis, windowstartTimeMillis);
-            timeAlreadyUsed += event.endTimeMillis - startTimeMillis;
+            final long startTimeMillis = Math.max(event.getStartTimeMillis(),
+                            windowstartTimeMillis);
+            timeAlreadyUsed += event.getEndTimeMillis() - startTimeMillis;
         }
 
         if (DEBUG) {
@@ -425,17 +432,18 @@
         // Calculate the time at which we want to recalculate mIsTimeAvailable in case a lux or
         // brightness change doesn't happen before then.
         long nextTimeout = -1;
+        final ArrayDeque<HbmEvent> hbmEvents = mHighBrightnessModeMetadata.getHbmEventQueue();
         if (mBrightness > mHbmData.transitionPoint) {
             // if we're in high-lux now, timeout when we run out of allowed time.
             nextTimeout = currentTime + remainingTime;
-        } else if (!mIsTimeAvailable && mEvents.size() > 0) {
+        } else if (!mIsTimeAvailable && hbmEvents.size() > 0) {
             // If we are not allowed...timeout when the oldest event moved outside of the timing
             // window by at least minTime. Basically, we're calculating the soonest time we can
             // get {@code timeMinMillis} back to us.
             final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis;
-            final HbmEvent lastEvent = mEvents.getLast();
+            final HbmEvent lastEvent = hbmEvents.peekLast();
             final long startTimePlusMinMillis =
-                    Math.max(windowstartTimeMillis, lastEvent.startTimeMillis)
+                    Math.max(windowstartTimeMillis, lastEvent.getStartTimeMillis())
                     + mHbmData.timeMinMillis;
             final long timeWhenMinIsGainedBack =
                     currentTime + (startTimePlusMinMillis - windowstartTimeMillis) - remainingTime;
@@ -459,9 +467,10 @@
                     + ", mUnthrottledBrightness: " + mUnthrottledBrightness
                     + ", mThrottlingReason: "
                         + BrightnessInfo.briMaxReasonToString(mThrottlingReason)
-                    + ", RunningStartTimeMillis: " + mRunningStartTimeMillis
+                    + ", RunningStartTimeMillis: "
+                        + mHighBrightnessModeMetadata.getRunningStartTimeMillis()
                     + ", nextTimeout: " + (nextTimeout != -1 ? (nextTimeout - currentTime) : -1)
-                    + ", events: " + mEvents);
+                    + ", events: " + hbmEvents);
         }
 
         if (nextTimeout != -1) {
@@ -588,25 +597,6 @@
         }
     }
 
-    /**
-     * Represents an event in which High Brightness Mode was enabled.
-     */
-    private static class HbmEvent {
-        public long startTimeMillis;
-        public long endTimeMillis;
-
-        HbmEvent(long startTimeMillis, long endTimeMillis) {
-            this.startTimeMillis = startTimeMillis;
-            this.endTimeMillis = endTimeMillis;
-        }
-
-        @Override
-        public String toString() {
-            return "[Event: {" + startTimeMillis + ", " + endTimeMillis + "}, total: "
-                    + ((endTimeMillis - startTimeMillis) / 1000) + "]";
-        }
-    }
-
     @VisibleForTesting
     class HdrListener extends SurfaceControlHdrLayerInfoListener {
         @Override
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java
new file mode 100644
index 0000000..8aa3631
--- /dev/null
+++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.display;
+
+import java.util.ArrayDeque;
+
+
+/**
+ * Represents High Brightness Mode metadata associated
+ * with a specific display.
+ * Required for separately storing data like time information,
+ * and related events when display was in HBM mode per display.
+ */
+class HighBrightnessModeMetadata {
+    /**
+     * Queue of previous HBM-events ordered from most recent to least recent.
+     * Meant to store only the events that fall into the most recent
+     * {@link HighBrightnessModeData#timeWindowMillis mHbmData.timeWindowMillis}.
+     */
+    private final ArrayDeque<HbmEvent> mEvents = new ArrayDeque<>();
+
+    /**
+     * If HBM is currently running, this is the start time for the current HBM session.
+     */
+    private long mRunningStartTimeMillis = -1;
+
+    public long getRunningStartTimeMillis() {
+        return mRunningStartTimeMillis;
+    }
+
+    public void setRunningStartTimeMillis(long setTime) {
+        mRunningStartTimeMillis = setTime;
+    }
+
+    public ArrayDeque<HbmEvent> getHbmEventQueue() {
+        return mEvents;
+    }
+
+    public void addHbmEvent(HbmEvent hbmEvent) {
+        mEvents.addFirst(hbmEvent);
+    }
+}
+
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index bf576b8..1f7232a 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -159,6 +159,7 @@
     private Layout mCurrentLayout = null;
     private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
     private int mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
+    private int mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE;
     private boolean mBootCompleted = false;
     private boolean mInteractive;
 
@@ -353,6 +354,12 @@
         ipw.println("mDeviceStatesOnWhichToWakeUp=" + mDeviceStatesOnWhichToWakeUp);
         ipw.println("mDeviceStatesOnWhichToSleep=" + mDeviceStatesOnWhichToSleep);
         ipw.println("mInteractive=" + mInteractive);
+        ipw.println("mBootCompleted=" + mBootCompleted);
+
+        ipw.println();
+        ipw.println("mDeviceState=" + mDeviceState);
+        ipw.println("mPendingDeviceState=" + mPendingDeviceState);
+        ipw.println("mDeviceStateToBeAppliedAfterBoot=" + mDeviceStateToBeAppliedAfterBoot);
 
         final int logicalDisplayCount = mLogicalDisplays.size();
         ipw.println();
@@ -370,6 +377,17 @@
     }
 
     void setDeviceStateLocked(int state, boolean isOverrideActive) {
+        if (!mBootCompleted) {
+            // The boot animation might still be in progress, we do not want to switch states now
+            // as the boot animation would end up with an incorrect size.
+            if (DEBUG) {
+                Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState
+                        + " until boot is completed");
+            }
+            mDeviceStateToBeAppliedAfterBoot = state;
+            return;
+        }
+
         Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
                 + ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted);
         // As part of a state transition, we may need to turn off some displays temporarily so that
@@ -378,6 +396,7 @@
         // temporarily turned off.
         resetLayoutLocked(mDeviceState, state, /* isStateChangeStarting= */ true);
         mPendingDeviceState = state;
+        mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE;
         final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState,
                 mInteractive, mBootCompleted);
         final boolean sleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, mDeviceState,
@@ -424,6 +443,10 @@
     void onBootCompleted() {
         synchronized (mSyncRoot) {
             mBootCompleted = true;
+            if (mDeviceStateToBeAppliedAfterBoot != DeviceStateManager.INVALID_DEVICE_STATE) {
+                setDeviceStateLocked(mDeviceStateToBeAppliedAfterBoot,
+                        /* isOverrideActive= */ false);
+            }
         }
     }
 
@@ -477,7 +500,8 @@
     @VisibleForTesting
     boolean shouldDeviceBePutToSleep(int pendingState, int currentState, boolean isOverrideActive,
             boolean isInteractive, boolean isBootCompleted) {
-        return mDeviceStatesOnWhichToSleep.get(pendingState)
+        return currentState != DeviceStateManager.INVALID_DEVICE_STATE
+                && mDeviceStatesOnWhichToSleep.get(pendingState)
                 && !mDeviceStatesOnWhichToSleep.get(currentState)
                 && !isOverrideActive
                 && isInteractive && isBootCompleted;
@@ -926,6 +950,15 @@
         final int layerStack = assignLayerStackLocked(displayId);
         final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device);
         display.updateLocked(mDisplayDeviceRepo);
+
+        final DisplayInfo info = display.getDisplayInfoLocked();
+        if (info.type == Display.TYPE_INTERNAL && mDeviceStateToLayoutMap.size() > 1) {
+            // If this is an internal display and the device uses a display layout configuration,
+            // the display should be disabled as later we will receive a device state update, which
+            // will tell us which internal displays should be enabled and which should be disabled.
+            display.setEnabledLocked(false);
+        }
+
         mLogicalDisplays.put(displayId, display);
         return display;
     }
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 017b96c..2276715 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -107,11 +107,6 @@
         Matrix.setIdentityM(MATRIX_IDENTITY, 0);
     }
 
-    /**
-     * The transition time, in milliseconds, for Night Display to turn on/off.
-     */
-    private static final long TRANSITION_DURATION = 3000L;
-
     private static final int MSG_USER_CHANGED = 0;
     private static final int MSG_SET_UP = 1;
     private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2;
@@ -661,7 +656,7 @@
             TintValueAnimator valueAnimator = TintValueAnimator.ofMatrix(COLOR_MATRIX_EVALUATOR,
                     from == null ? MATRIX_IDENTITY : from, to);
             tintController.setAnimator(valueAnimator);
-            valueAnimator.setDuration(TRANSITION_DURATION);
+            valueAnimator.setDuration(tintController.getTransitionDurationMilliseconds());
             valueAnimator.setInterpolator(AnimationUtils.loadInterpolator(
                     getContext(), android.R.interpolator.fast_out_slow_in));
             valueAnimator.addUpdateListener((ValueAnimator animator) -> {
diff --git a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
index 93a78c1..f27ccc7 100644
--- a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
+++ b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
@@ -59,7 +59,8 @@
     private float[] mCurrentColorTemperatureXYZ;
     @VisibleForTesting
     boolean mSetUp = false;
-    private float[] mMatrixDisplayWhiteBalance = new float[16];
+    private final float[] mMatrixDisplayWhiteBalance = new float[16];
+    private long mTransitionDuration;
     private Boolean mIsAvailable;
     // This feature becomes disallowed if the device is in an unsupported strong/light state.
     private boolean mIsAllowed = true;
@@ -119,6 +120,9 @@
         final int colorTemperature = res.getInteger(
                 R.integer.config_displayWhiteBalanceColorTemperatureDefault);
 
+        mTransitionDuration = res.getInteger(
+                R.integer.config_displayWhiteBalanceTransitionTime);
+
         synchronized (mLock) {
             mDisplayColorSpaceRGB = displayColorSpaceRGB;
             mDisplayNominalWhiteXYZ = displayNominalWhiteXYZ;
@@ -232,6 +236,11 @@
     }
 
     @Override
+    public long getTransitionDurationMilliseconds() {
+        return mTransitionDuration;
+    }
+
+    @Override
     public void dump(PrintWriter pw) {
         synchronized (mLock) {
             pw.println("    mSetUp = " + mSetUp);
diff --git a/services/core/java/com/android/server/display/color/TintController.java b/services/core/java/com/android/server/display/color/TintController.java
index 422dd32..c53ac06 100644
--- a/services/core/java/com/android/server/display/color/TintController.java
+++ b/services/core/java/com/android/server/display/color/TintController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.display.color;
 
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.Slog;
 
@@ -24,6 +23,11 @@
 
 abstract class TintController {
 
+    /**
+     * The default transition time, in milliseconds, for color transforms to turn on/off.
+     */
+    private static final long TRANSITION_DURATION = 3000L;
+
     private ColorDisplayService.TintValueAnimator mAnimator;
     private Boolean mIsActivated;
 
@@ -66,6 +70,10 @@
         return mIsActivated == null;
     }
 
+    public long getTransitionDurationMilliseconds() {
+        return TRANSITION_DURATION;
+    }
+
     /**
      * Dump debug information.
      */
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
index 85d5b4f..5b772fc 100644
--- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
@@ -419,16 +419,16 @@
         float ambientBrightness = mBrightnessFilter.getEstimate(time);
         mLatestAmbientBrightness = ambientBrightness;
 
-        if (ambientColorTemperature != -1.0f &&
-                mLowLightAmbientBrightnessToBiasSpline != null) {
+        if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
+                && mLowLightAmbientBrightnessToBiasSpline != null) {
             float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
             ambientColorTemperature =
                     bias * ambientColorTemperature + (1.0f - bias)
                     * mLowLightAmbientColorTemperature;
             mLatestLowLightBias = bias;
         }
-        if (ambientColorTemperature != -1.0f &&
-                mHighLightAmbientBrightnessToBiasSpline != null) {
+        if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
+                && mHighLightAmbientBrightnessToBiasSpline != null) {
             float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
             ambientColorTemperature =
                     (1.0f - bias) * ambientColorTemperature + bias
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index db9deb1..ccab7bc9 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -42,6 +42,7 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.Objects;
 
 /**
  * Internal controller for starting and stopping the current dream and managing related state.
@@ -119,10 +120,19 @@
                     + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze
                     + ", userId=" + userId + ", reason='" + reason + "'");
 
-            if (mCurrentDream != null) {
-                mPreviousDreams.add(mCurrentDream);
-            }
+            final DreamRecord oldDream = mCurrentDream;
             mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock);
+            if (oldDream != null) {
+                if (Objects.equals(oldDream.mName, mCurrentDream.mName)) {
+                    // We are attempting to start a dream that is currently waking up gently.
+                    // Let's silently stop the old instance here to clear the dream state.
+                    // This should happen after the new mCurrentDream is set to avoid announcing
+                    // a "dream stopped" state.
+                    stopDreamInstance(/* immediately */ true, "restarting same dream", oldDream);
+                } else {
+                    mPreviousDreams.add(oldDream);
+                }
+            }
 
             mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime();
             MetricsLogger.visible(mContext,
@@ -245,6 +255,7 @@
 
                 mListener.onDreamStopped(dream.mToken);
             }
+
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
@@ -269,7 +280,7 @@
         try {
             service.asBinder().linkToDeath(mCurrentDream, 0);
             service.attach(mCurrentDream.mToken, mCurrentDream.mCanDoze,
-                    mCurrentDream.mDreamingStartedCallback);
+                    mCurrentDream.mIsPreviewMode, mCurrentDream.mDreamingStartedCallback);
         } catch (RemoteException ex) {
             Slog.e(TAG, "The dream service died unexpectedly.", ex);
             stopDream(true /*immediate*/, "attach failed");
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index bb1e393..5b375d7 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -660,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)) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index faa219e..cbbee5d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4831,6 +4831,13 @@
             Slog.w(TAG, "Ignoring setInputMethod of uid " + Binder.getCallingUid()
                     + " token: " + token);
             return;
+        } else {
+            // Called with current IME's token.
+            if (mMethodMap.get(id) != null
+                    && mSettings.getEnabledInputMethodListWithFilterLocked(
+                            (info) -> info.getId().equals(id)).isEmpty()) {
+                throw new IllegalStateException("Requested IME is not enabled: " + id);
+            }
         }
 
         final long ident = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index dc52990..1fb00ef 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.server.location.injector;
 
+import static com.android.server.location.LocationManagerService.TAG;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -23,6 +25,7 @@
 import android.os.SystemClock;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
+import android.util.Log;
 
 import com.android.server.FgThread;
 
@@ -67,8 +70,12 @@
                 }
 
                 synchronized (SystemEmergencyHelper.this) {
-                    mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
-                            intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+                    try {
+                        mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
+                                intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+                    } catch (IllegalStateException e) {
+                        Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
+                    }
                 }
             }
         }, new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL));
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 1235352..f0aff2a 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -300,6 +300,7 @@
         public void deliverOnFlushComplete(int requestCode) throws PendingIntent.CanceledException {
             BroadcastOptions options = BroadcastOptions.makeBasic();
             options.setDontSendToRestrictedApps(true);
+            options.setPendingIntentBackgroundActivityLaunchAllowed(false);
 
             mPendingIntent.send(mContext, 0, new Intent().putExtra(KEY_FLUSH_COMPLETE, requestCode),
                     null, null, null, options.toBundle());
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 78cffa6..59794f4 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -80,6 +80,7 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -117,6 +118,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
+import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -203,7 +205,7 @@
     private static final String TAG = "LockSettingsService";
     private static final String PERMISSION = ACCESS_KEYGUARD_SECURE_STORAGE;
     private static final String BIOMETRIC_PERMISSION = MANAGE_BIOMETRIC;
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int PROFILE_KEY_IV_SIZE = 12;
     private static final String SEPARATE_PROFILE_CHALLENGE_KEY = "lockscreen.profilechallenge";
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
index 1203769..678698b 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
@@ -25,6 +25,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.IStrongAuthTracker;
 import android.content.Context;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -33,6 +34,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
@@ -46,8 +48,8 @@
  */
 public class LockSettingsStrongAuth {
 
-    private static final String TAG = "LockSettings";
-    private static final boolean DEBUG = false;
+    private static final String TAG = "LockSettingsStrongAuth";
+    private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int MSG_REQUIRE_STRONG_AUTH = 1;
     private static final int MSG_REGISTER_TRACKER = 2;
@@ -267,6 +269,7 @@
     }
 
     private void handleScheduleStrongAuthTimeout(int userId) {
+        if (DEBUG) Slog.d(TAG, "handleScheduleStrongAuthTimeout for userId=" + userId);
         rescheduleStrongAuthTimeoutAlarm(mInjector.getElapsedRealtimeMs(), userId);
 
         // cancel current non-strong biometric alarm listener for the user (if there was one)
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 4d55d4e..25fefad 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -132,6 +132,7 @@
 
     // contains connections to all connected services, including app services
     // and system services
+    @GuardedBy("mMutex")
     private final ArrayList<ManagedServiceInfo> mServices = new ArrayList<>();
     /**
      * The services that have been bound by us. If the service is also connected, it will also
@@ -150,13 +151,15 @@
             = new ArraySet<>();
     // Just the packages from mEnabledServicesForCurrentProfiles
     private ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
-    // List of enabled packages that have nevertheless asked not to be run
-    private ArraySet<ComponentName> mSnoozingForCurrentProfiles = new ArraySet<>();
+    // Per user id, list of enabled packages that have nevertheless asked not to be run
+    private final android.util.SparseSetArray<ComponentName> mSnoozing =
+            new android.util.SparseSetArray<>();
 
     // List of approved packages or components (by user, then by primary/secondary) that are
     // allowed to be bound as managed services. A package or component appearing in this list does
     // not mean that we are currently bound to said package/component.
-    protected ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved = new ArrayMap<>();
+    protected final ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved =
+            new ArrayMap<>();
 
     // List of packages or components (by user) that are configured to be enabled/disabled
     // explicitly by the user
@@ -315,6 +318,7 @@
         return changes;
     }
 
+    @GuardedBy("mApproved")
     private boolean clearUserSetFlagLocked(ComponentName component, int userId) {
         String approvedValue = getApprovedValue(component.flattenToString());
         ArraySet<String> userSet = mUserSetServices.get(userId);
@@ -375,8 +379,8 @@
             pw.println("      " + cmpt);
         }
 
-        pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
         synchronized (mMutex) {
+            pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
             for (ManagedServiceInfo info : mServices) {
                 if (filter != null && !filter.matches(info.component)) continue;
                 pw.println("      " + info.component
@@ -386,10 +390,15 @@
             }
         }
 
-        pw.println("    Snoozed " + getCaption() + "s (" +
-                mSnoozingForCurrentProfiles.size() + "):");
-        for (ComponentName name : mSnoozingForCurrentProfiles) {
-            pw.println("      " + name.flattenToShortString());
+        synchronized (mSnoozing) {
+            pw.println("    Snoozed " + getCaption() + "s ("
+                    + mSnoozing.size() + "):");
+            for (int i = 0; i < mSnoozing.size(); i++) {
+                pw.println("      User: " + mSnoozing.keyAt(i));
+                for (ComponentName name : mSnoozing.valuesAt(i)) {
+                    pw.println("        " + name.flattenToShortString());
+                }
+            }
         }
     }
 
@@ -431,8 +440,16 @@
             }
         }
 
-        for (ComponentName name : mSnoozingForCurrentProfiles) {
-            name.dumpDebug(proto, ManagedServicesProto.SNOOZED);
+        synchronized (mSnoozing) {
+            for (int i = 0; i < mSnoozing.size(); i++) {
+                long token = proto.start(ManagedServicesProto.SNOOZED);
+                proto.write(ManagedServicesProto.SnoozedServices.USER_ID,
+                        mSnoozing.keyAt(i));
+                for (ComponentName name : mSnoozing.valuesAt(i)) {
+                    name.dumpDebug(proto, ManagedServicesProto.SnoozedServices.SNOOZED);
+                }
+                proto.end(token);
+            }
         }
     }
 
@@ -975,6 +992,9 @@
         synchronized (mApproved) {
             mApproved.remove(user);
         }
+        synchronized (mSnoozing) {
+            mSnoozing.remove(user);
+        }
         rebindServices(true, user);
     }
 
@@ -994,10 +1014,12 @@
             return null;
         }
         final IBinder token = service.asBinder();
-        final int N = mServices.size();
-        for (int i = 0; i < N; i++) {
-            final ManagedServiceInfo info = mServices.get(i);
-            if (info.service.asBinder() == token) return info;
+        synchronized (mMutex) {
+            final int nServices = mServices.size();
+            for (int i = 0; i < nServices; i++) {
+                final ManagedServiceInfo info = mServices.get(i);
+                if (info.service.asBinder() == token) return info;
+            }
         }
         return null;
     }
@@ -1066,15 +1088,17 @@
     }
 
     protected void setComponentState(ComponentName component, int userId, boolean enabled) {
-        boolean previous = !mSnoozingForCurrentProfiles.contains(component);
-        if (previous == enabled) {
-            return;
-        }
+        synchronized (mSnoozing) {
+            boolean previous = !mSnoozing.contains(userId, component);
+            if (previous == enabled) {
+                return;
+            }
 
-        if (enabled) {
-            mSnoozingForCurrentProfiles.remove(component);
-        } else {
-            mSnoozingForCurrentProfiles.add(component);
+            if (enabled) {
+                mSnoozing.remove(userId, component);
+            } else {
+                mSnoozing.add(userId, component);
+            }
         }
 
         // State changed
@@ -1287,7 +1311,10 @@
             }
 
             final Set<ComponentName> add = new HashSet<>(userComponents);
-            add.removeAll(mSnoozingForCurrentProfiles);
+            ArraySet<ComponentName> snoozed = mSnoozing.get(userId);
+            if (snoozed != null) {
+                add.removeAll(snoozed);
+            }
 
             componentsToBind.put(userId, add);
 
@@ -1466,10 +1493,12 @@
         }
     }
 
+    @GuardedBy("mMutex")
     private void registerServiceLocked(final ComponentName name, final int userid) {
         registerServiceLocked(name, userid, false /* isSystem */);
     }
 
+    @GuardedBy("mMutex")
     private void registerServiceLocked(final ComponentName name, final int userid,
             final boolean isSystem) {
         if (DEBUG) Slog.v(TAG, "registerService: " + name + " u=" + userid);
@@ -1600,6 +1629,7 @@
         }
     }
 
+    @GuardedBy("mMutex")
     private void unregisterServiceLocked(ComponentName name, int userid) {
         final int N = mServices.size();
         for (int i = N - 1; i >= 0; i--) {
@@ -1634,6 +1664,7 @@
         return serviceInfo;
     }
 
+    @GuardedBy("mMutex")
     private ManagedServiceInfo removeServiceLocked(int i) {
         final ManagedServiceInfo info = mServices.remove(i);
         onServiceRemovedLocked(info);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 195101d..1f25905 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -239,7 +239,6 @@
 import android.service.notification.NotificationRecordProto;
 import android.service.notification.NotificationServiceDumpProto;
 import android.service.notification.NotificationStats;
-import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeProto;
@@ -6707,6 +6706,31 @@
             }
         }
 
+        // Ensure all actions are present
+        if (notification.actions != null) {
+            boolean hasNullActions = false;
+            int nActions = notification.actions.length;
+            for (int i = 0; i < nActions; i++) {
+                if (notification.actions[i] == null) {
+                    hasNullActions = true;
+                    break;
+                }
+            }
+            if (hasNullActions) {
+                ArrayList<Notification.Action> nonNullActions = new ArrayList<>();
+                for (int i = 0; i < nActions; i++) {
+                    if (notification.actions[i] != null) {
+                        nonNullActions.add(notification.actions[i]);
+                    }
+                }
+                if (nonNullActions.size() != 0) {
+                    notification.actions = nonNullActions.toArray(new Notification.Action[0]);
+                } else {
+                    notification.actions = null;
+                }
+            }
+        }
+
         // Ensure CallStyle has all the correct actions
         if (notification.isStyle(Notification.CallStyle.class)) {
             Notification.Builder builder =
@@ -8561,6 +8585,9 @@
             if (interceptBefore && !record.isIntercepted()
                     && record.isNewEnoughForAlerting(System.currentTimeMillis())) {
                 buzzBeepBlinkLocked(record);
+
+                // Log alert after change in intercepted state to Zen Log as well
+                ZenLog.traceAlertOnUpdatedIntercept(record);
             }
         }
         if (changed) {
@@ -8568,95 +8595,6 @@
         }
     }
 
-    static class NotificationRecordExtractorData {
-        // Class that stores any field in a NotificationRecord that can change via an extractor.
-        // Used to cache previous data used in a sort.
-        int mPosition;
-        int mVisibility;
-        boolean mShowBadge;
-        boolean mAllowBubble;
-        boolean mIsBubble;
-        NotificationChannel mChannel;
-        String mGroupKey;
-        ArrayList<String> mOverridePeople;
-        ArrayList<SnoozeCriterion> mSnoozeCriteria;
-        Integer mUserSentiment;
-        Integer mSuppressVisually;
-        ArrayList<Notification.Action> mSystemSmartActions;
-        ArrayList<CharSequence> mSmartReplies;
-        int mImportance;
-
-        // These fields may not trigger a reranking but diffs here may be logged.
-        float mRankingScore;
-        boolean mIsConversation;
-
-        NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
-                boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
-                ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
-                Integer userSentiment, Integer suppressVisually,
-                ArrayList<Notification.Action> systemSmartActions,
-                ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
-                boolean isConversation) {
-            mPosition = position;
-            mVisibility = visibility;
-            mShowBadge = showBadge;
-            mAllowBubble = allowBubble;
-            mIsBubble = isBubble;
-            mChannel = channel;
-            mGroupKey = groupKey;
-            mOverridePeople = overridePeople;
-            mSnoozeCriteria = snoozeCriteria;
-            mUserSentiment = userSentiment;
-            mSuppressVisually = suppressVisually;
-            mSystemSmartActions = systemSmartActions;
-            mSmartReplies = smartReplies;
-            mImportance = importance;
-            mRankingScore = rankingScore;
-            mIsConversation = isConversation;
-        }
-
-        // Returns whether the provided NotificationRecord differs from the cached data in any way.
-        // Should be guarded by mNotificationLock; not annotated here as this class is static.
-        boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
-            return mPosition != newPosition
-                    || mVisibility != r.getPackageVisibilityOverride()
-                    || mShowBadge != r.canShowBadge()
-                    || mAllowBubble != r.canBubble()
-                    || mIsBubble != r.getNotification().isBubbleNotification()
-                    || !Objects.equals(mChannel, r.getChannel())
-                    || !Objects.equals(mGroupKey, r.getGroupKey())
-                    || !Objects.equals(mOverridePeople, r.getPeopleOverride())
-                    || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
-                    || !Objects.equals(mUserSentiment, r.getUserSentiment())
-                    || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
-                    || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
-                    || !Objects.equals(mSmartReplies, r.getSmartReplies())
-                    || mImportance != r.getImportance();
-        }
-
-        // Returns whether the NotificationRecord has a change from this data for which we should
-        // log an update. This method specifically targets fields that may be changed via
-        // adjustments from the assistant.
-        //
-        // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
-        // and NotificationRecord.applyAdjustments.
-        //
-        // Should be guarded by mNotificationLock; not annotated here as this class is static.
-        boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
-            return mPosition != newPosition
-                    || !Objects.equals(mChannel, r.getChannel())
-                    || !Objects.equals(mGroupKey, r.getGroupKey())
-                    || !Objects.equals(mOverridePeople, r.getPeopleOverride())
-                    || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
-                    || !Objects.equals(mUserSentiment, r.getUserSentiment())
-                    || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
-                    || !Objects.equals(mSmartReplies, r.getSmartReplies())
-                    || mImportance != r.getImportance()
-                    || !r.rankingScoreMatches(mRankingScore)
-                    || mIsConversation != r.isConversation();
-        }
-    }
-
     void handleRankingSort() {
         if (mRankingHelper == null) return;
         synchronized (mNotificationLock) {
@@ -8682,7 +8620,8 @@
                         r.getSmartReplies(),
                         r.getImportance(),
                         r.getRankingScore(),
-                        r.isConversation());
+                        r.isConversation(),
+                        r.getProposedImportance());
                 extractorDataBefore.put(r.getKey(), extractorData);
                 mRankingHelper.extractSignals(r);
             }
@@ -9977,7 +9916,8 @@
                     record.getRankingScore() == 0
                             ? RANKING_UNCHANGED
                             : (record.getRankingScore() > 0 ?  RANKING_PROMOTED : RANKING_DEMOTED),
-                    record.getNotification().isBubbleNotification()
+                    record.getNotification().isBubbleNotification(),
+                    record.getProposedImportance()
             );
             rankings.add(ranking);
         }
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index cbaf485..1501d69 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -114,6 +114,8 @@
 
     // is this notification currently being intercepted by Zen Mode?
     private boolean mIntercept;
+    // has the intercept value been set explicitly? we only want to log it if new or changed
+    private boolean mInterceptSet;
 
     // is this notification hidden since the app pkg is suspended?
     private boolean mHidden;
@@ -210,6 +212,7 @@
     // Whether this notification record should have an update logged the next time notifications
     // are sorted.
     private boolean mPendingLogUpdate = false;
+    private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
 
     public NotificationRecord(Context context, StatusBarNotification sbn,
             NotificationChannel channel) {
@@ -499,6 +502,8 @@
         pw.println(prefix + "mImportance="
                 + NotificationListenerService.Ranking.importanceToString(mImportance));
         pw.println(prefix + "mImportanceExplanation=" + getImportanceExplanation());
+        pw.println(prefix + "mProposedImportance="
+                + NotificationListenerService.Ranking.importanceToString(mProposedImportance));
         pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
         pw.println(prefix + "mIntercept=" + mIntercept);
         pw.println(prefix + "mHidden==" + mHidden);
@@ -738,6 +743,12 @@
                             Adjustment.KEY_NOT_CONVERSATION,
                             Boolean.toString(mIsNotConversationOverride));
                 }
+                if (signals.containsKey(Adjustment.KEY_IMPORTANCE_PROPOSAL)) {
+                    mProposedImportance = signals.getInt(Adjustment.KEY_IMPORTANCE_PROPOSAL);
+                    EventLogTags.writeNotificationAdjusted(getKey(),
+                            Adjustment.KEY_IMPORTANCE_PROPOSAL,
+                            Integer.toString(mProposedImportance));
+                }
                 if (!signals.isEmpty() && adjustment.getIssuer() != null) {
                     mAdjustmentIssuer = adjustment.getIssuer();
                 }
@@ -870,6 +881,10 @@
         return stats.naturalImportance;
     }
 
+    public int getProposedImportance() {
+        return mProposedImportance;
+    }
+
     public float getRankingScore() {
         return mRankingScore;
     }
@@ -901,6 +916,7 @@
 
     public boolean setIntercepted(boolean intercept) {
         mIntercept = intercept;
+        mInterceptSet = true;
         return mIntercept;
     }
 
@@ -921,6 +937,10 @@
         return mIntercept;
     }
 
+    public boolean hasInterceptBeenSet() {
+        return mInterceptSet;
+    }
+
     public boolean isNewEnoughForAlerting(long now) {
         return getFreshnessMs(now) <= MAX_SOUND_DELAY_MS;
     }
diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
new file mode 100644
index 0000000..6dc9029
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
@@ -0,0 +1,118 @@
+/*
+ * 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.server.notification;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.service.notification.SnoozeCriterion;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Class that stores any field in a NotificationRecord that can change via an extractor.
+ * Used to cache previous data used in a sort.
+ */
+public final class NotificationRecordExtractorData {
+    private final int mPosition;
+    private final int mVisibility;
+    private final boolean mShowBadge;
+    private final boolean mAllowBubble;
+    private final boolean mIsBubble;
+    private final NotificationChannel mChannel;
+    private final String mGroupKey;
+    private final ArrayList<String> mOverridePeople;
+    private final ArrayList<SnoozeCriterion> mSnoozeCriteria;
+    private final Integer mUserSentiment;
+    private final Integer mSuppressVisually;
+    private final ArrayList<Notification.Action> mSystemSmartActions;
+    private final ArrayList<CharSequence> mSmartReplies;
+    private final int mImportance;
+
+    // These fields may not trigger a reranking but diffs here may be logged.
+    private final float mRankingScore;
+    private final boolean mIsConversation;
+    private final int mProposedImportance;
+
+    NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
+            boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
+            ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
+            Integer userSentiment, Integer suppressVisually,
+            ArrayList<Notification.Action> systemSmartActions,
+            ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
+            boolean isConversation, int proposedImportance) {
+        mPosition = position;
+        mVisibility = visibility;
+        mShowBadge = showBadge;
+        mAllowBubble = allowBubble;
+        mIsBubble = isBubble;
+        mChannel = channel;
+        mGroupKey = groupKey;
+        mOverridePeople = overridePeople;
+        mSnoozeCriteria = snoozeCriteria;
+        mUserSentiment = userSentiment;
+        mSuppressVisually = suppressVisually;
+        mSystemSmartActions = systemSmartActions;
+        mSmartReplies = smartReplies;
+        mImportance = importance;
+        mRankingScore = rankingScore;
+        mIsConversation = isConversation;
+        mProposedImportance = proposedImportance;
+    }
+
+    // Returns whether the provided NotificationRecord differs from the cached data in any way.
+    // Should be guarded by mNotificationLock; not annotated here as this class is static.
+    boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
+        return mPosition != newPosition
+                || mVisibility != r.getPackageVisibilityOverride()
+                || mShowBadge != r.canShowBadge()
+                || mAllowBubble != r.canBubble()
+                || mIsBubble != r.getNotification().isBubbleNotification()
+                || !Objects.equals(mChannel, r.getChannel())
+                || !Objects.equals(mGroupKey, r.getGroupKey())
+                || !Objects.equals(mOverridePeople, r.getPeopleOverride())
+                || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
+                || !Objects.equals(mUserSentiment, r.getUserSentiment())
+                || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
+                || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
+                || !Objects.equals(mSmartReplies, r.getSmartReplies())
+                || mImportance != r.getImportance()
+                || mProposedImportance != r.getProposedImportance();
+    }
+
+    // Returns whether the NotificationRecord has a change from this data for which we should
+    // log an update. This method specifically targets fields that may be changed via
+    // adjustments from the assistant.
+    //
+    // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
+    // and NotificationRecord.applyAdjustments.
+    //
+    // Should be guarded by mNotificationLock; not annotated here as this class is static.
+    boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
+        return mPosition != newPosition
+                || !Objects.equals(mChannel, r.getChannel())
+                || !Objects.equals(mGroupKey, r.getGroupKey())
+                || !Objects.equals(mOverridePeople, r.getPeopleOverride())
+                || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
+                || !Objects.equals(mUserSentiment, r.getUserSentiment())
+                || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
+                || !Objects.equals(mSmartReplies, r.getSmartReplies())
+                || mImportance != r.getImportance()
+                || !r.rankingScoreMatches(mRankingScore)
+                || mIsConversation != r.isConversation()
+                || mProposedImportance != r.getProposedImportance();
+    }
+}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 78dad12..7ca9ac7 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -108,7 +108,7 @@
     @VisibleForTesting
     static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000;
     @VisibleForTesting
-    static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 50000;
+    static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000;
 
     private static final int NOTIFICATION_PREFERENCES_PULL_LIMIT = 1000;
     private static final int NOTIFICATION_CHANNEL_PULL_LIMIT = 2000;
@@ -1007,6 +1007,7 @@
                     channel.setAllowBubbles(existing != null
                             ? existing.getAllowBubbles()
                             : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
+                    channel.setImportantConversation(false);
                 }
                 clearLockedFieldsLocked(channel);
 
diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java
index c0bc474..35b94e7 100644
--- a/services/core/java/com/android/server/notification/ZenLog.java
+++ b/services/core/java/com/android/server/notification/ZenLog.java
@@ -68,20 +68,23 @@
     private static final int TYPE_MATCHES_CALL_FILTER = 18;
     private static final int TYPE_RECORD_CALLER = 19;
     private static final int TYPE_CHECK_REPEAT_CALLER = 20;
+    private static final int TYPE_ALERT_ON_UPDATED_INTERCEPT = 21;
 
     private static int sNext;
     private static int sSize;
 
     public static void traceIntercepted(NotificationRecord record, String reason) {
-        if (record != null && record.isIntercepted()) return;  // already logged
         append(TYPE_INTERCEPTED, record.getKey() + "," + reason);
     }
 
     public static void traceNotIntercepted(NotificationRecord record, String reason) {
-        if (record != null && record.isUpdate) return;  // already logged
         append(TYPE_NOT_INTERCEPTED, record.getKey() + "," + reason);
     }
 
+    public static void traceAlertOnUpdatedIntercept(NotificationRecord record) {
+        append(TYPE_ALERT_ON_UPDATED_INTERCEPT, record.getKey());
+    }
+
     public static void traceSetRingerModeExternal(int ringerModeOld, int ringerModeNew,
             String caller, int ringerModeInternalIn, int ringerModeInternalOut) {
         append(TYPE_SET_RINGER_MODE_EXTERNAL, caller + ",e:" +
@@ -219,6 +222,7 @@
             case TYPE_MATCHES_CALL_FILTER: return "matches_call_filter";
             case TYPE_RECORD_CALLER: return "record_caller";
             case TYPE_CHECK_REPEAT_CALLER: return "check_repeat_caller";
+            case TYPE_ALERT_ON_UPDATED_INTERCEPT: return "alert_on_updated_intercept";
             default: return "unknown";
         }
     }
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index db0ce2e..5b7b0c1 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -155,85 +155,85 @@
 
         if (isCritical(record)) {
             // Zen mode is ignored for critical notifications.
-            ZenLog.traceNotIntercepted(record, "criticalNotification");
+            maybeLogInterceptDecision(record, false, "criticalNotification");
             return false;
         }
         // Make an exception to policy for the notification saying that policy has changed
         if (NotificationManager.Policy.areAllVisualEffectsSuppressed(policy.suppressedVisualEffects)
                 && "android".equals(record.getSbn().getPackageName())
                 && SystemMessageProto.SystemMessage.NOTE_ZEN_UPGRADE == record.getSbn().getId()) {
-            ZenLog.traceNotIntercepted(record, "systemDndChangedNotification");
+            maybeLogInterceptDecision(record, false, "systemDndChangedNotification");
             return false;
         }
         switch (zen) {
             case Global.ZEN_MODE_NO_INTERRUPTIONS:
                 // #notevenalarms
-                ZenLog.traceIntercepted(record, "none");
+                maybeLogInterceptDecision(record, true, "none");
                 return true;
             case Global.ZEN_MODE_ALARMS:
                 if (isAlarm(record)) {
                     // Alarms only
-                    ZenLog.traceNotIntercepted(record, "alarm");
+                    maybeLogInterceptDecision(record, false, "alarm");
                     return false;
                 }
-                ZenLog.traceIntercepted(record, "alarmsOnly");
+                maybeLogInterceptDecision(record, true, "alarmsOnly");
                 return true;
             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
                 // allow user-prioritized packages through in priority mode
                 if (record.getPackagePriority() == Notification.PRIORITY_MAX) {
-                    ZenLog.traceNotIntercepted(record, "priorityApp");
+                    maybeLogInterceptDecision(record, false, "priorityApp");
                     return false;
                 }
 
                 if (isAlarm(record)) {
                     if (!policy.allowAlarms()) {
-                        ZenLog.traceIntercepted(record, "!allowAlarms");
+                        maybeLogInterceptDecision(record, true, "!allowAlarms");
                         return true;
                     }
-                    ZenLog.traceNotIntercepted(record, "allowedAlarm");
+                    maybeLogInterceptDecision(record, false, "allowedAlarm");
                     return false;
                 }
                 if (isEvent(record)) {
                     if (!policy.allowEvents()) {
-                        ZenLog.traceIntercepted(record, "!allowEvents");
+                        maybeLogInterceptDecision(record, true, "!allowEvents");
                         return true;
                     }
-                    ZenLog.traceNotIntercepted(record, "allowedEvent");
+                    maybeLogInterceptDecision(record, false, "allowedEvent");
                     return false;
                 }
                 if (isReminder(record)) {
                     if (!policy.allowReminders()) {
-                        ZenLog.traceIntercepted(record, "!allowReminders");
+                        maybeLogInterceptDecision(record, true, "!allowReminders");
                         return true;
                     }
-                    ZenLog.traceNotIntercepted(record, "allowedReminder");
+                    maybeLogInterceptDecision(record, false, "allowedReminder");
                     return false;
                 }
                 if (isMedia(record)) {
                     if (!policy.allowMedia()) {
-                        ZenLog.traceIntercepted(record, "!allowMedia");
+                        maybeLogInterceptDecision(record, true, "!allowMedia");
                         return true;
                     }
-                    ZenLog.traceNotIntercepted(record, "allowedMedia");
+                    maybeLogInterceptDecision(record, false, "allowedMedia");
                     return false;
                 }
                 if (isSystem(record)) {
                     if (!policy.allowSystem()) {
-                        ZenLog.traceIntercepted(record, "!allowSystem");
+                        maybeLogInterceptDecision(record, true, "!allowSystem");
                         return true;
                     }
-                    ZenLog.traceNotIntercepted(record, "allowedSystem");
+                    maybeLogInterceptDecision(record, false, "allowedSystem");
                     return false;
                 }
                 if (isConversation(record)) {
                     if (policy.allowConversations()) {
                         if (policy.priorityConversationSenders == CONVERSATION_SENDERS_ANYONE) {
-                            ZenLog.traceNotIntercepted(record, "conversationAnyone");
+                            maybeLogInterceptDecision(record, false, "conversationAnyone");
                             return false;
                         } else if (policy.priorityConversationSenders
                                 == NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT
                                 && record.getChannel().isImportantConversation()) {
-                            ZenLog.traceNotIntercepted(record, "conversationMatches");
+                            maybeLogInterceptDecision(record, false, "conversationMatches");
                             return false;
                         }
                     }
@@ -244,31 +244,59 @@
                     if (policy.allowRepeatCallers()
                             && REPEAT_CALLERS.isRepeat(
                                     mContext, extras(record), record.getPhoneNumbers())) {
-                        ZenLog.traceNotIntercepted(record, "repeatCaller");
+                        maybeLogInterceptDecision(record, false, "repeatCaller");
                         return false;
                     }
                     if (!policy.allowCalls()) {
-                        ZenLog.traceIntercepted(record, "!allowCalls");
+                        maybeLogInterceptDecision(record, true, "!allowCalls");
                         return true;
                     }
                     return shouldInterceptAudience(policy.allowCallsFrom(), record);
                 }
                 if (isMessage(record)) {
                     if (!policy.allowMessages()) {
-                        ZenLog.traceIntercepted(record, "!allowMessages");
+                        maybeLogInterceptDecision(record, true, "!allowMessages");
                         return true;
                     }
                     return shouldInterceptAudience(policy.allowMessagesFrom(), record);
                 }
 
-                ZenLog.traceIntercepted(record, "!priority");
+                maybeLogInterceptDecision(record, true, "!priority");
                 return true;
             default:
-                ZenLog.traceNotIntercepted(record, "unknownZenMode");
+                maybeLogInterceptDecision(record, false, "unknownZenMode");
                 return false;
         }
     }
 
+    // Consider logging the decision of shouldIntercept for the given record.
+    // This will log the outcome if one of the following is true:
+    //   - it's the first time the intercept decision is set for the record
+    //   - OR it's not the first time, but the intercept decision changed
+    private static void maybeLogInterceptDecision(NotificationRecord record, boolean intercept,
+            String reason) {
+        boolean interceptBefore = record.isIntercepted();
+        if (record.hasInterceptBeenSet() && (interceptBefore == intercept)) {
+            // this record has already been evaluated for whether it should be intercepted, and
+            // the decision has not changed.
+            return;
+        }
+
+        // add a note to the reason indicating whether it's new or updated
+        String annotatedReason = reason;
+        if (!record.hasInterceptBeenSet()) {
+            annotatedReason = "new:" + reason;
+        } else if (interceptBefore != intercept) {
+            annotatedReason = "updated:" + reason;
+        }
+
+        if (intercept) {
+            ZenLog.traceIntercepted(record, annotatedReason);
+        } else {
+            ZenLog.traceNotIntercepted(record, annotatedReason);
+        }
+    }
+
     /**
      * Check if the notification is too critical to be suppressed.
      *
@@ -285,10 +313,10 @@
     private static boolean shouldInterceptAudience(int source, NotificationRecord record) {
         float affinity = record.getContactAffinity();
         if (!audienceMatches(source, affinity)) {
-            ZenLog.traceIntercepted(record, "!audienceMatches,affinity=" + affinity);
+            maybeLogInterceptDecision(record, true, "!audienceMatches,affinity=" + affinity);
             return true;
         }
-        ZenLog.traceNotIntercepted(record, "affinity=" + affinity);
+        maybeLogInterceptDecision(record, false, "affinity=" + affinity);
         return false;
     }
 
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 3816b07..b4a872c 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -94,6 +94,7 @@
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.ApplicationPackageManager;
+import android.app.BroadcastOptions;
 import android.app.backup.IBackupManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -641,7 +642,10 @@
         fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
                 PackageManager.installStatusToPublicStatus(returnCode));
         try {
-            target.sendIntent(context, 0, fillIn, null, null);
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+            target.sendIntent(context, 0, fillIn, null /* onFinished*/, null /* handler */,
+                    null /* requiredPermission */, options.toBundle());
         } catch (IntentSender.SendIntentException ignored) {
         }
     }
@@ -2425,10 +2429,10 @@
             // will be null whereas dataOwnerPkg will contain information about the package
             // which was uninstalled while keeping its data.
             AndroidPackage dataOwnerPkg = mPm.mPackages.get(packageName);
+            PackageSetting dataOwnerPs = mPm.mSettings.getPackageLPr(packageName);
             if (dataOwnerPkg  == null) {
-                PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
-                if (ps != null) {
-                    dataOwnerPkg = ps.getPkg();
+                if (dataOwnerPs != null) {
+                    dataOwnerPkg = dataOwnerPs.getPkg();
                 }
             }
 
@@ -2456,6 +2460,7 @@
             if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
                 if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
                         dataOwnerPkg.isDebuggable())) {
+                    // Downgrade is not permitted; a lower version of the app will not be allowed
                     try {
                         PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
                     } catch (PackageManagerException e) {
@@ -2464,6 +2469,28 @@
                         return Pair.create(
                                 PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
                     }
+                } else if (dataOwnerPs.isSystem()) {
+                    // Downgrade is permitted, but system apps can't be downgraded below
+                    // the version preloaded onto the system image
+                    final PackageSetting disabledPs = mPm.mSettings.getDisabledSystemPkgLPr(
+                            dataOwnerPs);
+                    if (disabledPs != null) {
+                        dataOwnerPkg = disabledPs.getPkg();
+                    }
+                    if (!Build.IS_DEBUGGABLE && !dataOwnerPkg.isDebuggable()) {
+                        // Only restrict non-debuggable builds and non-debuggable version of the app
+                        try {
+                            PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
+                        } catch (PackageManagerException e) {
+                            String errorMsg =
+                                    "System app: " + packageName + " cannot be downgraded to"
+                                            + " older than its preloaded version on the system"
+                                            + " image. " + e.getMessage();
+                            Slog.w(TAG, errorMsg);
+                            return Pair.create(
+                                    PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
+                        }
+                    }
                 }
             }
         }
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 5e0fc3b..8b04c3a 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -22,6 +22,7 @@
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.content.pm.LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS;
 import static android.content.pm.LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS;
 import static android.content.pm.LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS;
@@ -1114,12 +1115,19 @@
 
             // Flag for bubble
             ActivityOptions options = ActivityOptions.fromBundle(startActivityOptions);
-            if (options != null && options.isApplyActivityFlagsForBubbles()) {
-                // Flag for bubble to make behaviour match documentLaunchMode=always.
-                intents[0].addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
-                intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+            if (options != null) {
+                if (options.isApplyActivityFlagsForBubbles()) {
+                    // Flag for bubble to make behaviour match documentLaunchMode=always.
+                    intents[0].addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+                    intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                }
+                if (options.isApplyMultipleTaskFlagForShortcut()) {
+                    intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                }
+                if (options.isApplyNoUserActionFlagForShortcut()) {
+                    intents[0].addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
+                }
             }
-
             intents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             intents[0].setSourceBounds(sourceBounds);
 
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index bb23d89d..02cf433 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -27,6 +27,7 @@
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PackageDeleteObserver;
@@ -1360,7 +1361,10 @@
                     PackageInstaller.STATUS_PENDING_USER_ACTION);
             fillIn.putExtra(Intent.EXTRA_INTENT, intent);
             try {
-                mTarget.sendIntent(mContext, 0, fillIn, null, null);
+                final BroadcastOptions options = BroadcastOptions.makeBasic();
+                options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+                mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/,
+                        null /* handler */, null /* requiredPermission */, options.toBundle());
             } catch (SendIntentException ignored) {
             }
         }
@@ -1385,7 +1389,10 @@
                     PackageManager.deleteStatusToString(returnCode, msg));
             fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
             try {
-                mTarget.sendIntent(mContext, 0, fillIn, null, null);
+                final BroadcastOptions options = BroadcastOptions.makeBasic();
+                options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+                mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/,
+                        null /* handler */, null /* requiredPermission */, options.toBundle());
             } catch (SendIntentException ignored) {
             }
         }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 7c2e3ea..1823de8 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -54,6 +54,7 @@
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
 import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.admin.DevicePolicyEventLogger;
@@ -4274,7 +4275,10 @@
         fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_USER_ACTION);
         fillIn.putExtra(Intent.EXTRA_INTENT, intent);
         try {
-            target.sendIntent(context, 0, fillIn, null, null);
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+            target.sendIntent(context, 0, fillIn, null /* onFinished */,
+                    null /* handler */, null /* requiredPermission */, options.toBundle());
         } catch (IntentSender.SendIntentException ignored) {
         }
     }
@@ -4315,7 +4319,10 @@
             }
         }
         try {
-            target.sendIntent(context, 0, fillIn, null, null);
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+            target.sendIntent(context, 0, fillIn, null /* onFinished */,
+                    null /* handler */, null /* requiredPermission */, options.toBundle());
         } catch (IntentSender.SendIntentException ignored) {
         }
     }
@@ -4349,7 +4356,10 @@
             intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, "Staging Image Not Ready");
         }
         try {
-            target.sendIntent(context, 0, intent, null, null);
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+            target.sendIntent(context, 0, intent, null /* onFinished */,
+                    null /* handler */, null /* requiredPermission */, options.toBundle());
         } catch (IntentSender.SendIntentException ignored) {
         }
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 8d2714c..4786037 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -53,6 +53,7 @@
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.ApplicationPackageManager;
+import android.app.BroadcastOptions;
 import android.app.IActivityManager;
 import android.app.admin.IDevicePolicyManager;
 import android.app.admin.SecurityLog;
@@ -4798,7 +4799,11 @@
                 }
                 if (pi != null) {
                     try {
-                        pi.sendIntent(null, success ? 1 : 0, null, null, null);
+                        final BroadcastOptions options = BroadcastOptions.makeBasic();
+                        options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+                        pi.sendIntent(null, success ? 1 : 0, null /* intent */,
+                                null /* onFinished*/, null /* handler */,
+                                null /* requiredPermission */, options.toBundle());
                     } catch (SendIntentException e) {
                         Slog.w(TAG, e);
                     }
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 890c891..9212331 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -430,6 +430,7 @@
             @NonNull List<ShortcutInfo> changedShortcuts) {
         Preconditions.checkArgument(newShortcut.isEnabled(),
                 "pushDynamicShortcuts() cannot publish disabled shortcuts");
+        ensureShortcutCountBeforePush();
 
         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
 
@@ -437,7 +438,7 @@
         final ShortcutInfo oldShortcut = findShortcutById(newShortcut.getId());
         boolean deleted = false;
 
-        if (oldShortcut == null) {
+        if (oldShortcut == null || !oldShortcut.isDynamic()) {
             final ShortcutService service = mShortcutUser.mService;
             final int maxShortcuts = service.getMaxActivityShortcuts();
 
@@ -446,18 +447,12 @@
             final ArrayList<ShortcutInfo> activityShortcuts = all.get(newShortcut.getActivity());
 
             if (activityShortcuts != null && activityShortcuts.size() > maxShortcuts) {
-                Slog.e(TAG, "Error pushing shortcut. There are already "
-                        + activityShortcuts.size() + " shortcuts, exceeding the " + maxShortcuts
-                        + " shortcuts limit when pushing the new shortcut " + newShortcut
-                        + ". Id of shortcuts currently available in system memory are "
-                        + activityShortcuts.stream().map(ShortcutInfo::getId)
-                        .collect(Collectors.joining(",", "[", "]")));
-                // TODO: This should not have happened. If it does, identify the root cause where
-                //  possible, otherwise bail-out early to prevent memory issue.
+                // Root cause was discovered in b/233155034, so this should not be happening.
+                service.wtf("Error pushing shortcut. There are already "
+                        + activityShortcuts.size() + " shortcuts.");
             }
             if (activityShortcuts != null && activityShortcuts.size() == maxShortcuts) {
                 // Max has reached. Delete the shortcut with lowest rank.
-
                 // Sort by isManifestShortcut() and getRank().
                 Collections.sort(activityShortcuts, mShortcutTypeAndRankComparator);
 
@@ -473,7 +468,8 @@
                 deleted = deleteDynamicWithId(shortcut.getId(), /* ignoreInvisible =*/ true,
                         /*ignorePersistedShortcuts=*/ true) != null;
             }
-        } else {
+        }
+        if (oldShortcut != null) {
             // It's an update case.
             // Make sure the target is updatable. (i.e. should be mutable.)
             oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
@@ -505,6 +501,32 @@
         return deleted;
     }
 
+    private void ensureShortcutCountBeforePush() {
+        final ShortcutService service = mShortcutUser.mService;
+        // Ensure the total number of shortcuts doesn't exceed the hard limit per app.
+        final int maxShortcutPerApp = service.getMaxAppShortcuts();
+        synchronized (mLock) {
+            final List<ShortcutInfo> appShortcuts = mShortcuts.values().stream().filter(si ->
+                    !si.isPinned()).collect(Collectors.toList());
+            if (appShortcuts.size() >= maxShortcutPerApp) {
+                // Max has reached. Removes shortcuts until they fall within the hard cap.
+                // Sort by isManifestShortcut(), isDynamic() and getLastChangedTimestamp().
+                Collections.sort(appShortcuts, mShortcutTypeRankAndTimeComparator);
+
+                while (appShortcuts.size() >= maxShortcutPerApp) {
+                    final ShortcutInfo shortcut = appShortcuts.remove(appShortcuts.size() - 1);
+                    if (shortcut.isDeclaredInManifest()) {
+                        // All shortcuts are manifest shortcuts and cannot be removed.
+                        throw new IllegalArgumentException(getPackageName() + " has published "
+                                + appShortcuts.size() + " manifest shortcuts across different"
+                                + " activities.");
+                    }
+                    forceDeleteShortcutInner(shortcut.getId());
+                }
+            }
+        }
+    }
+
     /**
      * Remove all shortcuts that aren't pinned, cached nor dynamic.
      *
@@ -885,7 +907,12 @@
      * available ShareTarget definitions in this package.
      */
     public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
-            @NonNull IntentFilter filter) {
+            @NonNull final IntentFilter filter) {
+        return getMatchingShareTargets(filter, null);
+    }
+
+    List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
+            @NonNull final IntentFilter filter, @Nullable final String pkgName) {
         synchronized (mLock) {
             final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
             for (int i = 0; i < mShareTargets.size(); i++) {
@@ -909,8 +936,7 @@
             // included in the result
             findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
                     ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION,
-                    mShortcutUser.mService.mContext.getPackageName(),
-                    0, /*getPinnedByAnyLauncher=*/ false);
+                    pkgName, 0, /*getPinnedByAnyLauncher=*/ false);
 
             final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
             for (int i = 0; i < shortcuts.size(); i++) {
@@ -1108,7 +1134,7 @@
 
         // Now prepare to publish manifest shortcuts.
         List<ShortcutInfo> newManifestShortcutList = null;
-        final int shareTargetSize;
+        int shareTargetSize = 0;
         synchronized (mLock) {
             try {
                 shareTargetSize = mShareTargets.size();
@@ -1367,6 +1393,61 @@
     };
 
     /**
+     * To sort by isManifestShortcut(), isDynamic(), getRank() and
+     * getLastChangedTimestamp(). i.e. manifest shortcuts come before non-manifest shortcuts,
+     * dynamic shortcuts come before floating shortcuts, then sort by last changed timestamp.
+     *
+     * This is used to decide which shortcuts to remove when the total number of shortcuts retained
+     * for the app exceeds the limit defined in {@link ShortcutService#getMaxAppShortcuts()}.
+     *
+     * (Note the number of manifest shortcuts is always <= the max number, because if there are
+     * more, ShortcutParser would ignore the rest.)
+     */
+    final Comparator<ShortcutInfo> mShortcutTypeRankAndTimeComparator = (ShortcutInfo a,
+            ShortcutInfo b) -> {
+        if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) {
+            return -1;
+        }
+        if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) {
+            return 1;
+        }
+        if (a.isDynamic() && b.isDynamic()) {
+            return Integer.compare(a.getRank(), b.getRank());
+        }
+        if (a.isDynamic()) {
+            return -1;
+        }
+        if (b.isDynamic()) {
+            return 1;
+        }
+        if (a.isCached() && b.isCached()) {
+            // if both shortcuts are cached, prioritize shortcuts cached by people tile,
+            if (a.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)
+                    && !b.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)) {
+                return -1;
+            } else if (!a.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)
+                    && b.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)) {
+                return 1;
+            }
+            // followed by bubbles.
+            if (a.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)
+                    && !b.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)) {
+                return -1;
+            } else if (!a.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)
+                    && b.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)) {
+                return 1;
+            }
+        }
+        if (a.isCached()) {
+            return -1;
+        }
+        if (b.isCached()) {
+            return 1;
+        }
+        return Long.compare(b.getLastChangedTimestamp(), a.getLastChangedTimestamp());
+    };
+
+    /**
      * Build a list of shortcuts for each target activity and return as a map. The result won't
      * contain "floating" shortcuts because they don't belong on any activities.
      */
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 0b20683..7fc46fd 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,6 +181,9 @@
     static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
 
     @VisibleForTesting
+    static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 100;
+
+    @VisibleForTesting
     static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
 
     @VisibleForTesting
@@ -257,6 +260,11 @@
         String KEY_MAX_SHORTCUTS = "max_shortcuts";
 
         /**
+         * Key name for the max shortcuts can be retained in system ram per app. (int)
+         */
+        String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
+
+        /**
          * Key name for icon compression quality, 0-100.
          */
         String KEY_ICON_QUALITY = "icon_quality";
@@ -329,11 +337,16 @@
             new SparseArray<>();
 
     /**
-     * Max number of dynamic + manifest shortcuts that each application can have at a time.
+     * Max number of dynamic + manifest shortcuts that each activity can have at a time.
      */
     private int mMaxShortcuts;
 
     /**
+     * Max number of shortcuts that can exists in system ram for each application.
+     */
+    private int mMaxShortcutsPerApp;
+
+    /**
      * Max number of updating API calls that each application can make during the interval.
      */
     int mMaxUpdatesPerInterval;
@@ -807,6 +820,9 @@
         mMaxShortcuts = Math.max(0, (int) parser.getLong(
                 ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
 
+        mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
+                ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
+
         final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
                 ? (int) parser.getLong(
                 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1759,6 +1775,13 @@
     }
 
     /**
+     * Return the max number of shortcuts can be retaiend in system ram for each application.
+     */
+    int getMaxAppShortcuts() {
+        return mMaxShortcutsPerApp;
+    }
+
+    /**
      * - Sends a notification to LauncherApps
      * - Write to file
      */
@@ -2512,11 +2535,17 @@
         }
         enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APP_PREDICTIONS,
                 "getShareTargets");
+        final ComponentName chooser = injectChooserActivity();
+        final String pkg = (chooser != null
+                && mPackageManagerInternal.getComponentEnabledSetting(chooser,
+                injectBinderCallingUid(), userId) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+                ? chooser.getPackageName() : mContext.getPackageName();
         synchronized (mLock) {
             throwIfUserLockedL(userId);
             final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>();
             final ShortcutUser user = getUserShortcutsLocked(userId);
-            user.forAllPackages(p -> shortcutInfoList.addAll(p.getMatchingShareTargets(filter)));
+            user.forAllPackages(p -> shortcutInfoList.addAll(
+                    p.getMatchingShareTargets(filter, pkg)));
             return new ParceledListSlice<>(shortcutInfoList);
         }
     }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 0a465e9..89a920a 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -91,11 +91,11 @@
 import android.service.gatekeeper.IGateKeeperService;
 import android.service.voice.VoiceInteractionManagerInternal;
 import android.stats.devicepolicy.DevicePolicyEnums;
+import android.telecom.TelecomManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
-import android.util.EventLog;
 import android.util.IndentingPrintWriter;
 import android.util.IntArray;
 import android.util.Slog;
@@ -1764,6 +1764,63 @@
         }
     }
 
+    /**
+     * Returns whether switching users is currently allowed for the provided user.
+     * <p>
+     * Switching users is not allowed in the following cases:
+     * <li>the user is in a phone call</li>
+     * <li>{@link UserManager#DISALLOW_USER_SWITCH} is set</li>
+     * <li>system user hasn't been unlocked yet</li>
+     *
+     * @return A {@link UserManager.UserSwitchabilityResult} flag indicating if the user is
+     * switchable.
+     */
+    public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) {
+        checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserSwitchability");
+
+        final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+        t.traceBegin("getUserSwitchability-" + userId);
+
+        int flags = UserManager.SWITCHABILITY_STATUS_OK;
+
+        t.traceBegin("TM.isInCall");
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+            if (telecomManager != null && telecomManager.isInCall()) {
+                flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        t.traceEnd();
+
+        t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
+        if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
+            flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
+        }
+        t.traceEnd();
+
+        // System User is always unlocked in Headless System User Mode, so ignore this flag
+        if (!UserManager.isHeadlessSystemUserMode()) {
+            t.traceBegin("getInt-ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED");
+            final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
+                    mContext.getContentResolver(),
+                    Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
+            t.traceEnd();
+            t.traceBegin("isUserUnlocked-USER_SYSTEM");
+            final boolean systemUserUnlocked = mLocalService.isUserUnlocked(UserHandle.USER_SYSTEM);
+            t.traceEnd();
+
+            if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
+                flags |= UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
+            }
+        }
+        t.traceEnd();
+
+        return flags;
+    }
+
     @Override
     public boolean isUserSwitcherEnabled(@UserIdInt int mUserId) {
         boolean multiUserSettingOn = Settings.Global.getInt(mContext.getContentResolver(),
@@ -4974,13 +5031,6 @@
     public void setApplicationRestrictions(String packageName, Bundle restrictions,
             @UserIdInt int userId) {
         checkSystemOrRoot("set application restrictions");
-        String validationResult = validateName(packageName);
-        if (validationResult != null) {
-            if (packageName.contains("../")) {
-                EventLog.writeEvent(0x534e4554, "239701237", -1, "");
-            }
-            throw new IllegalArgumentException("Invalid package name: " + validationResult);
-        }
         if (restrictions != null) {
             restrictions.setDefusable(true);
         }
@@ -5007,39 +5057,6 @@
         mContext.sendBroadcastAsUser(changeIntent, UserHandle.of(userId));
     }
 
-    /**
-     * Check if the given name is valid.
-     *
-     * Note: the logic is taken from FrameworkParsingPackageUtils in master, edited to remove
-     * unnecessary parts. Copied here for a security fix.
-     *
-     * @param name The name to check.
-     * @return null if it's valid, error message if not
-     */
-    @VisibleForTesting
-    static String validateName(String name) {
-        final int n = name.length();
-        boolean front = true;
-        for (int i = 0; i < n; i++) {
-            final char c = name.charAt(i);
-            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
-                front = false;
-                continue;
-            }
-            if (!front) {
-                if ((c >= '0' && c <= '9') || c == '_') {
-                    continue;
-                }
-                if (c == '.') {
-                    front = true;
-                    continue;
-                }
-            }
-            return "bad character '" + c + "'";
-        }
-        return null;
-    }
-
     private int getUidForPackage(String packageName) {
         final long ident = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 554e269..9ed5aa7 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -3268,7 +3268,7 @@
         if (Objects.equals(packageName, PLATFORM_PACKAGE_NAME)) {
             return true;
         }
-        if (!pkg.isPrivileged()) {
+        if (!(pkg.isSystem() && pkg.isPrivileged())) {
             return true;
         }
         if (!mPrivilegedPermissionAllowlistSourcePackageNames
@@ -4215,7 +4215,6 @@
         }
         boolean changed = false;
 
-        Set<Permission> needsUpdate = null;
         synchronized (mLock) {
             final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator();
             while (it.hasNext()) {
@@ -4234,26 +4233,6 @@
                             + " that used to be declared by " + bp.getPackageName());
                     it.remove();
                 }
-                if (needsUpdate == null) {
-                    needsUpdate = new ArraySet<>();
-                }
-                needsUpdate.add(bp);
-            }
-        }
-        if (needsUpdate != null) {
-            for (final Permission bp : needsUpdate) {
-                final AndroidPackage sourcePkg =
-                        mPackageManagerInt.getPackage(bp.getPackageName());
-                final PackageStateInternal sourcePs =
-                        mPackageManagerInt.getPackageStateInternal(bp.getPackageName());
-                synchronized (mLock) {
-                    if (sourcePkg != null && sourcePs != null) {
-                        continue;
-                    }
-                    Slog.w(TAG, "Removing dangling permission tree: " + bp.getName()
-                            + " from package " + bp.getPackageName());
-                    mRegistry.removePermission(bp.getName());
-                }
             }
         }
         return changed;
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index 7ce7f7e..810fa5f 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -247,6 +247,9 @@
     private static final String MAX_NUM_COMPONENTS_ERR_MSG =
             "Total number of components has exceeded the maximum number: " + MAX_NUM_COMPONENTS;
 
+    /** The maximum permission name length. */
+    private static final int MAX_PERMISSION_NAME_LENGTH = 512;
+
     @IntDef(flag = true, prefix = { "PARSE_" }, value = {
             PARSE_CHATTY,
             PARSE_COLLECT_CERTIFICATES,
@@ -1275,6 +1278,11 @@
             // that may change.
             String name = sa.getNonResourceString(
                     R.styleable.AndroidManifestUsesPermission_name);
+            if (TextUtils.length(name) > MAX_PERMISSION_NAME_LENGTH) {
+                return input.error(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+                        "The name in the <uses-permission> is greater than "
+                                + MAX_PERMISSION_NAME_LENGTH);
+            }
 
             int maxSdkVersion = 0;
             TypedValue val = sa.peekValue(
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index c8a11a5..e877d56 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -322,6 +322,10 @@
     static final int TRIPLE_PRESS_PRIMARY_NOTHING = 0;
     static final int TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY = 1;
 
+    // Must match: config_searchKeyBehavior in config.xml
+    static final int SEARCH_BEHAVIOR_DEFAULT_SEARCH = 0;
+    static final int SEARCH_BEHAVIOR_TARGET_ACTIVITY = 1;
+
     static public final String SYSTEM_DIALOG_REASON_KEY = "reason";
     static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
     static public final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
@@ -330,6 +334,8 @@
     static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
     static public final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
 
+    public static final String TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD = "waitForAllWindowsDrawn";
+
     private static final String TALKBACK_LABEL = "TalkBack";
 
     private static final int POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS = 800;
@@ -525,6 +531,8 @@
     boolean mWakeOnAssistKeyPress;
     boolean mWakeOnBackKeyPress;
     long mWakeUpToLastStateTimeout;
+    int mSearchKeyBehavior;
+    ComponentName mSearchKeyTargetActivity;
 
     private boolean mHandleVolumeKeysInWM;
 
@@ -568,6 +576,9 @@
     // What we do when the user double-taps on home
     private int mDoubleTapOnHomeBehavior;
 
+    // Whether to lock the device after the next app transition has finished.
+    private boolean mLockAfterAppTransitionFinished;
+
     // Allowed theater mode wake actions
     private boolean mAllowTheaterModeWakeFromKey;
     private boolean mAllowTheaterModeWakeFromPowerKey;
@@ -711,7 +722,7 @@
                     handleRingerChordGesture();
                     break;
                 case MSG_SCREENSHOT_CHORD:
-                    handleScreenShot(msg.arg1, msg.arg2);
+                    handleScreenShot(msg.arg1);
                     break;
             }
         }
@@ -974,12 +985,7 @@
             powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
         } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
             Slog.d(TAG, "No behavior defined for power press count " + count);
-        } else if (count == 1 && interactive) {
-            if (beganFromNonInteractive) {
-                // The screen off case, where we might want to start dreaming on power button press.
-                attemptToDreamFromShortPowerButtonPress(false, () -> {});
-                return;
-            }
+        } else if (count == 1 && interactive && !beganFromNonInteractive) {
             if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) {
                 Slog.i(TAG, "Suppressing power key because the user is interacting with the "
                         + "fingerprint sensor");
@@ -1057,11 +1063,10 @@
             return;
         }
 
-        // Make sure the device locks. Unfortunately, this has the side-effect of briefly revealing
-        // the lock screen before the dream appears. Note that locking is a side-effect of the no
-        // dream action that is executed if we early return above.
-        // TODO(b/261662912): Find a better way to lock the device that doesn't result in jank.
-        lockNow(null);
+        synchronized (mLock) {
+            // Lock the device after the dream transition has finished.
+            mLockAfterAppTransitionFinished = true;
+        }
 
         dreamManagerInternal.requestDream();
     }
@@ -1502,9 +1507,9 @@
                 || mShortPressOnStemPrimaryBehavior != SHORT_PRESS_PRIMARY_NOTHING;
     }
 
-    private void interceptScreenshotChord(int type, int source, long pressDelay) {
+    private void interceptScreenshotChord(int source, long pressDelay) {
         mHandler.removeMessages(MSG_SCREENSHOT_CHORD);
-        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCREENSHOT_CHORD, type, source),
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCREENSHOT_CHORD, source),
                 pressDelay);
     }
 
@@ -1574,9 +1579,8 @@
         }
     };
 
-    private void handleScreenShot(@WindowManager.ScreenshotType int type,
-            @WindowManager.ScreenshotSource int source) {
-        mDefaultDisplayPolicy.takeScreenshot(type, source);
+    private void handleScreenShot(@WindowManager.ScreenshotSource int source) {
+        mDefaultDisplayPolicy.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, source);
     }
 
     @Override
@@ -2078,6 +2082,12 @@
         mWakeUpToLastStateTimeout = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_wakeUpToLastStateTimeoutMillis);
 
+        mSearchKeyBehavior = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_searchKeyBehavior);
+
+        mSearchKeyTargetActivity = ComponentName.unflattenFromString(
+            mContext.getResources().getString(
+                com.android.internal.R.string.config_searchKeyTargetActivity));
         readConfigurationDependentBehaviors();
 
         mDisplayFoldController = DisplayFoldController.create(context, DEFAULT_DISPLAY);
@@ -2142,6 +2152,22 @@
                 handleStartTransitionForKeyguardLw(
                         keyguardGoingAway, false /* keyguardOccludingStarted */,
                         0 /* duration */);
+
+                synchronized (mLock) {
+                    mLockAfterAppTransitionFinished = false;
+                }
+            }
+
+            @Override
+            public void onAppTransitionFinishedLocked(IBinder token) {
+                synchronized (mLock) {
+                    if (!mLockAfterAppTransitionFinished) {
+                        return;
+                    }
+                    mLockAfterAppTransitionFinished = false;
+                }
+
+                lockNow(null);
             }
         });
 
@@ -2173,7 +2199,7 @@
                         @Override
                         void execute() {
                             mPowerKeyHandled = true;
-                            interceptScreenshotChord(TAKE_SCREENSHOT_FULLSCREEN,
+                            interceptScreenshotChord(
                                     SCREENSHOT_KEY_CHORD, getScreenshotChordLongPressDelay());
                         }
                         @Override
@@ -2867,8 +2893,7 @@
                 break;
             case KeyEvent.KEYCODE_S:
                 if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
-                    interceptScreenshotChord(
-                            TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+                    interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
                     return key_consumed;
                 }
                 break;
@@ -3010,7 +3035,19 @@
                     toggleNotificationPanel();
                 }
                 return key_consumed;
-
+            case KeyEvent.KEYCODE_SEARCH:
+                if (down && repeatCount == 0 && !keyguardOn()) {
+                    switch(mSearchKeyBehavior) {
+                        case SEARCH_BEHAVIOR_TARGET_ACTIVITY: {
+                            launchTargetSearchActivity();
+                            return key_consumed;
+                        }
+                        case SEARCH_BEHAVIOR_DEFAULT_SEARCH:
+                        default:
+                            break;
+                    }
+                }
+                break;
             case KeyEvent.KEYCODE_SPACE:
                 // Handle keyboard layout switching. (META + SPACE)
                 if ((metaState & KeyEvent.META_META_MASK) == 0) {
@@ -3252,8 +3289,7 @@
                 break;
             case KeyEvent.KEYCODE_SYSRQ:
                 if (down && repeatCount == 0) {
-                    interceptScreenshotChord(
-                            TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+                    interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
                 }
                 return true;
         }
@@ -3325,8 +3361,8 @@
     }
 
     @Override
-    public void onKeyguardOccludedChangedLw(boolean occluded) {
-        if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()) {
+    public void onKeyguardOccludedChangedLw(boolean occluded, boolean waitAppTransition) {
+        if (mKeyguardDelegate != null && waitAppTransition) {
             mPendingKeyguardOccluded = occluded;
             mKeyguardOccludedChanged = true;
         } else {
@@ -4152,9 +4188,6 @@
             case KeyEvent.KEYCODE_DEMO_APP_2:
             case KeyEvent.KEYCODE_DEMO_APP_3:
             case KeyEvent.KEYCODE_DEMO_APP_4: {
-                // TODO(b/254604589): Dispatch KeyEvent to System UI.
-                sendSystemKeyToStatusBarAsync(keyCode);
-
                 // Just drop if keys are not intercepted for direct key.
                 result &= ~ACTION_PASS_TO_USER;
                 break;
@@ -4740,10 +4773,15 @@
 
         // ... eventually calls finishWindowsDrawn which will finalize our screen turn on
         // as well as enabling the orientation change logic/sensor.
+        Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
+                TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
         mWindowManagerInternal.waitForAllWindowsDrawn(() -> {
             if (DEBUG_WAKEUP) Slog.i(TAG, "All windows ready for every display");
             mHandler.sendMessage(mHandler.obtainMessage(MSG_WINDOW_MANAGER_DRAWN_COMPLETE,
                     INVALID_DISPLAY, 0));
+
+            Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER,
+                    TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
             }, WAITING_FOR_DRAWN_TIMEOUT, INVALID_DISPLAY);
     }
 
@@ -4799,10 +4837,16 @@
             }
         } else {
             mScreenOnListeners.put(displayId, screenOnListener);
+
+            Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
+                    TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
             mWindowManagerInternal.waitForAllWindowsDrawn(() -> {
                 if (DEBUG_WAKEUP) Slog.i(TAG, "All windows ready for display: " + displayId);
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_WINDOW_MANAGER_DRAWN_COMPLETE,
                         displayId, 0));
+
+                Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER,
+                        TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
             }, WAITING_FOR_DRAWN_TIMEOUT, displayId);
         }
     }
@@ -6080,4 +6124,22 @@
         }
     }
 
+    private void launchTargetSearchActivity() {
+        Intent intent;
+        if (mSearchKeyTargetActivity != null) {
+            intent = new Intent();
+            intent.setComponent(mSearchKeyTargetActivity);
+        } else {
+            intent = new Intent(Intent.ACTION_WEB_SEARCH);
+        }
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+        try {
+            startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+        } catch (ActivityNotFoundException ignore) {
+            Slog.e(TAG, "Could not resolve activity with : "
+                    + intent.getComponent().flattenToString()
+                    + " name.");
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 4f00992..77007fa 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -166,9 +166,10 @@
 
     /**
      * Called when the Keyguard occluded state changed.
+     *
      * @param occluded Whether Keyguard is currently occluded or not.
      */
-    void onKeyguardOccludedChangedLw(boolean occluded);
+    void onKeyguardOccludedChangedLw(boolean occluded, boolean waitAppTransition);
 
     /**
      * Applies a keyguard occlusion change if one happened.
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index e7221c8..fde0b34 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -191,4 +191,12 @@
      * @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp
      */
     void enterStageSplitFromRunningApp(boolean leftOrTop);
+
+    /**
+     * Shows the media output switcher dialog.
+     *
+     * @param packageName of the session for which the output switcher is shown.
+     * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher
+     */
+    void showMediaOutputSwitcher(String packageName);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 45748e6..ab7292d 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -714,6 +714,16 @@
                 } catch (RemoteException ex) { }
             }
         }
+
+        @Override
+        public void showMediaOutputSwitcher(String packageName) {
+            if (mBar != null) {
+                try {
+                    mBar.showMediaOutputSwitcher(packageName);
+                } catch (RemoteException ex) {
+                }
+            }
+        }
     };
 
     private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 7252545..7489f80 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -709,7 +709,8 @@
         synchronized (mGlobalLock) {
             final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
             return r != null
-                    ? r.getRequestedOrientation() : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+                    ? r.getOverrideOrientation()
+                    : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 6b01a77..5f8e01e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -341,6 +341,7 @@
 import android.window.WindowContainerToken;
 
 import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.content.ReferrerIntent;
@@ -439,6 +440,9 @@
     // finished destroying itself.
     private static final int DESTROY_TIMEOUT = 10 * 1000;
 
+    // Rounding tolerance to be used in aspect ratio computations
+    private static final float ASPECT_RATIO_ROUNDING_TOLERANCE = 0.005f;
+
     final ActivityTaskManagerService mAtmService;
     @NonNull
     final ActivityInfo info; // activity info provided by developer in AndroidManifest
@@ -500,7 +504,10 @@
     private RemoteTransition mPendingRemoteTransition;
     ActivityOptions returningOptions; // options that are coming back via convertToTranslucent
     AppTimeTracker appTimeTracker; // set if we are tracking the time in this app/task/activity
+    @GuardedBy("this")
     ActivityServiceConnectionsHolder mServiceConnectionsHolder; // Service connections.
+    /** @see android.content.Context#BIND_ADJUST_WITH_ACTIVITY */
+    volatile boolean mVisibleForServiceConnection;
     UriPermissionOwner uriPermissions; // current special URI access perms.
     WindowProcessController app;      // if non-null, hosting application
     private State mState;    // current state we are in
@@ -550,7 +557,8 @@
     long lastLaunchTime;    // time of last launch of this activity
     ComponentName requestedVrComponent; // the requested component for handling VR mode.
 
-    boolean inHistory;  // are we in the history task?
+    /** Whether this activity is reachable from hierarchy. */
+    volatile boolean inHistory;
     final ActivityTaskSupervisor mTaskSupervisor;
     final RootWindowContainer mRootWindowContainer;
 
@@ -1165,8 +1173,10 @@
             pw.println(prefix + "mVoiceInteraction=true");
         }
         pw.print(prefix); pw.print("mOccludesParent="); pw.println(mOccludesParent);
-        pw.print(prefix); pw.print("mOrientation=");
-        pw.println(ActivityInfo.screenOrientationToString(mOrientation));
+        pw.print(prefix); pw.print("overrideOrientation=");
+        pw.println(ActivityInfo.screenOrientationToString(getOverrideOrientation()));
+        pw.print(prefix); pw.print("requestedOrientation=");
+        pw.println(ActivityInfo.screenOrientationToString(super.getOverrideOrientation()));
         pw.println(prefix + "mVisibleRequested=" + mVisibleRequested
                 + " mVisible=" + mVisible + " mClientVisible=" + isClientVisible()
                 + ((mDeferHidingClient) ? " mDeferHidingClient=" + mDeferHidingClient : "")
@@ -1899,7 +1909,9 @@
         }
     }
 
-    static @Nullable ActivityRecord forTokenLocked(IBinder token) {
+    /** Gets the corresponding record by the token. Note that it may not exist in the hierarchy. */
+    @Nullable
+    static ActivityRecord forToken(IBinder token) {
         if (token == null) return null;
         final Token activityToken;
         try {
@@ -1908,7 +1920,11 @@
             Slog.w(TAG, "Bad activity token: " + token, e);
             return null;
         }
-        final ActivityRecord r = activityToken.mActivityRef.get();
+        return activityToken.mActivityRef.get();
+    }
+
+    static @Nullable ActivityRecord forTokenLocked(IBinder token) {
+        final ActivityRecord r = forToken(token);
         return r == null || r.getRootTask() == null ? null : r;
     }
 
@@ -1964,6 +1980,15 @@
                     new ComponentName(info.packageName, info.targetActivity);
         }
 
+        // Don't move below setActivityType since it triggers onConfigurationChange ->
+        // resolveOverrideConfiguration that requires having mLetterboxUiController initialised.
+        // Don't move below setOrientation(info.screenOrientation) since it triggers
+        // getOverrideOrientation that requires having mLetterboxUiController
+        // initialised.
+        mLetterboxUiController = new LetterboxUiController(mWmService, this);
+        mCameraCompatControlEnabled = mWmService.mContext.getResources()
+                .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
+
         mTargetSdk = info.applicationInfo.targetSdkVersion;
         mShowForAllUsers = (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0;
         setOrientation(info.screenOrientation);
@@ -2084,12 +2109,6 @@
 
         launchMode = aInfo.launchMode;
 
-        // Don't move below setActivityType since it triggers onConfigurationChange ->
-        // resolveOverrideConfiguration that requires having mLetterboxUiController initialised.
-        mLetterboxUiController = new LetterboxUiController(mWmService, this);
-        mCameraCompatControlEnabled = mWmService.mContext.getResources()
-                .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
-
         setActivityType(_componentSpecified, _launchedFromUid, _intent, options, sourceRecord);
 
         immersive = (aInfo.flags & FLAG_IMMERSIVE) != 0;
@@ -2486,7 +2505,8 @@
             if (topAttached != null) {
                 if (topAttached.isSnapshotCompatible(snapshot)
                         // This trampoline must be the same rotation.
-                        && mDisplayContent.getDisplayRotation().rotationForOrientation(mOrientation,
+                        && mDisplayContent.getDisplayRotation().rotationForOrientation(
+                                getOverrideOrientation(),
                                 mDisplayContent.getRotation()) == snapshot.getRotation()) {
                     return STARTING_WINDOW_TYPE_SNAPSHOT;
                 }
@@ -3897,6 +3917,10 @@
         }
     }
 
+    boolean isFinishing() {
+        return finishing;
+    }
+
     /**
      * This method is to only be called from the client via binder when the activity is destroyed
      * AND finished.
@@ -4023,17 +4047,32 @@
         mDisplayContent.getDisplayPolicy().removeRelaunchingApp(this);
     }
 
+    ActivityServiceConnectionsHolder getOrCreateServiceConnectionsHolder() {
+        synchronized (this) {
+            if (mServiceConnectionsHolder == null) {
+                mServiceConnectionsHolder = new ActivityServiceConnectionsHolder(this);
+            }
+            return mServiceConnectionsHolder;
+        }
+    }
+
     /**
      * Perform clean-up of service connections in an activity record.
      */
     private void cleanUpActivityServices() {
-        if (mServiceConnectionsHolder == null) {
-            return;
+        synchronized (this) {
+            if (mServiceConnectionsHolder == null) {
+                return;
+            }
+            // Throw away any services that have been bound by this activity.
+            mServiceConnectionsHolder.disconnectActivityFromServices();
+            // This activity record is removing, make sure not to disconnect twice.
+            mServiceConnectionsHolder = null;
         }
-        // Throw away any services that have been bound by this activity.
-        mServiceConnectionsHolder.disconnectActivityFromServices();
-        // This activity record is removing, make sure not to disconnect twice.
-        mServiceConnectionsHolder = null;
+    }
+
+    private void updateVisibleForServiceConnection() {
+        mVisibleForServiceConnection = mVisibleRequested || mState == RESUMED || mState == PAUSING;
     }
 
     /**
@@ -5126,6 +5165,7 @@
             taskFragment.onActivityVisibleRequestedChanged();
         }
         setInsetsFrozen(!visible);
+        updateVisibleForServiceConnection();
         if (app != null) {
             mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */);
         }
@@ -5604,6 +5644,7 @@
                 return;
             }
         }
+        updateVisibleForServiceConnection();
         if (app != null) {
             mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */);
         }
@@ -7704,13 +7745,13 @@
                 return mLetterboxUiController.getInheritedOrientation();
             }
         }
-        if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) {
+        if (task != null && getOverrideOrientation() == SCREEN_ORIENTATION_BEHIND) {
             // We use Task here because we want to be consistent with what happens in
             // multi-window mode where other tasks orientations are ignored.
             final ActivityRecord belowCandidate = task.getActivity(
-                    a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing
-                            && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this,
-                    false /* includeBoundary */, true /* traverseTopToBottom */);
+                    a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
+                    this /* boundary */, false /* includeBoundary */,
+                    true /* traverseTopToBottom */);
             if (belowCandidate != null) {
                 return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
             }
@@ -7718,6 +7759,19 @@
         return super.getRequestedConfigurationOrientation(forDisplay);
     }
 
+    /**
+     * Whether this activity can be used as an orientation source for activities above with
+     * {@link SCREEN_ORIENTATION_BEHIND}.
+     */
+    boolean canDefineOrientationForActivitiesAbove() {
+        if (finishing) {
+            return false;
+        }
+        final int overrideOrientation = getOverrideOrientation();
+        return overrideOrientation != SCREEN_ORIENTATION_UNSET
+                && overrideOrientation != SCREEN_ORIENTATION_BEHIND;
+    }
+
     @Override
     void onCancelFixedRotationTransform(int originalDisplayRotation) {
         if (this != mDisplayContent.getLastOrientationSource()) {
@@ -7744,7 +7798,7 @@
         }
     }
 
-    void setRequestedOrientation(int requestedOrientation) {
+    void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) {
         if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) {
             return;
         }
@@ -7789,7 +7843,7 @@
             // Allow app to specify orientation regardless of its visibility state if the current
             // candidate want us to use orientation behind. I.e. the visible app on-top of this one
             // wants us to use the orientation of the app behind it.
-            return mOrientation;
+            return getOverrideOrientation();
         }
 
         // The {@link ActivityRecord} should only specify an orientation when it is not closing.
@@ -7797,15 +7851,31 @@
         // task being started in the wrong orientation during the transition.
         if (!getDisplayContent().mClosingApps.contains(this)
                 && (isVisibleRequested() || getDisplayContent().mOpeningApps.contains(this))) {
-            return mOrientation;
+            return getOverrideOrientation();
         }
 
         return SCREEN_ORIENTATION_UNSET;
     }
 
-    /** Returns the app's preferred orientation regardless of its currently visibility state. */
+    /**
+     * Returns the app's preferred orientation regardless of its current visibility state taking
+     * into account orientation per-app overrides applied by the device manufacturers.
+     */
+    @Override
+    protected int getOverrideOrientation() {
+        return mLetterboxUiController.overrideOrientationIfNeeded(super.getOverrideOrientation());
+    }
+
+    /**
+     * Returns the app's preferred orientation regardless of its currently visibility state. This
+     * is used to return a requested value to an app if they call {@link
+     * android.app.Activity#getRequestedOrientation} since {@link #getOverrideOrientation} value
+     * with override can confuse an app if it's different from what they requested with {@link
+     * android.app.Activity#setRequestedOrientation}.
+     */
+    @ActivityInfo.ScreenOrientation
     int getRequestedOrientation() {
-        return mOrientation;
+        return super.getOverrideOrientation();
     }
 
     /**
@@ -7838,6 +7908,14 @@
     }
 
     /**
+     * @return The {@code true} if the current instance has {@link mCompatDisplayInsets} without
+     * considering the inheritance implemented in {@link #getCompatDisplayInsets()}
+     */
+    boolean hasCompatDisplayInsetsWithoutInheritance() {
+        return mCompatDisplayInsets != null;
+    }
+
+    /**
      * @return {@code true} if this activity is in size compatibility mode that uses the different
      *         density than its parent or its bounds don't fit in parent naturally.
      */
@@ -7845,7 +7923,7 @@
         if (mInSizeCompatModeForBounds) {
             return true;
         }
-        if (mCompatDisplayInsets == null || !shouldCreateCompatDisplayInsets()
+        if (getCompatDisplayInsets() == null || !shouldCreateCompatDisplayInsets()
                 // The orientation is different from parent when transforming.
                 || isFixedRotationTransforming()) {
             return false;
@@ -7916,11 +7994,7 @@
 
     // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
     private void updateCompatDisplayInsets() {
-        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
-            mCompatDisplayInsets =  mLetterboxUiController.getInheritedCompatDisplayInsets();
-            return;
-        }
-        if (mCompatDisplayInsets != null || !shouldCreateCompatDisplayInsets()) {
+        if (getCompatDisplayInsets() != null || !shouldCreateCompatDisplayInsets()) {
             // The override configuration is set only once in size compatibility mode.
             return;
         }
@@ -7983,9 +8057,6 @@
 
     @Override
     float getCompatScale() {
-        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
-            return mLetterboxUiController.getInheritedSizeCompatScale();
-        }
         return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale();
     }
 
@@ -8029,7 +8100,7 @@
             resolveFixedOrientationConfiguration(newParentConfiguration);
         }
 
-        if (mCompatDisplayInsets != null) {
+        if (getCompatDisplayInsets() != null) {
             resolveSizeCompatModeConfiguration(newParentConfiguration);
         } else if (inMultiWindowMode() && !isFixedOrientationLetterboxAllowed) {
             // We ignore activities' requested orientation in multi-window modes. They may be
@@ -8047,7 +8118,7 @@
             resolveAspectRatioRestriction(newParentConfiguration);
         }
 
-        if (isFixedOrientationLetterboxAllowed || mCompatDisplayInsets != null
+        if (isFixedOrientationLetterboxAllowed || getCompatDisplayInsets() != null
                 // In fullscreen, can be letterboxed for aspect ratio.
                 || !inMultiWindowMode()) {
             updateResolvedBoundsPosition(newParentConfiguration);
@@ -8055,7 +8126,8 @@
 
         boolean isIgnoreOrientationRequest = mDisplayContent != null
                 && mDisplayContent.getIgnoreOrientationRequest();
-        if (mCompatDisplayInsets == null // for size compat mode set in updateCompatDisplayInsets
+        if (getCompatDisplayInsets() == null
+                // for size compat mode set in updateCompatDisplayInsets
                 // Fixed orientation letterboxing is possible on both large screen devices
                 // with ignoreOrientationRequest enabled and on phones in split screen even with
                 // ignoreOrientationRequest disabled.
@@ -8101,7 +8173,7 @@
                         info.neverSandboxDisplayApis(sConstrainDisplayApisConfig),
                         info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig),
                         !matchParentBounds(),
-                        mCompatDisplayInsets != null,
+                        getCompatDisplayInsets() != null,
                         shouldCreateCompatDisplayInsets());
             }
             resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
@@ -8204,8 +8276,12 @@
             if (screenResolvedBounds.width() <= parentAppBounds.width()) {
                 float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
                         newParentConfiguration);
-                offsetX = (int) Math.ceil((parentAppBounds.width() - screenResolvedBounds.width())
-                        * positionMultiplier);
+                offsetX = Math.max(0, (int) Math.ceil((parentAppBounds.width()
+                        - screenResolvedBounds.width()) * positionMultiplier)
+                        // This is added to make sure that insets added inside
+                        // CompatDisplayInsets#getContainerBounds() do not break the alignment
+                        // provided by the positionMultiplier
+                        - screenResolvedBounds.left + parentAppBounds.left);
             }
         }
 
@@ -8215,8 +8291,12 @@
             if (screenResolvedBounds.height() <= parentAppBounds.height()) {
                 float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier(
                         newParentConfiguration);
-                offsetY = (int) Math.ceil((parentAppBounds.height() - screenResolvedBounds.height())
-                        * positionMultiplier);
+                offsetY = Math.max(0, (int) Math.ceil((parentAppBounds.height()
+                        - screenResolvedBounds.height()) * positionMultiplier)
+                        // This is added to make sure that insets added inside
+                        // CompatDisplayInsets#getContainerBounds() do not break the alignment
+                        // provided by the positionMultiplier
+                        - screenResolvedBounds.top + parentAppBounds.top);
             }
         }
 
@@ -8243,7 +8323,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() {
@@ -8357,8 +8443,8 @@
         // If orientation is respected when insets are applied, then stableBounds will be empty.
         boolean orientationRespectedWithInsets =
                 orientationRespectedWithInsets(parentBounds, stableBounds);
-        if (orientationRespectedWithInsets
-                && handlesOrientationChangeFromDescendant(mOrientation)) {
+        if (orientationRespectedWithInsets && handlesOrientationChangeFromDescendant(
+                getOverrideOrientation())) {
             // No need to letterbox because of fixed orientation. Display will handle
             // fixed-orientation requests and a display rotation is enough to respect requested
             // orientation with insets applied.
@@ -8386,8 +8472,9 @@
                 || orientationRespectedWithInsets)) {
             return;
         }
+        final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets();
 
-        if (mCompatDisplayInsets != null && !mCompatDisplayInsets.mIsInFixedOrientationLetterbox) {
+        if (compatDisplayInsets != null && !compatDisplayInsets.mIsInFixedOrientationLetterbox) {
             // App prefers to keep its original size.
             // If the size compat is from previous fixed orientation letterboxing, we may want to
             // have fixed orientation letterbox again, otherwise it will show the size compat
@@ -8442,9 +8529,9 @@
         mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingBoundsWithInsets,
                 containingBounds, desiredAspectRatio);
 
-        if (mCompatDisplayInsets != null) {
-            mCompatDisplayInsets.getBoundsByRotation(
-                    mTmpBounds, newParentConfig.windowConfiguration.getRotation());
+        if (compatDisplayInsets != null) {
+            compatDisplayInsets.getBoundsByRotation(mTmpBounds,
+                    newParentConfig.windowConfiguration.getRotation());
             if (resolvedBounds.width() != mTmpBounds.width()
                     || resolvedBounds.height() != mTmpBounds.height()) {
                 // The app shouldn't be resized, we only do fixed orientation letterboxing if the
@@ -8458,7 +8545,7 @@
         // Calculate app bounds using fixed orientation bounds because they will be needed later
         // for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}.
         getTaskFragment().computeConfigResourceOverrides(getResolvedOverrideConfiguration(),
-                newParentConfig, mCompatDisplayInsets);
+                newParentConfig, compatDisplayInsets);
         mLetterboxBoundsForFixedOrientationAndAspectRatio = new Rect(resolvedBounds);
     }
 
@@ -8515,13 +8602,13 @@
                 ? requestedOrientation
                 // We should use the original orientation of the activity when possible to avoid
                 // forcing the activity in the opposite orientation.
-                : mCompatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED
-                        ? mCompatDisplayInsets.mOriginalRequestedOrientation
+                : getCompatDisplayInsets().mOriginalRequestedOrientation != ORIENTATION_UNDEFINED
+                        ? getCompatDisplayInsets().mOriginalRequestedOrientation
                         : newParentConfiguration.orientation;
         int rotation = newParentConfiguration.windowConfiguration.getRotation();
         final boolean isFixedToUserRotation = mDisplayContent == null
                 || mDisplayContent.getDisplayRotation().isFixedToUserRotation();
-        if (!isFixedToUserRotation && !mCompatDisplayInsets.mIsFloating) {
+        if (!isFixedToUserRotation && !getCompatDisplayInsets().mIsFloating) {
             // Use parent rotation because the original display can be rotated.
             resolvedConfig.windowConfiguration.setRotation(rotation);
         } else {
@@ -8537,11 +8624,11 @@
         // rely on them to contain the original and unchanging width and height of the app.
         final Rect containingAppBounds = new Rect();
         final Rect containingBounds = mTmpBounds;
-        mCompatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation,
+        getCompatDisplayInsets().getContainerBounds(containingAppBounds, containingBounds, rotation,
                 orientation, orientationRequested, isFixedToUserRotation);
         resolvedBounds.set(containingBounds);
         // The size of floating task is fixed (only swap), so the aspect ratio is already correct.
-        if (!mCompatDisplayInsets.mIsFloating) {
+        if (!getCompatDisplayInsets().mIsFloating) {
             mIsAspectRatioApplied =
                     applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds);
         }
@@ -8550,7 +8637,7 @@
         // are calculated in compat container space. The actual position on screen will be applied
         // later, so the calculation is simpler that doesn't need to involve offset from parent.
         getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
-                mCompatDisplayInsets);
+                getCompatDisplayInsets());
         // Use current screen layout as source because the size of app is independent to parent.
         resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
                 getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
@@ -8585,14 +8672,9 @@
 
         // Calculates the scale the size compatibility bounds into the region which is available
         // to application.
-        final int contentW = resolvedAppBounds.width();
-        final int contentH = resolvedAppBounds.height();
-        final int viewportW = containerAppBounds.width();
-        final int viewportH = containerAppBounds.height();
         final float lastSizeCompatScale = mSizeCompatScale;
-        // Only allow to scale down.
-        mSizeCompatScale = (contentW <= viewportW && contentH <= viewportH)
-                ? 1f : Math.min((float) viewportW / contentW, (float) viewportH / contentH);
+        updateSizeCompatScale(resolvedAppBounds, containerAppBounds);
+
         final int containerTopInset = containerAppBounds.top - containerBounds.top;
         final boolean topNotAligned =
                 containerTopInset != resolvedAppBounds.top - resolvedBounds.top;
@@ -8632,6 +8714,20 @@
                 isInSizeCompatModeForBounds(resolvedAppBounds, containerAppBounds);
     }
 
+    void updateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) {
+        // Only allow to scale down.
+        mSizeCompatScale = mLetterboxUiController.findOpaqueNotFinishingActivityBelow()
+                .map(activityRecord -> activityRecord.mSizeCompatScale)
+                .orElseGet(() -> {
+                    final int contentW = resolvedAppBounds.width();
+                    final int contentH = resolvedAppBounds.height();
+                    final int viewportW = containerAppBounds.width();
+                    final int viewportH = containerAppBounds.height();
+                    return (contentW <= viewportW && contentH <= viewportH) ? 1f : Math.min(
+                            (float) viewportW / contentW, (float) viewportH / contentH);
+                });
+    }
+
     private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) {
         if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
             // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity
@@ -8694,10 +8790,16 @@
 
     @Override
     public Rect getBounds() {
-        if (mSizeCompatBounds != null) {
-            return mSizeCompatBounds;
-        }
-        return super.getBounds();
+        // TODO(b/268458693): Refactor configuration inheritance in case of translucent activities
+        final Rect superBounds = super.getBounds();
+        return mLetterboxUiController.findOpaqueNotFinishingActivityBelow()
+                .map(ActivityRecord::getBounds)
+                .orElseGet(() -> {
+                    if (mSizeCompatBounds != null) {
+                        return mSizeCompatBounds;
+                    }
+                    return superBounds;
+                });
     }
 
     @Override
@@ -8722,7 +8824,7 @@
         // Max bounds should be sandboxed when an activity should have compatDisplayInsets, and it
         // will keep the same bounds and screen configuration when it was first launched regardless
         // how its parent window changes, so that the sandbox API will provide a consistent result.
-        if (mCompatDisplayInsets != null || shouldCreateCompatDisplayInsets()) {
+        if (getCompatDisplayInsets() != null || shouldCreateCompatDisplayInsets()) {
             return true;
         }
 
@@ -8764,7 +8866,7 @@
                 mTransitionController.collect(this);
             }
         }
-        if (mCompatDisplayInsets != null) {
+        if (getCompatDisplayInsets() != null) {
             Configuration overrideConfig = getRequestedOverrideConfiguration();
             // Adapt to changes in orientation locking. The app is still non-resizable, but
             // it can change which orientation is fixed. If the fixed orientation changes,
@@ -8840,7 +8942,7 @@
         if (mVisibleRequested) {
             // It may toggle the UI for user to restart the size compatibility mode activity.
             display.handleActivitySizeCompatModeIfNeeded(this);
-        } else if (mCompatDisplayInsets != null && !visibleIgnoringKeyguard
+        } else if (getCompatDisplayInsets() != null && !visibleIgnoringKeyguard
                 && (app == null || !app.hasVisibleActivities())) {
             // visibleIgnoringKeyguard is checked to avoid clearing mCompatDisplayInsets during
             // displays change. Displays are turned off during the change so mVisibleRequested
@@ -8915,7 +9017,7 @@
         int activityWidth = containingAppWidth;
         int activityHeight = containingAppHeight;
 
-        if (containingRatio > desiredAspectRatio) {
+        if (containingRatio - desiredAspectRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
             if (containingAppWidth < containingAppHeight) {
                 // Width is the shorter side, so we use that to figure-out what the max. height
                 // should be given the aspect ratio.
@@ -8925,7 +9027,7 @@
                 // should be given the aspect ratio.
                 activityWidth = (int) ((activityHeight * desiredAspectRatio) + 0.5f);
             }
-        } else if (containingRatio < desiredAspectRatio) {
+        } else if (desiredAspectRatio - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
             boolean adjustWidth;
             switch (getRequestedConfigurationOrientation()) {
                 case ORIENTATION_LANDSCAPE:
@@ -9003,7 +9105,8 @@
         }
 
         if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY)
-                && !ActivityInfo.isFixedOrientationPortrait(getRequestedOrientation())) {
+                && !ActivityInfo.isFixedOrientationPortrait(
+                        getOverrideOrientation())) {
             return info.getMinAspectRatio();
         }
 
@@ -9998,6 +10101,7 @@
                     isLandscape ? shortSide : longSide);
         }
 
+        // TODO(b/267151420): Explore removing getContainerBounds() from CompatDisplayInsets.
         /** Gets the horizontal centered container bounds for size compatibility mode. */
         void getContainerBounds(Rect outAppBounds, Rect outBounds, int rotation, int orientation,
                 boolean orientationRequested, boolean isFixedToUserRotation) {
@@ -10166,8 +10270,8 @@
     // TODO(b/263592337): Explore enabling compat fake focus for fullscreen, e.g. for when
     // covered with bubbles.
     boolean shouldSendCompatFakeFocus() {
-        return mWmService.mLetterboxConfiguration.isCompatFakeFocusEnabled(info)
-                && inMultiWindowMode() && !inPinnedWindowingMode() && !inFreeformWindowingMode();
+        return mLetterboxUiController.shouldSendFakeFocus() && inMultiWindowMode()
+                && !inPinnedWindowingMode() && !inFreeformWindowingMode();
     }
 
     static class Builder {
diff --git a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
index 0859d40..5f56af7 100644
--- a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
+++ b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
@@ -16,14 +16,14 @@
 
 package com.android.server.wm;
 
-import static com.android.server.wm.ActivityRecord.State.PAUSING;
-import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_CLEANUP;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 
 import android.util.ArraySet;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.util.function.Consumer;
 
@@ -38,8 +38,6 @@
  */
 public class ActivityServiceConnectionsHolder<T> {
 
-    private final ActivityTaskManagerService mService;
-
     /** The activity the owns this service connection object. */
     private final ActivityRecord mActivity;
 
@@ -49,19 +47,19 @@
      * on the WM side since we don't perform operations on the object. Mainly here for communication
      * and booking with the AM side.
      */
+    @GuardedBy("mActivity")
     private ArraySet<T> mConnections;
 
     /** Whether all connections of {@link #mActivity} are being removed. */
     private volatile boolean mIsDisconnecting;
 
-    ActivityServiceConnectionsHolder(ActivityTaskManagerService service, ActivityRecord activity) {
-        mService = service;
+    ActivityServiceConnectionsHolder(ActivityRecord activity) {
         mActivity = activity;
     }
 
     /** Adds a connection record that the activity has bound to a specific service. */
     public void addConnection(T c) {
-        synchronized (mService.mGlobalLock) {
+        synchronized (mActivity) {
             if (mIsDisconnecting) {
                 // This is unlikely to happen because the caller should create a new holder.
                 if (DEBUG_CLEANUP) {
@@ -79,7 +77,7 @@
 
     /** Removed a connection record between the activity and a specific service. */
     public void removeConnection(T c) {
-        synchronized (mService.mGlobalLock) {
+        synchronized (mActivity) {
             if (mConnections == null) {
                 return;
             }
@@ -90,20 +88,18 @@
         }
     }
 
+    /** @see android.content.Context#BIND_ADJUST_WITH_ACTIVITY */
     public boolean isActivityVisible() {
-        synchronized (mService.mGlobalLock) {
-            return mActivity.isVisibleRequested() || mActivity.isState(RESUMED, PAUSING);
-        }
+        return mActivity.mVisibleForServiceConnection;
     }
 
     public int getActivityPid() {
-        synchronized (mService.mGlobalLock) {
-            return mActivity.hasProcess() ? mActivity.app.getPid() : -1;
-        }
+        final WindowProcessController wpc = mActivity.app;
+        return wpc != null ? wpc.getPid() : -1;
     }
 
     public void forEachConnection(Consumer<T> consumer) {
-        synchronized (mService.mGlobalLock) {
+        synchronized (mActivity) {
             if (mConnections == null || mConnections.isEmpty()) {
                 return;
             }
@@ -118,6 +114,7 @@
      * general, this method is used to clean up if the activity didn't unbind services before it
      * is destroyed.
      */
+    @GuardedBy("mActivity")
     void disconnectActivityFromServices() {
         if (mConnections == null || mConnections.isEmpty() || mIsDisconnecting) {
             return;
@@ -130,16 +127,14 @@
         // still in the message queue, so keep the reference of {@link #mConnections} to make sure
         // the connection list is up-to-date.
         mIsDisconnecting = true;
-        mService.mH.post(() -> {
-            mService.mAmInternal.disconnectActivityFromServices(this);
+        mActivity.mAtmService.mH.post(() -> {
+            mActivity.mAtmService.mAmInternal.disconnectActivityFromServices(this);
             mIsDisconnecting = false;
         });
     }
 
     public void dump(PrintWriter pw, String prefix) {
-        synchronized (mService.mGlobalLock) {
-            pw.println(prefix + "activity=" + mActivity);
-        }
+        pw.println(prefix + "activity=" + mActivity);
     }
 
     /** Used by {@link ActivityRecord#dump}. */
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 4cbb2cd..9236ab6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1486,7 +1486,7 @@
         a.persistableMode = ActivityInfo.PERSIST_NEVER;
         a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
-        a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
+        a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_NO_HISTORY;
         a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
         a.configChanges = 0xffffffff;
 
@@ -4724,6 +4724,7 @@
                 mTaskChangeNotificationController.notifyTaskFocusChanged(prevTask.mTaskId, false);
             }
             mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true);
+            mTaskSupervisor.mRecentTasks.add(task);
         }
 
         applyUpdateLockStateLocked(r);
@@ -6008,18 +6009,11 @@
 
         @Override
         public ActivityServiceConnectionsHolder getServiceConnectionsHolder(IBinder token) {
-            synchronized (mGlobalLock) {
-                final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
-                if (r == null) {
-                    return null;
-                }
-                if (r.mServiceConnectionsHolder == null) {
-                    r.mServiceConnectionsHolder = new ActivityServiceConnectionsHolder(
-                            ActivityTaskManagerService.this, r);
-                }
-
-                return r.mServiceConnectionsHolder;
+            final ActivityRecord r = ActivityRecord.forToken(token);
+            if (r == null || !r.inHistory) {
+                return null;
             }
+            return r.getOrCreateServiceConnectionsHolder();
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index fd6c974..b160af6a 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -98,7 +98,7 @@
                     throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
                 }
                 return mService.getRecentTasks().createRecentTaskInfo(task,
-                        false /* stripExtras */);
+                        false /* stripExtras */, true /* getTasksAllowed */);
             } finally {
                 Binder.restoreCallingIdentity(origId);
             }
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index abaa363..87f985a 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -181,6 +181,42 @@
                 || !transitionGoodToGoForTaskFragments()) {
             return;
         }
+        final boolean isRecentsInOpening = mDisplayContent.mOpeningApps.stream().anyMatch(
+                ConfigurationContainer::isActivityTypeRecents);
+        // In order to avoid visual clutter caused by a conflict between app transition
+        // animation and recents animation, app transition is delayed until recents finishes.
+        // One exceptional case. When 3P launcher is used and a user taps a task screenshot in
+        // task switcher (isRecentsInOpening=true), app transition must start even though
+        // recents is running. Otherwise app transition is blocked until timeout (b/232984498).
+        // When 1P launcher is used, this animation is controlled by the launcher outside of
+        // the app transition, so delaying app transition doesn't cause visible delay. After
+        // recents finishes, app transition is handled just to commit visibility on apps.
+        if (!isRecentsInOpening) {
+            final ArraySet<WindowContainer> participants = new ArraySet<>();
+            participants.addAll(mDisplayContent.mOpeningApps);
+            participants.addAll(mDisplayContent.mChangingContainers);
+            boolean deferForRecents = false;
+            for (int i = 0; i < participants.size(); i++) {
+                WindowContainer wc = participants.valueAt(i);
+                final ActivityRecord activity = getAppFromContainer(wc);
+                if (activity == null) {
+                    continue;
+                }
+                // Don't defer recents animation if one of activity isn't running for it, that one
+                // might be started from quickstep.
+                if (!activity.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
+                    deferForRecents = false;
+                    break;
+                }
+                deferForRecents = true;
+            }
+            if (deferForRecents) {
+                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
+                        "Delaying app transition for recents animation to finish");
+                return;
+            }
+        }
+
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");
 
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "**** GOOD TO GO");
@@ -892,7 +928,7 @@
      *
      * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled.
      */
-    private static boolean isTaskViewTask(WindowContainer wc) {
+    static boolean isTaskViewTask(WindowContainer wc) {
         // We use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and
         // it is not guaranteed to work this logic in the future version.
         return wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer;
@@ -1249,27 +1285,12 @@
                     "Delaying app transition for screen rotation animation to finish");
             return false;
         }
-        final boolean isRecentsInOpening = mDisplayContent.mOpeningApps.stream().anyMatch(
-                ConfigurationContainer::isActivityTypeRecents);
         for (int i = 0; i < apps.size(); i++) {
             WindowContainer wc = apps.valueAt(i);
             final ActivityRecord activity = getAppFromContainer(wc);
             if (activity == null) {
                 continue;
             }
-            // In order to avoid visual clutter caused by a conflict between app transition
-            // animation and recents animation, app transition is delayed until recents finishes.
-            // One exceptional case. When 3P launcher is used and a user taps a task screenshot in
-            // task switcher (isRecentsInOpening=true), app transition must start even though
-            // recents is running. Otherwise app transition is blocked until timeout (b/232984498).
-            // When 1P launcher is used, this animation is controlled by the launcher outside of
-            // the app transition, so delaying app transition doesn't cause visible delay. After
-            // recents finishes, app transition is handled just to commit visibility on apps.
-            if (!isRecentsInOpening && activity.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                        "Delaying app transition for recents animation to finish");
-                return false;
-            }
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                     "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
                             + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
index a6f8557..7d9a4ec 100644
--- a/services/core/java/com/android/server/wm/DeviceStateController.java
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -16,80 +16,107 @@
 
 package com.android.server.wm;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.devicestate.DeviceStateManager;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 
+import com.android.internal.R;
 import com.android.internal.util.ArrayUtils;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Consumer;
 
 /**
- * Class that registers callbacks with the {@link DeviceStateManager} and
- * responds to fold state changes by forwarding such events to a delegate.
+ * Class that registers callbacks with the {@link DeviceStateManager} and responds to device
+ * changes.
  */
-final class DeviceStateController {
+final class DeviceStateController implements DeviceStateManager.DeviceStateCallback {
+
+    @NonNull
     private final DeviceStateManager mDeviceStateManager;
-    private final Context mContext;
+    @NonNull
+    private final int[] mOpenDeviceStates;
+    @NonNull
+    private final int[] mHalfFoldedDeviceStates;
+    @NonNull
+    private final int[] mFoldedDeviceStates;
+    @NonNull
+    private final int[] mRearDisplayDeviceStates;
+    @NonNull
+    private final int[] mReverseRotationAroundZAxisStates;
+    @NonNull
+    private final List<Consumer<DeviceState>> mDeviceStateCallbacks = new ArrayList<>();
 
-    private FoldStateListener mDeviceStateListener;
+    @Nullable
+    private DeviceState mLastDeviceState;
+    private int mCurrentState;
 
-    public enum FoldState {
-        UNKNOWN, OPEN, FOLDED, HALF_FOLDED
+    public enum DeviceState {
+        UNKNOWN, OPEN, FOLDED, HALF_FOLDED, REAR,
     }
 
-    DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) {
-        mContext = context;
-        mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class);
+    DeviceStateController(@NonNull Context context, @NonNull Handler handler) {
+        mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+
+        mOpenDeviceStates = context.getResources()
+                .getIntArray(R.array.config_openDeviceStates);
+        mHalfFoldedDeviceStates = context.getResources()
+                .getIntArray(R.array.config_halfFoldedDeviceStates);
+        mFoldedDeviceStates = context.getResources()
+                .getIntArray(R.array.config_foldedDeviceStates);
+        mRearDisplayDeviceStates = context.getResources()
+                .getIntArray(R.array.config_rearDisplayDeviceStates);
+        mReverseRotationAroundZAxisStates = context.getResources()
+                .getIntArray(R.array.config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis);
+
         if (mDeviceStateManager != null) {
-            mDeviceStateListener = new FoldStateListener(mContext, delegate);
-            mDeviceStateManager
-                    .registerCallback(new HandlerExecutor(handler),
-                            mDeviceStateListener);
+            mDeviceStateManager.registerCallback(new HandlerExecutor(handler), this);
         }
     }
 
     void unregisterFromDeviceStateManager() {
-        if (mDeviceStateListener != null) {
-            mDeviceStateManager.unregisterCallback(mDeviceStateListener);
+        if (mDeviceStateManager != null) {
+            mDeviceStateManager.unregisterCallback(this);
         }
     }
 
+    void registerDeviceStateCallback(@NonNull Consumer<DeviceState> callback) {
+        mDeviceStateCallbacks.add(callback);
+    }
+
     /**
-     * A listener for half-fold device state events that dispatches state changes to a delegate.
+     * @return true if the rotation direction on the Z axis should be reversed.
      */
-    static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+    boolean shouldReverseRotationDirectionAroundZAxis() {
+        return ArrayUtils.contains(mReverseRotationAroundZAxisStates, mCurrentState);
+    }
 
-        private final int[] mHalfFoldedDeviceStates;
-        private final int[] mFoldedDeviceStates;
+    @Override
+    public void onStateChanged(int state) {
+        mCurrentState = state;
 
-        @Nullable
-        private FoldState mLastResult;
-        private final Consumer<FoldState> mDelegate;
-
-        FoldStateListener(Context context, Consumer<FoldState> delegate) {
-            mFoldedDeviceStates = context.getResources().getIntArray(
-                    com.android.internal.R.array.config_foldedDeviceStates);
-            mHalfFoldedDeviceStates = context.getResources().getIntArray(
-                    com.android.internal.R.array.config_halfFoldedDeviceStates);
-            mDelegate = delegate;
+        final DeviceState deviceState;
+        if (ArrayUtils.contains(mHalfFoldedDeviceStates, state)) {
+            deviceState = DeviceState.HALF_FOLDED;
+        } else if (ArrayUtils.contains(mFoldedDeviceStates, state)) {
+            deviceState = DeviceState.FOLDED;
+        } else if (ArrayUtils.contains(mRearDisplayDeviceStates, state)) {
+            deviceState = DeviceState.REAR;
+        } else if (ArrayUtils.contains(mOpenDeviceStates, state)) {
+            deviceState = DeviceState.OPEN;
+        } else {
+            deviceState = DeviceState.UNKNOWN;
         }
 
-        @Override
-        public void onStateChanged(int state) {
-            final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state);
-            FoldState result;
-            if (halfFolded) {
-                result = FoldState.HALF_FOLDED;
-            } else {
-                final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
-                result = folded ? FoldState.FOLDED : FoldState.OPEN;
-            }
-            if (mLastResult == null || !mLastResult.equals(result)) {
-                mLastResult = result;
-                mDelegate.accept(result);
+        if (mLastDeviceState == null || !mLastDeviceState.equals(deviceState)) {
+            mLastDeviceState = deviceState;
+
+            for (Consumer<DeviceState> callback : mDeviceStateCallbacks) {
+                callback.accept(mLastDeviceState);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 71fef05..fde2105 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -94,7 +95,7 @@
     DisplayArea(WindowManagerService wms, Type type, String name, int featureId) {
         super(wms);
         // TODO(display-area): move this up to ConfigurationContainer
-        mOrientation = SCREEN_ORIENTATION_UNSET;
+        setOverrideOrientation(SCREEN_ORIENTATION_UNSET);
         mType = type;
         mName = name;
         mFeatureId = featureId;
@@ -166,7 +167,8 @@
         // If this is set to ignore the orientation request, we don't propagate descendant
         // orientation request.
         final int orientation = requestingContainer != null
-                ? requestingContainer.mOrientation : SCREEN_ORIENTATION_UNSET;
+                ? requestingContainer.getOverrideOrientation()
+                : SCREEN_ORIENTATION_UNSET;
         return !getIgnoreOrientationRequest(orientation)
                 && super.onDescendantOrientationChanged(requestingContainer);
     }
@@ -245,7 +247,22 @@
                 || orientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
             return false;
         }
-        return getIgnoreOrientationRequest();
+        return getIgnoreOrientationRequest()
+                && !shouldRespectOrientationRequestDueToPerAppOverride();
+    }
+
+    private boolean shouldRespectOrientationRequestDueToPerAppOverride() {
+        if (mDisplayContent == null) {
+            return false;
+        }
+        ActivityRecord activity = mDisplayContent.topRunningActivity(
+                /* considerKeyguardState= */ true);
+        return activity != null && activity.getTaskFragment() != null
+                // Checking TaskFragment rather than ActivityRecord to ensure that transition
+                // between fullscreen and PiP would work well. Checking TaskFragment rather than
+                // Task to ensure that Activity Embedding is excluded.
+                && activity.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                && activity.mLetterboxUiController.isOverrideRespectRequestedOrientationEnabled();
     }
 
     boolean getIgnoreOrientationRequest() {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 75d84ea..ec355b7 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -27,6 +27,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -1125,14 +1126,18 @@
                     mWmService.mAtmService.getRecentTasks().getInputListener());
         }
 
-        mDisplayPolicy = new DisplayPolicy(mWmService, this);
-        mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address);
+        mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH);
 
-        mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
-                newFoldState -> {
+        mDisplayPolicy = new DisplayPolicy(mWmService, this);
+        mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address,
+                mDeviceStateController);
+
+        final Consumer<DeviceStateController.DeviceState> deviceStateConsumer =
+                (@NonNull DeviceStateController.DeviceState newFoldState) -> {
                     mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
                     mDisplayRotation.foldStateChanged(newFoldState);
-                });
+                };
+        mDeviceStateController.registerDeviceStateCallback(deviceStateConsumer);
 
         mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat(
                 R.dimen.config_closeToSquareDisplayMaxAspectRatio);
@@ -1572,7 +1577,8 @@
         // If display rotation class tells us that it doesn't consider app requested orientation,
         // this display won't rotate just because of an app changes its requested orientation. Thus
         // it indicates that this display chooses not to handle this request.
-        final int orientation = requestingContainer != null ? requestingContainer.mOrientation
+        final int orientation = requestingContainer != null
+                ? requestingContainer.getOverrideOrientation()
                 : SCREEN_ORIENTATION_UNSET;
         final boolean handled = handlesOrientationChangeFromDescendant(orientation);
         if (config == null) {
@@ -1673,7 +1679,7 @@
             }
             // The orientation source may not be the top if it uses SCREEN_ORIENTATION_BEHIND.
             final ActivityRecord topCandidate = !r.isVisibleRequested() ? topRunningActivity() : r;
-            if (handleTopActivityLaunchingInDifferentOrientation(
+            if (topCandidate != null && handleTopActivityLaunchingInDifferentOrientation(
                     topCandidate, r, true /* checkOpening */)) {
                 // Display orientation should be deferred until the top fixed rotation is finished.
                 return false;
@@ -1698,15 +1704,15 @@
         if (mTransitionController.useShellTransitionsRotation()) {
             return ROTATION_UNDEFINED;
         }
+        final int activityOrientation = r.getOverrideOrientation();
         if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM
-                || getIgnoreOrientationRequest(r.mOrientation)) {
+                || getIgnoreOrientationRequest(activityOrientation)) {
             return ROTATION_UNDEFINED;
         }
-        if (r.mOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
+        if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
             final ActivityRecord nextCandidate = getActivity(
-                    a -> a.mOrientation != SCREEN_ORIENTATION_UNSET
-                            && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND,
-                    r, false /* includeBoundary */, true /* traverseTopToBottom */);
+                    a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
+                    r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
             if (nextCandidate != null) {
                 r = nextCandidate;
             }
@@ -2110,6 +2116,10 @@
                 w.seamlesslyRotateIfAllowed(transaction, oldRotation, rotation, rotateSeamlessly);
             }, true /* traverseTopToBottom */);
             mPinnedTaskController.startSeamlessRotationIfNeeded(transaction, oldRotation, rotation);
+            if (!mDisplayRotation.hasSeamlessRotatingWindow()) {
+                // Make sure DisplayRotation#isRotatingSeamlessly() will return false.
+                mDisplayRotation.cancelSeamlessRotation();
+            }
         }
 
         mWmService.mDisplayManagerInternal.performTraversal(transaction);
@@ -2723,6 +2733,15 @@
         final int orientation = super.getOrientation();
 
         if (!handlesOrientationChangeFromDescendant(orientation)) {
+            ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true);
+            if (topActivity != null && topActivity.mLetterboxUiController
+                    .shouldUseDisplayLandscapeNaturalOrientation()) {
+                ProtoLog.v(WM_DEBUG_ORIENTATION,
+                        "Display id=%d is ignoring orientation request for %d, return %d"
+                        + " following a per-app override for %s",
+                        mDisplayId, orientation, SCREEN_ORIENTATION_LANDSCAPE, topActivity);
+                return SCREEN_ORIENTATION_LANDSCAPE;
+            }
             mLastOrientationSource = null;
             // Return SCREEN_ORIENTATION_UNSPECIFIED so that Display respect sensor rotation
             ProtoLog.v(WM_DEBUG_ORIENTATION,
@@ -2974,7 +2993,7 @@
         if (density == mInitialDisplayDensity) {
             density = 0;
         }
-        mWmService.mDisplayWindowSettings.setForcedDensity(this, density, userId);
+        mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId);
     }
 
     /** @param mode {@link #FORCE_SCALING_MODE_AUTO} or {@link #FORCE_SCALING_MODE_DISABLED}. */
@@ -3080,27 +3099,6 @@
     }
 
     /**
-     * Returns true if the input point is within an app window.
-     */
-    boolean pointWithinAppWindow(int x, int y) {
-        final int[] targetWindowType = {-1};
-        final PooledConsumer fn = PooledLambda.obtainConsumer((w, nonArg) -> {
-            if (targetWindowType[0] != -1) {
-                return;
-            }
-
-            if (w.isOnScreen() && w.isVisible() && w.getFrame().contains(x, y)) {
-                targetWindowType[0] = w.mAttrs.type;
-                return;
-            }
-        }, PooledLambda.__(WindowState.class), mTmpRect);
-        forAllWindows(fn, true /* traverseTopToBottom */);
-        fn.recycle();
-        return FIRST_APPLICATION_WINDOW <= targetWindowType[0]
-                && targetWindowType[0] <= LAST_APPLICATION_WINDOW;
-    }
-
-    /**
      * Find the task whose outside touch area (for resizing) (x, y) falls within.
      * Returns null if the touch doesn't fall into a resizing area.
      */
@@ -4507,24 +4505,8 @@
      */
     @VisibleForTesting
     SurfaceControl computeImeParent() {
-        if (mImeLayeringTarget != null) {
-            // Ensure changing the IME parent when the layering target that may use IME has
-            // became to the input target for preventing IME flickers.
-            // Note that:
-            // 1) For the imeLayeringTarget that may not use IME but requires IME on top
-            // of it (e.g. an overlay window with NOT_FOCUSABLE|ALT_FOCUSABLE_IM flags), we allow
-            // it to re-parent the IME on top the display to keep the legacy behavior.
-            // 2) Even though the starting window won't use IME, the associated activity
-            // behind the starting window may request the input. If so, then we should still hold
-            // the IME parent change until the activity started the input.
-            boolean imeLayeringTargetMayUseIme =
-                    LayoutParams.mayUseInputMethod(mImeLayeringTarget.mAttrs.flags)
-                    || mImeLayeringTarget.mAttrs.type == TYPE_APPLICATION_STARTING;
-            if (imeLayeringTargetMayUseIme && (mImeInputTarget == null
-                    || mImeLayeringTarget.mActivityRecord != mImeInputTarget.getActivityRecord())) {
-                // Do not change parent if the window hasn't requested IME.
-                return null;
-            }
+        if (!canComputeImeParent(mImeLayeringTarget, mImeInputTarget)) {
+            return null;
         }
         // Attach it to app if the target is part of an app and such app is covering the entire
         // screen. If it's not covering the entire screen the IME might extend beyond the apps
@@ -4537,6 +4519,76 @@
                 ? mImeWindowsContainer.getParent().getSurfaceControl() : null;
     }
 
+    private static boolean canComputeImeParent(@Nullable WindowState imeLayeringTarget,
+            @Nullable InputTarget imeInputTarget) {
+        if (imeLayeringTarget == null) {
+            return false;
+        }
+        if (shouldComputeImeParentForEmbeddedActivity(imeLayeringTarget, imeInputTarget)) {
+            return true;
+        }
+        // Ensure changing the IME parent when the layering target that may use IME has
+        // became to the input target for preventing IME flickers.
+        // Note that:
+        // 1) For the imeLayeringTarget that may not use IME but requires IME on top
+        // of it (e.g. an overlay window with NOT_FOCUSABLE|ALT_FOCUSABLE_IM flags), we allow
+        // it to re-parent the IME on top the display to keep the legacy behavior.
+        // 2) Even though the starting window won't use IME, the associated activity
+        // behind the starting window may request the input. If so, then we should still hold
+        // the IME parent change until the activity started the input.
+        boolean imeLayeringTargetMayUseIme =
+                LayoutParams.mayUseInputMethod(imeLayeringTarget.mAttrs.flags)
+                        || imeLayeringTarget.mAttrs.type == TYPE_APPLICATION_STARTING;
+
+        if (imeLayeringTargetMayUseIme && (imeInputTarget == null
+                || imeLayeringTarget.mActivityRecord != imeInputTarget.getActivityRecord())) {
+            // Do not change parent if the window hasn't requested IME.
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Called from {@link #computeImeParent()} to check the given IME targets if the IME surface
+     * parent should be updated in ActivityEmbeddings.
+     *
+     * As the IME layering target is calculated according to the window hierarchy by
+     * {@link #computeImeTarget}, the layering target and input target may be different when the
+     * window hasn't started input connection, WindowManagerService hasn't yet received the
+     * input target which reported from InputMethodManagerService. To make the IME surface will be
+     * shown on the best fit IME layering target, we basically won't update IME parent until both
+     * IME input and layering target updated for better IME transition.
+     *
+     * However, in activity embedding, tapping a window won't update it to the top window so the
+     * calculated IME layering target may higher than input target. Update IME parent for this case.
+     *
+     * @return {@code true} means the layer of IME layering target is higher than the input target
+     * and {@link #computeImeParent()} should keep progressing to update the IME
+     * surface parent on the display in case the IME surface left behind.
+     */
+    private static boolean shouldComputeImeParentForEmbeddedActivity(
+            @Nullable WindowState imeLayeringTarget, @Nullable InputTarget imeInputTarget) {
+        if (imeInputTarget == null || imeLayeringTarget == null) {
+            return false;
+        }
+        final WindowState inputTargetWindow = imeInputTarget.getWindowState();
+        if (inputTargetWindow == null || !imeLayeringTarget.isAttached()
+                || !inputTargetWindow.isAttached()) {
+            return false;
+        }
+
+        final ActivityRecord inputTargetRecord = imeInputTarget.getActivityRecord();
+        final ActivityRecord layeringTargetRecord = imeLayeringTarget.getActivityRecord();
+        if (inputTargetRecord == null || layeringTargetRecord == null
+                || inputTargetRecord == layeringTargetRecord
+                || inputTargetRecord.getTask() != layeringTargetRecord.getTask()
+                || !inputTargetRecord.isEmbedded() || !layeringTargetRecord.isEmbedded()) {
+            // Check whether the input target and layering target are embedded in the same Task.
+            return false;
+        }
+        return imeLayeringTarget.compareTo(inputTargetWindow) > 0;
+    }
+
     void setLayoutNeeded() {
         if (DEBUG_LAYOUT) Slog.w(TAG_WM, "setLayoutNeeded: callers=" + Debug.getCallers(3));
         mLayoutNeeded = true;
@@ -4809,7 +4861,7 @@
         mInsetsStateController.getImeSourceProvider().checkShowImePostLayout();
 
         mLastHasContent = mTmpApplySurfaceChangesTransactionState.displayHasContent;
-        if (!mWmService.mDisplayFrozen) {
+        if (!mWmService.mDisplayFrozen && !mDisplayRotation.isRotatingSeamlessly()) {
             mWmService.mDisplayManagerInternal.setDisplayProperties(mDisplayId,
                     mLastHasContent,
                     mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 0bb4022..acc096d 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -142,6 +142,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.statusbar.LetterboxDetails;
 import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
 import com.android.internal.util.function.TriConsumer;
 import com.android.internal.view.AppearanceRegion;
 import com.android.internal.widget.PointerLocationView;
@@ -1576,9 +1577,8 @@
         applyKeyguardPolicy(win, imeTarget);
 
         // Check if the freeform window overlaps with the navigation bar area.
-        final boolean isOverlappingWithNavBar = isOverlappingWithNavBar(win);
-        if (isOverlappingWithNavBar && !mIsFreeformWindowOverlappingWithNavBar
-                && win.inFreeformWindowingMode()) {
+        if (!mIsFreeformWindowOverlappingWithNavBar && win.inFreeformWindowingMode()
+                && win.mActivityRecord != null && isOverlappingWithNavBar(win)) {
             mIsFreeformWindowOverlappingWithNavBar = true;
         }
 
@@ -1636,7 +1636,7 @@
             // mode; if it's in gesture navigation mode, the navigation bar will be
             // NAV_BAR_FORCE_TRANSPARENT and its appearance won't be decided by overlapping
             // windows.
-            if (isOverlappingWithNavBar) {
+            if (isOverlappingWithNavBar(win)) {
                 if (mNavBarColorWindowCandidate == null) {
                     mNavBarColorWindowCandidate = win;
                     addSystemBarColorApp(win);
@@ -1664,7 +1664,7 @@
                     addSystemBarColorApp(win);
                 }
             }
-            if (isOverlappingWithNavBar && mNavBarColorWindowCandidate == null) {
+            if (isOverlappingWithNavBar(win) && mNavBarColorWindowCandidate == null) {
                 mNavBarColorWindowCandidate = win;
             }
         }
@@ -2006,7 +2006,8 @@
                 dc.getDisplayPolicy().simulateLayoutDisplay(df);
                 final InsetsState insetsState = df.mInsetsState;
                 final Rect displayFrame = insetsState.getDisplayFrame();
-                final Insets decor = calculateDecorInsetsWithInternalTypes(insetsState);
+                final Insets decor = insetsState.calculateInsets(displayFrame, DECOR_TYPES,
+                        true /* ignoreVisibility */);
                 final Insets statusBar = insetsState.calculateInsets(displayFrame,
                         Type.statusBars(), true /* ignoreVisibility */);
                 mNonDecorInsets.set(decor.left, decor.top, decor.right, decor.bottom);
@@ -2038,17 +2039,8 @@
             }
         }
 
-        // TODO (b/235842600): Use public type once we can treat task bar as navigation bar.
-        static final int[] INTERNAL_DECOR_TYPES;
-        static {
-            final ArraySet<Integer> decorTypes = InsetsState.toInternalType(
-                    Type.displayCutout() | Type.navigationBars());
-            decorTypes.remove(ITYPE_EXTRA_NAVIGATION_BAR);
-            INTERNAL_DECOR_TYPES = new int[decorTypes.size()];
-            for (int i = 0; i < INTERNAL_DECOR_TYPES.length; i++) {
-                INTERNAL_DECOR_TYPES[i] = decorTypes.valueAt(i);
-            }
-        }
+
+        static final int DECOR_TYPES = Type.displayCutout() | Type.navigationBars();
 
         private final DisplayContent mDisplayContent;
         private final Info[] mInfoForRotation = new Info[4];
@@ -2075,20 +2067,6 @@
                 info.mNeedUpdate = true;
             }
         }
-
-        // TODO (b/235842600): Remove this method once we can treat task bar as navigation bar.
-        private static Insets calculateDecorInsetsWithInternalTypes(InsetsState state) {
-            final Rect frame = state.getDisplayFrame();
-            Insets insets = Insets.NONE;
-            for (int i = INTERNAL_DECOR_TYPES.length - 1; i >= 0; i--) {
-                final InsetsSource source = state.peekSource(INTERNAL_DECOR_TYPES[i]);
-                if (source != null) {
-                    insets = Insets.max(source.calculateInsets(frame, true /* ignoreVisibility */),
-                            insets);
-                }
-            }
-            return insets;
-        }
     }
 
     /**
@@ -2221,7 +2199,7 @@
      * Called when an app has started replacing its main window.
      */
     void addRelaunchingApp(ActivityRecord app) {
-        if (mSystemBarColorApps.contains(app)) {
+        if (mSystemBarColorApps.contains(app) && !app.hasStartingWindow()) {
             mRelaunchingSystemBarColorApps.add(app);
         }
     }
@@ -2666,8 +2644,9 @@
      */
     public void takeScreenshot(int screenshotType, int source) {
         if (mScreenshotHelper != null) {
-            mScreenshotHelper.takeScreenshot(screenshotType,
-                    source, mHandler, null /* completionConsumer */);
+            ScreenshotRequest request =
+                    new ScreenshotRequest.Builder(screenshotType, source).build();
+            mScreenshotHelper.takeScreenshot(request, mHandler, null /* completionConsumer */);
         }
     }
 
@@ -2823,6 +2802,7 @@
         lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+        lp.privateFlags |= LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
         lp.setFitInsetsTypes(0);
         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
         if (ActivityManager.isHighEndGfx()) {
@@ -2878,7 +2858,7 @@
 
     @VisibleForTesting
     static boolean isOverlappingWithNavBar(@NonNull WindowState win) {
-        if (win.mActivityRecord == null || !win.isVisible()) {
+        if (!win.isVisible()) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index e6d8b3d..3404279 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -41,6 +41,7 @@
 
 import android.annotation.AnimRes;
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.ContentResolver;
@@ -117,6 +118,8 @@
     private SettingsObserver mSettingsObserver;
     @Nullable
     private FoldController mFoldController;
+    @NonNull
+    private final DeviceStateController mDeviceStateController;
 
     @ScreenOrientation
     private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -218,21 +221,24 @@
     private boolean mDemoRotationLock;
 
     DisplayRotation(WindowManagerService service, DisplayContent displayContent,
-            DisplayAddress displayAddress) {
+            DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController) {
         this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(),
-                service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock());
+                service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock(),
+                deviceStateController);
     }
 
     @VisibleForTesting
     DisplayRotation(WindowManagerService service, DisplayContent displayContent,
             DisplayAddress displayAddress, DisplayPolicy displayPolicy,
-            DisplayWindowSettings displayWindowSettings, Context context, Object lock) {
+            DisplayWindowSettings displayWindowSettings, Context context, Object lock,
+            @NonNull DeviceStateController deviceStateController) {
         mService = service;
         mDisplayContent = displayContent;
         mDisplayPolicy = displayPolicy;
         mDisplayWindowSettings = displayWindowSettings;
         mContext = context;
         mLock = lock;
+        mDeviceStateController = deviceStateController;
         isDefaultDisplay = displayContent.isDefaultDisplay;
         mCompatPolicyForImmersiveApps = initImmersiveAppCompatPolicy(service, displayContent);
 
@@ -243,11 +249,13 @@
         mDeskDockRotation = readRotation(R.integer.config_deskDockRotation);
         mUndockedHdmiRotation = readRotation(R.integer.config_undockedHdmiRotation);
 
-        mRotation = readDefaultDisplayRotation(displayAddress);
+        int defaultRotation = readDefaultDisplayRotation(displayAddress);
+        mRotation = defaultRotation;
 
         if (isDefaultDisplay) {
             final Handler uiHandler = UiThread.getHandler();
-            mOrientationListener = new OrientationListener(mContext, uiHandler);
+            mOrientationListener =
+                    new OrientationListener(mContext, uiHandler, defaultRotation);
             mOrientationListener.setCurrentRotation(mRotation);
             mSettingsObserver = new SettingsObserver(uiHandler);
             mSettingsObserver.observe();
@@ -1137,6 +1145,15 @@
         int sensorRotation = mOrientationListener != null
                 ? mOrientationListener.getProposedRotation() // may be -1
                 : -1;
+        if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) {
+            // Flipping 270 and 90 has the same effect as changing the direction which rotation is
+            // applied.
+            if (sensorRotation == Surface.ROTATION_90) {
+                sensorRotation = Surface.ROTATION_270;
+            } else if (sensorRotation == Surface.ROTATION_270) {
+                sensorRotation = Surface.ROTATION_90;
+            }
+        }
         mLastSensorRotation = sensorRotation;
         if (sensorRotation < 0) {
             sensorRotation = lastRotation;
@@ -1573,7 +1590,7 @@
         proto.end(token);
     }
 
-    boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+    boolean isDeviceInPosture(DeviceStateController.DeviceState state, boolean isTabletop) {
         if (mFoldController == null) return false;
         return mFoldController.isDeviceInPosture(state, isTabletop);
     }
@@ -1585,10 +1602,10 @@
     /**
      * Called by the DeviceStateManager callback when the device state changes.
      */
-    void foldStateChanged(DeviceStateController.FoldState foldState) {
+    void foldStateChanged(DeviceStateController.DeviceState deviceState) {
         if (mFoldController != null) {
             synchronized (mLock) {
-                mFoldController.foldStateChanged(foldState);
+                mFoldController.foldStateChanged(deviceState);
             }
         }
     }
@@ -1596,8 +1613,8 @@
     private class FoldController {
         @Surface.Rotation
         private int mHalfFoldSavedRotation = -1; // No saved rotation
-        private DeviceStateController.FoldState mFoldState =
-                DeviceStateController.FoldState.UNKNOWN;
+        private DeviceStateController.DeviceState mDeviceState =
+                DeviceStateController.DeviceState.UNKNOWN;
         private boolean mInHalfFoldTransition = false;
         private final boolean mIsDisplayAlwaysSeparatingHinge;
         private final Set<Integer> mTabletopRotations;
@@ -1637,32 +1654,33 @@
                     R.bool.config_isDisplayHingeAlwaysSeparating);
         }
 
-        boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
-            if (state != mFoldState) {
+        boolean isDeviceInPosture(DeviceStateController.DeviceState state, boolean isTabletop) {
+            if (state != mDeviceState) {
                 return false;
             }
-            if (mFoldState == DeviceStateController.FoldState.HALF_FOLDED) {
+            if (mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED) {
                 return !(isTabletop ^ mTabletopRotations.contains(mRotation));
             }
             return true;
         }
 
-        DeviceStateController.FoldState getFoldState() {
-            return mFoldState;
+        DeviceStateController.DeviceState getFoldState() {
+            return mDeviceState;
         }
 
         boolean isSeparatingHinge() {
-            return mFoldState == DeviceStateController.FoldState.HALF_FOLDED
-                    || (mFoldState == DeviceStateController.FoldState.OPEN
+            return mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED
+                    || (mDeviceState == DeviceStateController.DeviceState.OPEN
                         && mIsDisplayAlwaysSeparatingHinge);
         }
 
         boolean overrideFrozenRotation() {
-            return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
+            return mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED;
         }
 
         boolean shouldRevertOverriddenRotation() {
-            return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+            // When transitioning to open.
+            return mDeviceState == DeviceStateController.DeviceState.OPEN
                     && mInHalfFoldTransition
                     && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
                     && mUserRotationMode
@@ -1676,30 +1694,30 @@
             return savedRotation;
         }
 
-        void foldStateChanged(DeviceStateController.FoldState newState) {
+        void foldStateChanged(DeviceStateController.DeviceState newState) {
             ProtoLog.v(WM_DEBUG_ORIENTATION,
                     "foldStateChanged: displayId %d, halfFoldStateChanged %s, "
                     + "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, "
                     + "mLastOrientation: %d, mRotation: %d",
                     mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation,
                     mUserRotation, mLastSensorRotation, mLastOrientation, mRotation);
-            if (mFoldState == DeviceStateController.FoldState.UNKNOWN) {
-                mFoldState = newState;
+            if (mDeviceState == DeviceStateController.DeviceState.UNKNOWN) {
+                mDeviceState = newState;
                 return;
             }
-            if (newState == DeviceStateController.FoldState.HALF_FOLDED
-                    && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) {
+            if (newState == DeviceStateController.DeviceState.HALF_FOLDED
+                    && mDeviceState != DeviceStateController.DeviceState.HALF_FOLDED) {
                 // The device has transitioned to HALF_FOLDED state: save the current rotation and
                 // update the device rotation.
                 mHalfFoldSavedRotation = mRotation;
-                mFoldState = newState;
+                mDeviceState = newState;
                 // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will
                 // return true, so rotation is unlocked.
                 mService.updateRotation(false /* alwaysSendConfiguration */,
                         false /* forceRelayout */);
             } else {
                 mInHalfFoldTransition = true;
-                mFoldState = newState;
+                mDeviceState = newState;
                 // Tell the device to update its orientation.
                 mService.updateRotation(false /* alwaysSendConfiguration */,
                         false /* forceRelayout */);
@@ -1719,8 +1737,9 @@
     private class OrientationListener extends WindowOrientationListener implements Runnable {
         transient boolean mEnabled;
 
-        OrientationListener(Context context, Handler handler) {
-            super(context, handler);
+        OrientationListener(Context context, Handler handler,
+                @Surface.Rotation int defaultRotation) {
+            super(context, handler, defaultRotation);
         }
 
         @Override
@@ -1822,7 +1841,7 @@
             final long mTimestamp = System.currentTimeMillis();
             final int mHalfFoldSavedRotation;
             final boolean mInHalfFoldTransition;
-            final DeviceStateController.FoldState mFoldState;
+            final DeviceStateController.DeviceState mDeviceState;
             @Nullable final String mDisplayRotationCompatPolicySummary;
 
             Record(DisplayRotation dr, int fromRotation, int toRotation) {
@@ -1843,8 +1862,9 @@
                 if (source != null) {
                     mLastOrientationSource = source.toString();
                     final WindowState w = source.asWindowState();
-                    mSourceOrientation =
-                            w != null ? w.mAttrs.screenOrientation : source.mOrientation;
+                    mSourceOrientation = w != null
+                            ? w.mAttrs.screenOrientation
+                            : source.getOverrideOrientation();
                 } else {
                     mLastOrientationSource = null;
                     mSourceOrientation = SCREEN_ORIENTATION_UNSET;
@@ -1852,11 +1872,11 @@
                 if (dr.mFoldController != null) {
                     mHalfFoldSavedRotation = dr.mFoldController.mHalfFoldSavedRotation;
                     mInHalfFoldTransition = dr.mFoldController.mInHalfFoldTransition;
-                    mFoldState = dr.mFoldController.mFoldState;
+                    mDeviceState = dr.mFoldController.mDeviceState;
                 } else {
                     mHalfFoldSavedRotation = NO_FOLD_CONTROLLER;
                     mInHalfFoldTransition = false;
-                    mFoldState = DeviceStateController.FoldState.UNKNOWN;
+                    mDeviceState = DeviceStateController.DeviceState.UNKNOWN;
                 }
                 mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
                         ? null
@@ -1882,7 +1902,7 @@
                     pw.println(prefix + " halfFoldSavedRotation="
                             + mHalfFoldSavedRotation
                             + " mInHalfFoldTransition=" + mInHalfFoldTransition
-                            + " mFoldState=" + mFoldState);
+                            + " mFoldState=" + mDeviceState);
                 }
                 if (mDisplayRotationCompatPolicySummary != null) {
                     pw.println(prefix + mDisplayRotationCompatPolicySummary);
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index ba0413d..47bdba3 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -34,6 +36,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.StringRes;
 import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.RefreshCallbackItem;
 import android.app.servertransaction.ResumeActivityItem;
@@ -44,10 +47,13 @@
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.widget.Toast;
 
+import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.server.UiThread;
 
 import java.util.Map;
 import java.util.Set;
@@ -203,8 +209,11 @@
                 || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
             return;
         }
-        boolean cycleThroughStop = mWmService.mLetterboxConfiguration
-                .isCameraCompatRefreshCycleThroughStopEnabled();
+        boolean cycleThroughStop =
+                mWmService.mLetterboxConfiguration
+                        .isCameraCompatRefreshCycleThroughStopEnabled()
+                && !activity.mLetterboxUiController
+                        .shouldRefreshActivityViaPauseForCameraCompat();
         try {
             activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
             ProtoLog.v(WM_DEBUG_STATES,
@@ -229,6 +238,24 @@
         activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
     }
 
+    /**
+     * Notifies that animation in {@link ScreenAnimationRotation} has finished.
+     *
+     * <p>This class uses this signal as a trigger for notifying the user about forced rotation
+     * reason with the {@link Toast}.
+     */
+    void onScreenRotationAnimationFinished() {
+        if (!isTreatmentEnabledForDisplay() || mCameraIdPackageBiMap.isEmpty()) {
+            return;
+        }
+        ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                    /* considerKeyguardState= */ true);
+        if (!isTreatmentEnabledForActivity(topActivity)) {
+            return;
+        }
+        showToast(R.string.display_rotation_camera_compat_toast_after_rotation);
+    }
+
     String getSummaryForDisplayRotationHistoryRecord() {
         String summaryIfEnabled = "";
         if (isTreatmentEnabledForDisplay()) {
@@ -255,7 +282,8 @@
             Configuration lastReportedConfig) {
         return newConfig.windowConfiguration.getDisplayRotation()
                         != lastReportedConfig.windowConfiguration.getDisplayRotation()
-                && isTreatmentEnabledForActivity(activity);
+                && isTreatmentEnabledForActivity(activity)
+                && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat();
     }
 
     /**
@@ -277,24 +305,42 @@
                 && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
     }
 
+    boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord 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 && !activity.inMultiWindowMode()
+        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.
-                && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
-                && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
-                && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+                && activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR
+                && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED;
+    }
+
+    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();
     }
 
     private synchronized void notifyCameraOpened(
@@ -329,7 +375,32 @@
             }
             mCameraIdPackageBiMap.put(packageName, cameraId);
         }
-        updateOrientationWithWmLock();
+        ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                    /* considerKeyguardState= */ true);
+        if (topActivity == null || topActivity.getTask() == null) {
+            return;
+        }
+        // Checking whether an activity in fullscreen rather than the task as this camera compat
+        // treatment doesn't cover activity embedding.
+        if (topActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+            if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
+                topActivity.recomputeConfiguration();
+            }
+            updateOrientationWithWmLock();
+            return;
+        }
+        // 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
+                && isTreatmentEnabledForActivity(topActivity, /* mustBeFullscreen */ false)) {
+            showToast(R.string.display_rotation_camera_compat_toast_in_split_screen);
+        }
+    }
+
+    @VisibleForTesting
+    void showToast(@StringRes int stringRes) {
+        UiThread.getHandler().post(
+                () -> Toast.makeText(mWmService.mContext, stringRes, Toast.LENGTH_LONG).show());
     }
 
     private synchronized void notifyCameraClosed(@NonNull String cameraId) {
@@ -370,6 +441,17 @@
         ProtoLog.v(WM_DEBUG_ORIENTATION,
                 "Display id=%d is notified that Camera %s is closed, updating rotation.",
                 mDisplayContent.mDisplayId, cameraId);
+        ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                /* considerKeyguardState= */ true);
+        if (topActivity == null
+                // Checking whether an activity in fullscreen rather than the task as this camera
+                // compat treatment doesn't cover activity embedding.
+                || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+            return;
+        }
+        if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
+            topActivity.recomputeConfiguration();
+        }
         updateOrientationWithWmLock();
     }
 
@@ -391,6 +473,10 @@
         private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
         private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
 
+        boolean isEmpty() {
+            return mCameraIdToPackageMap.isEmpty();
+        }
+
         void put(String packageName, String cameraId) {
             // Always using the last connected camera ID for the package even for the concurrent
             // camera use case since we can't guess which camera is more important anyway.
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 8763900..2e8c9ac 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -77,14 +77,14 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setForcedDensity(DisplayContent displayContent, int density, int userId) {
-        if (displayContent.isDefaultDisplay) {
+    void setForcedDensity(DisplayInfo info, int density, int userId) {
+        if (info.displayId == Display.DEFAULT_DISPLAY) {
             final String densityString = density == 0 ? "" : Integer.toString(density);
             Settings.Secure.putStringForUser(mService.mContext.getContentResolver(),
                     Settings.Secure.DISPLAY_DENSITY_FORCED, densityString, userId);
         }
 
-        final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+        final DisplayInfo displayInfo = info;
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
         overrideSettings.mForcedDensity = density;
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 48258a1..1d21b9d 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -403,8 +403,10 @@
             return;
         }
 
-        mWindowManager.mPolicy.onKeyguardOccludedChangedLw(isDisplayOccluded(DEFAULT_DISPLAY));
-        if (isKeyguardLocked(displayId)) {
+        final boolean waitAppTransition = isKeyguardLocked(displayId);
+        mWindowManager.mPolicy.onKeyguardOccludedChangedLw(isDisplayOccluded(DEFAULT_DISPLAY),
+                waitAppTransition);
+        if (waitAppTransition) {
             mService.deferWindowLayout();
             try {
                 mRootWindowContainer.getDefaultDisplay()
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index f916ee4..fa49a6ba 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -18,14 +18,16 @@
 
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ALLOW_IGNORE_ORIENTATION_REQUEST;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_CAMERA_COMPAT_TREATMENT;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_COMPAT_FAKE_FOCUS;
 import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
 import android.graphics.Color;
 import android.provider.DeviceConfig;
 import android.util.Slog;
@@ -42,10 +44,6 @@
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM;
 
-    @VisibleForTesting
-    static final String DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS =
-            "enable_compat_fake_focus";
-
     /**
      * Override of aspect ratio for fixed orientation letterboxing that is set via ADB with
      * set-fixed-orientation-letterbox-aspect-ratio or via {@link
@@ -115,12 +113,6 @@
     /** Letterboxed app window is aligned to the right side. */
     static final int LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM = 2;
 
-    @VisibleForTesting
-    static final String PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN = "com.android.COMPAT_FAKE_FOCUS_OPT_IN";
-    @VisibleForTesting
-    static final String PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT =
-            "com.android.COMPAT_FAKE_FOCUS_OPT_OUT";
-
     final Context mContext;
 
     // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
@@ -197,6 +189,12 @@
     // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps.
     private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled;
 
+    // Whether using display aspect ratio as a default aspect ratio for all letterboxed apps.
+    // mIsSplitScreenAspectRatioForUnresizableAppsEnabled and
+    // config_letterboxDefaultMinAspectRatioForUnresizableApps take priority over this for
+    // unresizable apps
+    private boolean mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
+
     // Whether letterboxing strategy is enabled for translucent activities. If {@value false}
     // all the feature is disabled
     private boolean mTranslucentLetterboxingEnabled;
@@ -288,6 +286,9 @@
                 R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps));
         mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
+        mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = mContext.getResources()
+                .getBoolean(R.bool
+                        .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
         mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsEnabledForTranslucentActivities);
         mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
@@ -296,18 +297,36 @@
                 R.bool.config_isCompatFakeFocusEnabled);
         mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
-
         mIsDisplayRotationImmersiveAppCompatPolicyEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled);
         mDeviceConfig.updateFlagActiveStatus(
+                /* isActive */ mIsCameraCompatTreatmentEnabled,
+                /* key */ KEY_ENABLE_CAMERA_COMPAT_TREATMENT);
+        mDeviceConfig.updateFlagActiveStatus(
                 /* isActive */ mIsDisplayRotationImmersiveAppCompatPolicyEnabled,
                 /* key */ KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY);
-
+        mDeviceConfig.updateFlagActiveStatus(
+                /* isActive */ true,
+                /* key */ KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
+        mDeviceConfig.updateFlagActiveStatus(
+                /* isActive */ mIsCompatFakeFocusEnabled,
+                /* key */ KEY_ENABLE_COMPAT_FAKE_FOCUS);
+        mDeviceConfig.updateFlagActiveStatus(
+                /* isActive */ mTranslucentLetterboxingEnabled,
+                /* key */ KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY);
         mLetterboxConfigurationPersister = letterboxConfigurationPersister;
         mLetterboxConfigurationPersister.start();
     }
 
     /**
+     * Whether enabling ignoreOrientationRequest is allowed on the device. This value is controlled
+     * via {@link android.provider.DeviceConfig}.
+     */
+    boolean isIgnoreOrientationRequestAllowed() {
+        return mDeviceConfig.getFlag(KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
+    }
+
+    /**
      * Overrides the aspect ratio of letterbox for fixed orientation. If given value is <= {@link
      * #MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO}, both it and a value of {@link
      * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and
@@ -943,6 +962,13 @@
     }
 
     /**
+     * Whether using display aspect ratio as a default aspect ratio for all letterboxed apps.
+     */
+    boolean getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox() {
+        return mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
+    }
+
+    /**
      * Overrides whether using split screen aspect ratio as a default aspect ratio for unresizable
      * apps.
      */
@@ -951,6 +977,14 @@
     }
 
     /**
+     * Overrides whether using display aspect ratio as a default aspect ratio for all letterboxed
+     * apps.
+     */
+    void setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(boolean enabled) {
+        mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = enabled;
+    }
+
+    /**
      * Resets whether using split screen aspect ratio as a default aspect ratio for unresizable
      * apps {@link R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled}.
      */
@@ -959,9 +993,19 @@
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
     }
 
+    /**
+     * Resets whether using display aspect ratio as a default aspect ratio for all letterboxed
+     * apps {@link R.bool.config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled}.
+     */
+    void resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox() {
+        mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = mContext.getResources()
+                .getBoolean(R.bool
+                        .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
+    }
+
     boolean isTranslucentLetterboxingEnabled() {
         return mTranslucentLetterboxingOverrideEnabled || (mTranslucentLetterboxingEnabled
-                && isTranslucentLetterboxingAllowed());
+                && mDeviceConfig.getFlag(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY));
     }
 
     void setTranslucentLetterboxingEnabled(boolean translucentLetterboxingEnabled) {
@@ -1009,48 +1053,9 @@
                 isDeviceInTabletopMode, nextVerticalPosition);
     }
 
-    // TODO(b/262378106): Cache a runtime flag and implement
-    // DeviceConfig.OnPropertiesChangedListener
-    static boolean isTranslucentLetterboxingAllowed() {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                "enable_translucent_activity_letterbox", false);
-    }
-
-    @VisibleForTesting
-    boolean getPackageManagerProperty(PackageManager pm, String property) {
-        boolean enabled = false;
-        try {
-            final PackageManager.Property p = pm.getProperty(property, mContext.getPackageName());
-            enabled = p.getBoolean();
-        } catch (PackageManager.NameNotFoundException e) {
-            // Property not found
-        }
-        return enabled;
-    }
-
-    @VisibleForTesting
-    boolean isCompatFakeFocusEnabled(ActivityInfo info) {
-        if (!isCompatFakeFocusEnabledOnDevice()) {
-            return false;
-        }
-        // See if the developer has chosen to opt in / out of treatment
-        PackageManager pm = mContext.getPackageManager();
-        if (getPackageManagerProperty(pm, PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT)) {
-            return false;
-        } else if (getPackageManagerProperty(pm, PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN)) {
-            return true;
-        }
-        if (info.isChangeEnabled(ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS)) {
-            return true;
-        }
-        return false;
-    }
-
     /** Whether fake sending focus is enabled for unfocused apps in splitscreen */
-    boolean isCompatFakeFocusEnabledOnDevice() {
-        return mIsCompatFakeFocusEnabled
-                && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                        DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, true);
+    boolean isCompatFakeFocusEnabled() {
+        return mIsCompatFakeFocusEnabled && mDeviceConfig.getFlag(KEY_ENABLE_COMPAT_FAKE_FOCUS);
     }
 
     /**
@@ -1072,15 +1077,8 @@
 
     /** Whether camera compatibility treatment is enabled. */
     boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
-        return mIsCameraCompatTreatmentEnabled
-                && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
-    }
-
-    // TODO(b/262977416): Cache a runtime flag and implement
-    // DeviceConfig.OnPropertiesChangedListener
-    private static boolean isCameraCompatTreatmentAllowed() {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                "enable_compat_camera_treatment", true);
+        return mIsCameraCompatTreatmentEnabled && (!checkDeviceConfig
+                || mDeviceConfig.getFlag(KEY_ENABLE_CAMERA_COMPAT_TREATMENT));
     }
 
     /** Whether camera compatibility refresh is enabled. */
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
index cf123a1..df3c8f0 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
@@ -33,17 +33,45 @@
 final class LetterboxConfigurationDeviceConfig
         implements DeviceConfig.OnPropertiesChangedListener {
 
+    static final String KEY_ENABLE_CAMERA_COMPAT_TREATMENT = "enable_compat_camera_treatment";
+    private static final boolean DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT = true;
+
     static final String KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
             "enable_display_rotation_immersive_app_compat_policy";
     private static final boolean DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
             true;
 
+    static final String KEY_ALLOW_IGNORE_ORIENTATION_REQUEST =
+            "allow_ignore_orientation_request";
+    private static final boolean DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST = true;
+
+    static final String KEY_ENABLE_COMPAT_FAKE_FOCUS = "enable_compat_fake_focus";
+    private static final boolean DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS = true;
+
+    static final String KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY =
+            "enable_letterbox_translucent_activity";
+
+    private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY = true;
+
     @VisibleForTesting
     static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of(
+            KEY_ENABLE_CAMERA_COMPAT_TREATMENT,
+            DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT,
             KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
-            DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY
+            DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
+            KEY_ALLOW_IGNORE_ORIENTATION_REQUEST,
+            DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST,
+            KEY_ENABLE_COMPAT_FAKE_FOCUS,
+            DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS,
+            KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY,
+            DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY
     );
 
+    // Whether camera compatibility treatment is enabled.
+    // See DisplayRotationCompatPolicy for context.
+    private boolean mIsCameraCompatTreatmentEnabled =
+            DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT;
+
     // Whether enabling rotation compat policy for immersive apps that prevents auto rotation
     // into non-optimal screen orientation while in fullscreen. This is needed because immersive
     // apps, such as games, are often not optimized for all orientations and can have a poor UX
@@ -52,6 +80,19 @@
     private boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
             DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
 
+    // Whether enabling ignoreOrientationRequest is allowed on the device.
+    private boolean mIsAllowIgnoreOrientationRequest =
+            DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST;
+
+    // Whether sending compat fake focus for split screen resumed activities is enabled. This is
+    // needed because some game engines wait to get focus before drawing the content of the app
+    // which isn't guaranteed by default in multi-window modes.
+    private boolean mIsCompatFakeFocusAllowed = DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS;
+
+    // Whether the letterbox strategy for transparent activities is allowed
+    private boolean mIsTranslucentLetterboxingAllowed =
+            DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY;
+
     // Set of active device configs that need to be updated in
     // DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged.
     private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>();
@@ -91,8 +132,16 @@
      */
     boolean getFlag(String key) {
         switch (key) {
+            case KEY_ENABLE_CAMERA_COMPAT_TREATMENT:
+                return mIsCameraCompatTreatmentEnabled;
             case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
                 return mIsDisplayRotationImmersiveAppCompatPolicyEnabled;
+            case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
+                return mIsAllowIgnoreOrientationRequest;
+            case KEY_ENABLE_COMPAT_FAKE_FOCUS:
+                return mIsCompatFakeFocusAllowed;
+            case KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY:
+                return mIsTranslucentLetterboxingAllowed;
             default:
                 throw new AssertionError("Unexpected flag name: " + key);
         }
@@ -104,10 +153,22 @@
             throw new AssertionError("Haven't found default value for flag: " + key);
         }
         switch (key) {
+            case KEY_ENABLE_CAMERA_COMPAT_TREATMENT:
+                mIsCameraCompatTreatmentEnabled = getDeviceConfig(key, defaultValue);
+                break;
             case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
                 mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
                         getDeviceConfig(key, defaultValue);
                 break;
+            case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
+                mIsAllowIgnoreOrientationRequest = getDeviceConfig(key, defaultValue);
+                break;
+            case KEY_ENABLE_COMPAT_FAKE_FOCUS:
+                mIsCompatFakeFocusAllowed = getDeviceConfig(key, defaultValue);
+                break;
+            case KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY:
+                mIsTranslucentLetterboxingAllowed = getDeviceConfig(key, defaultValue);
+                break;
             default:
                 throw new AssertionError("Unexpected flag name: " + key);
         }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 0c8a645..e1dbe01a 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -17,12 +17,34 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.isFixedOrientation;
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
 import static android.content.pm.ActivityInfo.screenOrientationToString;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
@@ -56,6 +78,10 @@
 import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
 
+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;
@@ -79,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
@@ -89,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;
@@ -99,6 +131,47 @@
 
     private final ActivityRecord mActivityRecord;
 
+    /**
+     * Taskbar expanded height. Used to determine when to crop an app window to display the
+     * rounded corners above the expanded taskbar.
+     */
+    private final float mExpandedTaskBarHeight;
+
+    // TODO(b/265576778): Cache other overrides as well.
+
+    // Corresponds to OVERRIDE_ANY_ORIENTATION
+    private final boolean mIsOverrideAnyOrientationEnabled;
+    // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+    private final boolean mIsOverrideToPortraitOrientationEnabled;
+    // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR
+    private final boolean mIsOverrideToNosensorOrientationEnabled;
+    // Corresponds to OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE
+    private final boolean mIsOverrideToReverseLandscapeOrientationEnabled;
+    // Corresponds to OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA
+    private final boolean mIsOverrideOrientationOnlyForCameraEnabled;
+    // Corresponds to OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION
+    private final boolean mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled;
+    // Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
+    private final boolean mIsOverrideRespectRequestedOrientationEnabled;
+
+    // Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION
+    private final boolean mIsOverrideCameraCompatDisableForceRotationEnabled;
+    // Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH
+    private final boolean mIsOverrideCameraCompatDisableRefreshEnabled;
+    // Corresponds to OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE
+    private final boolean mIsOverrideCameraCompatEnableRefreshViaPauseEnabled;
+
+    // Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION
+    private final boolean mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled;
+
+    // Corresponds to OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS
+    private final boolean mIsOverrideEnableCompatFakeFocusEnabled;
+
+    @Nullable
+    private final Boolean mBooleanPropertyAllowOrientationOverride;
+    @Nullable
+    private final Boolean mBooleanPropertyAllowDisplayOrientationOverride;
+
     /*
      * WindowContainerListener responsible to make translucent activities inherit
      * constraints from the first opaque activity beneath them. It's null for not
@@ -120,18 +193,21 @@
     // The app compat state for the opaque activity if any
     private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
 
-    // If true it means that the opaque activity beneath a translucent one is in SizeCompatMode.
-    private boolean mIsInheritedInSizeCompatMode;
-
-    // This is the SizeCompatScale of the opaque activity beneath a translucent one
-    private float mInheritedSizeCompatScale;
-
     // The CompatDisplayInsets of the opaque activity beneath the translucent one.
     private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
 
     @Nullable
     private Letterbox mLetterbox;
 
+    @Nullable
+    private final Boolean mBooleanPropertyCameraCompatAllowForceRotation;
+
+    @Nullable
+    private final Boolean mBooleanPropertyCameraCompatAllowRefresh;
+
+    @Nullable
+    private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause;
+
     // Whether activity "refresh" was requested but not finished in
     // ActivityRecord#activityResumedLocked following the camera compat force rotation in
     // DisplayRotationCompatPolicy.
@@ -140,6 +216,9 @@
     @Nullable
     private final Boolean mBooleanPropertyIgnoreRequestedOrientation;
 
+    @Nullable
+    private final Boolean mBooleanPropertyFakeFocus;
+
     private boolean mIsRelauchingAfterRequestedOrientationChanged;
 
     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
@@ -154,12 +233,80 @@
                 readComponentProperty(packageManager, mActivityRecord.packageName,
                         mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
                         PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+        mBooleanPropertyFakeFocus =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        mLetterboxConfiguration::isCompatFakeFocusEnabled,
+                        PROPERTY_COMPAT_ENABLE_FAKE_FOCUS);
+        mBooleanPropertyCameraCompatAllowForceRotation =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                                /* checkDeviceConfig */ true),
+                        PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION);
+        mBooleanPropertyCameraCompatAllowRefresh =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                                /* checkDeviceConfig */ true),
+                        PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH);
+        mBooleanPropertyCameraCompatEnableRefreshViaPause =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                                /* checkDeviceConfig */ true),
+                        PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
+
+        mExpandedTaskBarHeight =
+                getResources().getDimensionPixelSize(R.dimen.taskbar_frame_height);
+
+        mBooleanPropertyAllowOrientationOverride =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        /* gatingCondition */ null,
+                        PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+        mBooleanPropertyAllowDisplayOrientationOverride =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        /* gatingCondition */ null,
+                        PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE);
+
+        mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION);
+        mIsOverrideToPortraitOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT);
+        mIsOverrideToReverseLandscapeOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE);
+        mIsOverrideToNosensorOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
+        mIsOverrideOrientationOnlyForCameraEnabled =
+                isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA);
+        mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION);
+        mIsOverrideRespectRequestedOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION);
+
+        mIsOverrideCameraCompatDisableForceRotationEnabled =
+                isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION);
+        mIsOverrideCameraCompatDisableRefreshEnabled =
+                isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH);
+        mIsOverrideCameraCompatEnableRefreshViaPauseEnabled =
+                isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
+
+        mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+        mIsOverrideEnableCompatFakeFocusEnabled =
+                isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS);
     }
 
+    /**
+     * Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code
+     * propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the
+     * property isn't specified for the package.
+     *
+     * <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the
+     * property is unset. Particularly, when this returns {@code null}, {@link
+     * #shouldEnableWithOverrideAndProperty} will check the value of override for the final
+     * decision.
+     */
     @Nullable
     private static Boolean readComponentProperty(PackageManager packageManager, String packageName,
-            BooleanSupplier gatingCondition, String propertyName) {
-        if (!gatingCondition.getAsBoolean()) {
+            @Nullable BooleanSupplier gatingCondition, String propertyName) {
+        if (gatingCondition != null && !gatingCondition.getAsBoolean()) {
             return null;
         }
         try {
@@ -210,15 +357,11 @@
      * </ul>
      */
     boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
-        if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) {
-            return false;
-        }
-        if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) {
-            return false;
-        }
-        if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation)
-                && !mActivityRecord.info.isChangeEnabled(
-                        OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) {
+        if (!shouldEnableWithOverrideAndProperty(
+                /* gatingCondition */ mLetterboxConfiguration
+                        ::isPolicyForIgnoringRequestedOrientationEnabled,
+                mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled,
+                mBooleanPropertyIgnoreRequestedOrientation)) {
             return false;
         }
         if (mIsRelauchingAfterRequestedOrientationChanged) {
@@ -243,6 +386,25 @@
     }
 
     /**
+     * Whether sending compat fake focus for split screen resumed activities is enabled. Needed
+     * because some game engines wait to get focus before drawing the content of the app which isn't
+     * guaranteed by default in multi-window modes.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the treatment is enabled
+     *     <li>Component property is NOT set to false
+     *     <li>Component property is set to true or per-app override is enabled
+     * </ul>
+     */
+    boolean shouldSendFakeFocus() {
+        return shouldEnableWithOverrideAndProperty(
+                /* gatingCondition */ mLetterboxConfiguration::isCompatFakeFocusEnabled,
+                mIsOverrideEnableCompatFakeFocusEnabled,
+                mBooleanPropertyFakeFocus);
+    }
+
+    /**
      * Sets whether an activity is relaunching after the app has called {@link
      * android.app.Activity#setRequestedOrientation}.
      */
@@ -262,6 +424,206 @@
         mIsRefreshAfterRotationRequested = isRequested;
     }
 
+    boolean isOverrideRespectRequestedOrientationEnabled() {
+        return mIsOverrideRespectRequestedOrientationEnabled;
+    }
+
+    /**
+     * Whether should fix display orientation to landscape natural orientation when a task is
+     * fullscreen and the display is ignoring orientation requests.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Opt-out component property isn't enabled
+     *     <li>Opt-in per-app override is enabled
+     *     <li>Task is in fullscreen.
+     *     <li>{@link DisplayContent#getIgnoreOrientationRequest} is enabled
+     *     <li>Natural orientation of the display is landscape.
+     * </ul>
+     */
+    boolean shouldUseDisplayLandscapeNaturalOrientation() {
+        return shouldEnableWithOptInOverrideAndOptOutProperty(
+                /* gatingCondition */ () -> mActivityRecord.mDisplayContent != null
+                        && mActivityRecord.getTask() != null
+                        && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()
+                        && !mActivityRecord.getTask().inMultiWindowMode()
+                        && mActivityRecord.mDisplayContent.getNaturalOrientation()
+                                == ORIENTATION_LANDSCAPE,
+                mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled,
+                mBooleanPropertyAllowDisplayOrientationOverride);
+    }
+
+    @ScreenOrientation
+    int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
+        if (FALSE.equals(mBooleanPropertyAllowOrientationOverride)) {
+            return candidate;
+        }
+
+        DisplayContent displayContent = mActivityRecord.mDisplayContent;
+        if (mIsOverrideOrientationOnlyForCameraEnabled && displayContent != null
+                && (displayContent.mDisplayRotationCompatPolicy == null
+                        || !displayContent.mDisplayRotationCompatPolicy
+                                .isActivityEligibleForOrientationOverride(mActivityRecord))) {
+            return candidate;
+        }
+
+        if (mIsOverrideToReverseLandscapeOrientationEnabled
+                && (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
+            Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
+                    + mActivityRecord + " is overridden to "
+                    + screenOrientationToString(SCREEN_ORIENTATION_REVERSE_LANDSCAPE));
+            return SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+        }
+
+        if (!mIsOverrideAnyOrientationEnabled && isFixedOrientation(candidate)) {
+            return candidate;
+        }
+
+        if (mIsOverrideToPortraitOrientationEnabled) {
+            Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
+                    + mActivityRecord + " is overridden to "
+                    + screenOrientationToString(SCREEN_ORIENTATION_PORTRAIT));
+            return SCREEN_ORIENTATION_PORTRAIT;
+        }
+
+        if (mIsOverrideToNosensorOrientationEnabled) {
+            Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
+                    + mActivityRecord + " is overridden to "
+                    + screenOrientationToString(SCREEN_ORIENTATION_NOSENSOR));
+            return SCREEN_ORIENTATION_NOSENSOR;
+        }
+
+        return candidate;
+    }
+
+    boolean isOverrideOrientationOnlyForCameraEnabled() {
+        return mIsOverrideOrientationOnlyForCameraEnabled;
+    }
+
+    /**
+     * Whether activity is eligible for activity "refresh" after camera compat force rotation
+     * treatment. See {@link DisplayRotationCompatPolicy} for context.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the camera compat treatment is enabled.
+     *     <li>Activity isn't opted out by the device manufacturer with override or by the app
+     *     developers with the component property.
+     * </ul>
+     */
+    boolean shouldRefreshActivityForCameraCompat() {
+        return shouldEnableWithOptOutOverrideAndProperty(
+                /* gatingCondition */ () -> mLetterboxConfiguration
+                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                mIsOverrideCameraCompatDisableRefreshEnabled,
+                mBooleanPropertyCameraCompatAllowRefresh);
+    }
+
+    /**
+     * Whether activity should be "refreshed" after the camera compat force rotation treatment
+     * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped
+     * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the camera compat treatment is enabled.
+     *     <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the
+     *     component property by the app developers.
+     *     <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device
+     *     manufacturer with override / by the app developers with the component property.
+     * </ul>
+     */
+    boolean shouldRefreshActivityViaPauseForCameraCompat() {
+        return shouldEnableWithOverrideAndProperty(
+                /* gatingCondition */ () -> mLetterboxConfiguration
+                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                mIsOverrideCameraCompatEnableRefreshViaPauseEnabled,
+                mBooleanPropertyCameraCompatEnableRefreshViaPause);
+    }
+
+    /**
+     * Whether activity is eligible for camera compat force rotation treatment. See {@link
+     * DisplayRotationCompatPolicy} for context.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the camera compat treatment is enabled.
+     *     <li>Activity isn't opted out by the device manufacturer with override or by the app
+     *     developers with the component property.
+     * </ul>
+     */
+    boolean shouldForceRotateForCameraCompat() {
+        return shouldEnableWithOptOutOverrideAndProperty(
+                /* gatingCondition */ () -> mLetterboxConfiguration
+                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                mIsOverrideCameraCompatDisableForceRotationEnabled,
+                mBooleanPropertyCameraCompatAllowForceRotation);
+    }
+
+    private boolean isCompatChangeEnabled(long overrideChangeId) {
+        return mActivityRecord.info.isChangeEnabled(overrideChangeId);
+    }
+
+    /**
+     * Returns {@code true} when the following conditions are met:
+     * <ul>
+     *     <li>{@code gatingCondition} isn't {@code false}
+     *     <li>OEM didn't opt out with a per-app override
+     *     <li>App developers didn't opt out with a component {@code property}
+     * </ul>
+     *
+     * <p>This is used for the treatments that are enabled based with the heuristic but can be
+     * disabled on per-app basis by OEMs or app developers.
+     */
+    private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition,
+            boolean isOverrideChangeEnabled, Boolean property) {
+        if (!gatingCondition.getAsBoolean()) {
+            return false;
+        }
+        return !FALSE.equals(property) && !isOverrideChangeEnabled;
+    }
+
+    /**
+     * Returns {@code true} when the following conditions are met:
+     * <ul>
+     *     <li>{@code gatingCondition} isn't {@code false}
+     *     <li>OEM did opt in with a per-app override
+     *     <li>App developers didn't opt out with a component {@code property}
+     * </ul>
+     *
+     * <p>This is used for the treatments that are enabled based with the heuristic but can be
+     * disabled on per-app basis by OEMs or app developers.
+     */
+    private boolean shouldEnableWithOptInOverrideAndOptOutProperty(BooleanSupplier gatingCondition,
+            boolean isOverrideChangeEnabled, Boolean property) {
+        if (!gatingCondition.getAsBoolean()) {
+            return false;
+        }
+        return !FALSE.equals(property) && isOverrideChangeEnabled;
+    }
+
+    /**
+     * Returns {@code true} when the following conditions are met:
+     * <ul>
+     *     <li>{@code gatingCondition} isn't {@code false}
+     *     <li>App developers didn't opt out with a component {@code property}
+     *     <li>App developers opted in with a component {@code property} or an OEM opted in with a
+     *     per-app override
+     * </ul>
+     *
+     * <p>This is used for the treatments that are enabled only on per-app basis.
+     */
+    private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition,
+            boolean isOverrideChangeEnabled, Boolean property) {
+        if (!gatingCondition.getAsBoolean()) {
+            return false;
+        }
+        if (FALSE.equals(property)) {
+            return false;
+        }
+        return TRUE.equals(property) || isOverrideChangeEnabled;
+    }
+
     boolean hasWallpaperBackgroundForLetterbox() {
         return mShowWallpaperForLetterboxBackground;
     }
@@ -280,10 +642,9 @@
         if (mLetterbox != null) {
             outBounds.set(mLetterbox.getInnerFrame());
             final WindowState w = mActivityRecord.findMainWindow();
-            if (w == null) {
-                return;
+            if (w != null) {
+                adjustBoundsForTaskbar(w, outBounds);
             }
-            adjustBoundsForTaskbar(w, outBounds);
         } else {
             outBounds.setEmpty();
         }
@@ -326,13 +687,13 @@
         if (w == null || winHint != null && w != winHint) {
             return;
         }
-        updateRoundedCorners(w);
+        updateRoundedCornersIfNeeded(w);
         // If there is another main window that is not an application-starting window, we should
         // update rounded corners for it as well, to avoid flickering rounded corners.
         final WindowState nonStartingAppW = mActivityRecord.findMainWindow(
                 /* includeStartingApp= */ false);
         if (nonStartingAppW != null && nonStartingAppW != w) {
-            updateRoundedCorners(nonStartingAppW);
+            updateRoundedCornersIfNeeded(nonStartingAppW);
         }
 
         updateWallpaperForLetterbox(w);
@@ -368,8 +729,21 @@
                     : mActivityRecord.inMultiWindowMode()
                             ? mActivityRecord.getTask().getBounds()
                             : mActivityRecord.getRootTask().getParent().getBounds();
+            // In case of translucent activities an option is to use the WindowState#getFrame() of
+            // the first opaque activity beneath. In some cases (e.g. an opaque activity is using
+            // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
+            // information and in particular it might provide a value for a smaller area making
+            // the letterbox overlap with the translucent activity's frame.
+            // If we use WindowState#getFrame() for the translucent activity's letterbox inner
+            // frame, the letterbox will then be overlapped with the translucent activity's frame.
+            // Because the surface layer of letterbox is lower than an activity window, this
+            // won't crop the content, but it may affect other features that rely on values stored
+            // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
+            // For this reason we use ActivityRecord#getBounds() that the translucent activity
+            // inherits from the first opaque activity beneath and also takes care of the scaling
+            // in case of activities in size compat mode.
             final Rect innerFrame = hasInheritedLetterboxBehavior()
-                    ? mActivityRecord.getWindowConfiguration().getBounds() : w.getFrame();
+                    ? mActivityRecord.getBounds() : w.getFrame();
             mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
         } else if (mLetterbox != null) {
             mLetterbox.hide();
@@ -394,7 +768,7 @@
     // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
     // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
     // actually fullscreen.
-    private boolean isDisplayFullScreenAndInPosture(DeviceStateController.FoldState state,
+    private boolean isDisplayFullScreenAndInPosture(DeviceStateController.DeviceState state,
             boolean isTabletop) {
         Task task = mActivityRecord.getTask();
         return mActivityRecord.mDisplayContent != null
@@ -420,7 +794,7 @@
         // Don't check resolved configuration because it may not be updated yet during
         // configuration change.
         boolean bookMode = isDisplayFullScreenAndInPosture(
-                DeviceStateController.FoldState.HALF_FOLDED, false /* isTabletop */);
+                DeviceStateController.DeviceState.HALF_FOLDED, false /* isTabletop */);
         return isHorizontalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
@@ -432,7 +806,7 @@
         // Don't check resolved configuration because it may not be updated yet during
         // configuration change.
         boolean tabletopMode = isDisplayFullScreenAndInPosture(
-                DeviceStateController.FoldState.HALF_FOLDED, true /* isTabletop */);
+                DeviceStateController.DeviceState.HALF_FOLDED, true /* isTabletop */);
         return isVerticalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
@@ -445,7 +819,7 @@
                 ? getSplitScreenAspectRatio()
                 : mActivityRecord.shouldCreateCompatDisplayInsets()
                     ? getDefaultMinAspectRatioForUnresizableApps()
-                    : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+                    : getDefaultMinAspectRatio();
     }
 
     private float getDefaultMinAspectRatioForUnresizableApps() {
@@ -454,7 +828,7 @@
             return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
                     > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
                             ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
-                            : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+                            : getDefaultMinAspectRatio();
         }
 
         return getSplitScreenAspectRatio();
@@ -482,6 +856,16 @@
         return computeAspectRatio(bounds);
     }
 
+    private float getDefaultMinAspectRatio() {
+        final DisplayContent displayContent = mActivityRecord.getDisplayContent();
+        if (displayContent == null
+                || !mLetterboxConfiguration
+                    .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
+            return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+        }
+        return computeAspectRatio(new Rect(displayContent.getBounds()));
+    }
+
     Resources getResources() {
         return mActivityRecord.mWmService.mContext.getResources();
     }
@@ -575,6 +959,7 @@
      *   <li>Activity is portrait-only.
      *   <li>Fullscreen window in landscape device orientation.
      *   <li>Horizontal Reachability is enabled.
+     *   <li>Activity fills parent vertically.
      * </ul>
      */
     private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
@@ -582,10 +967,14 @@
                 && parentConfiguration.windowConfiguration.getWindowingMode()
                         == WINDOWING_MODE_FULLSCREEN
                 && (parentConfiguration.orientation == ORIENTATION_LANDSCAPE
-                && mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT);
+                        && mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT)
+                // Check whether the activity fills the parent vertically.
+                && parentConfiguration.windowConfiguration.getAppBounds().height()
+                        <= mActivityRecord.getBounds().height();
     }
 
-    private boolean isHorizontalReachabilityEnabled() {
+    @VisibleForTesting
+    boolean isHorizontalReachabilityEnabled() {
         return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
     }
 
@@ -597,6 +986,7 @@
      *   <li>Activity is landscape-only.
      *   <li>Fullscreen window in portrait device orientation.
      *   <li>Vertical Reachability is enabled.
+     *   <li>Activity fills parent horizontally.
      * </ul>
      */
     private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
@@ -604,28 +994,29 @@
                 && parentConfiguration.windowConfiguration.getWindowingMode()
                         == WINDOWING_MODE_FULLSCREEN
                 && (parentConfiguration.orientation == ORIENTATION_PORTRAIT
-                && mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE);
+                        && mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE)
+                // Check whether the activity fills the parent horizontally.
+                && parentConfiguration.windowConfiguration.getBounds().width()
+                        == mActivityRecord.getBounds().width();
     }
 
-    private boolean isVerticalReachabilityEnabled() {
+    @VisibleForTesting
+    boolean isVerticalReachabilityEnabled() {
         return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
     }
 
     @VisibleForTesting
     boolean shouldShowLetterboxUi(WindowState mainWindow) {
-        return isSurfaceReadyAndVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed()
+        return isSurfaceVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed()
                 // Check for FLAG_SHOW_WALLPAPER explicitly instead of using
                 // WindowContainer#showWallpaper because the later will return true when this
-                // activity is using blurred wallpaper for letterbox backgroud.
-                && (mainWindow.mAttrs.flags & FLAG_SHOW_WALLPAPER) == 0;
+                // activity is using blurred wallpaper for letterbox background.
+                && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0;
     }
 
     @VisibleForTesting
-    boolean isSurfaceReadyAndVisible(WindowState mainWindow) {
-        boolean surfaceReady = mainWindow.isDrawn() // Regular case
-                // Waiting for relayoutWindow to call preserveSurface
-                || mainWindow.isDragResizeChanged();
-        return surfaceReady && (mActivityRecord.isVisible()
+    boolean isSurfaceVisible(WindowState mainWindow) {
+        return mainWindow.isOnScreen() && (mActivityRecord.isVisible()
                 || mActivityRecord.isVisibleRequested());
     }
 
@@ -669,108 +1060,109 @@
         return mLetterboxConfiguration.getLetterboxBackgroundColor();
     }
 
-    private void updateRoundedCorners(WindowState mainWindow) {
+    private void updateRoundedCornersIfNeeded(final WindowState mainWindow) {
         final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
-        if (windowSurface != null && windowSurface.isValid()) {
-            final Transaction transaction = mActivityRecord.getSyncTransaction();
-
-            if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
-                // We don't want corner radius on the window.
-                // In the case the ActivityRecord requires a letterboxed animation we never want
-                // rounded corners on the window because rounded corners are applied at the
-                // animation-bounds surface level and rounded corners on the window would interfere
-                // with that leading to unexpected rounded corner positioning during the animation.
-                transaction
-                        .setWindowCrop(windowSurface, null)
-                        .setCornerRadius(windowSurface, 0);
-                return;
-            }
-
-            Rect cropBounds = null;
-
-            if (hasVisibleTaskbar(mainWindow)) {
-                cropBounds = new Rect(mActivityRecord.getBounds());
-
-                // Rounded corners should be displayed above the taskbar.
-                // It is important to call adjustBoundsForTaskbarUnchecked before offsetTo
-                // because taskbar bounds are in screen coordinates
-                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
-
-                // Activity bounds are in screen coordinates while (0,0) for activity's surface
-                // control is at the top left corner of an app window so offsetting bounds
-                // accordingly.
-                cropBounds.offsetTo(0, 0);
-            }
-
-            transaction
-                    .setWindowCrop(windowSurface, cropBounds)
-                    .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
+        if (windowSurface == null || !windowSurface.isValid()) {
+            return;
         }
+
+        // cropBounds must be non-null for the cornerRadius to be ever applied.
+        mActivityRecord.getSyncTransaction()
+                .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow))
+                .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
     }
 
-    private boolean requiresRoundedCorners(WindowState mainWindow) {
-        final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
+    @VisibleForTesting
+    @Nullable
+    Rect getCropBoundsIfNeeded(final WindowState mainWindow) {
+        if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
+            // We don't want corner radius on the window.
+            // In the case the ActivityRecord requires a letterboxed animation we never want
+            // rounded corners on the window because rounded corners are applied at the
+            // animation-bounds surface level and rounded corners on the window would interfere
+            // with that leading to unexpected rounded corner positioning during the animation.
+            return null;
+        }
 
+        final Rect cropBounds = new Rect(mActivityRecord.getBounds());
+
+        // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo}
+        // because taskbar bounds used in {@link #adjustBoundsIfNeeded}
+        // are in screen coordinates
+        adjustBoundsForTaskbar(mainWindow, cropBounds);
+
+        final float scale = mainWindow.mInvGlobalScale;
+        if (scale != 1f && scale > 0f) {
+            cropBounds.scale(scale);
+        }
+
+        // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface
+        // control is in the top left corner of an app window so offsetting bounds
+        // accordingly.
+        cropBounds.offsetTo(0, 0);
+        return cropBounds;
+    }
+
+    private boolean requiresRoundedCorners(final WindowState mainWindow) {
         return isLetterboxedNotForDisplayCutout(mainWindow)
-                && mLetterboxConfiguration.isLetterboxActivityCornersRounded()
-                && taskbarInsetsSource != null;
+                && mLetterboxConfiguration.isLetterboxActivityCornersRounded();
     }
 
     // Returns rounded corners radius the letterboxed activity should have based on override in
     // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
-    // Device corners can be different on the right and left sides but we use the same radius
+    // Device corners can be different on the right and left sides, but we use the same radius
     // for all corners for consistency and pick a minimal bottom one for consistency with a
     // taskbar rounded corners.
-    int getRoundedCornersRadius(WindowState mainWindow) {
-        if (!requiresRoundedCorners(mainWindow)) {
+    int getRoundedCornersRadius(final WindowState mainWindow) {
+        if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
             return 0;
         }
 
+        final int radius;
         if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
-            return mLetterboxConfiguration.getLetterboxActivityCornersRadius();
+            radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius();
+        } else {
+            final InsetsState insetsState = mainWindow.getInsetsState();
+            radius = Math.min(
+                    getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
+                    getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
         }
 
-        final InsetsState insetsState = mainWindow.getInsetsState();
-        return Math.min(
-                getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
-                getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
+        final float scale = mainWindow.mInvGlobalScale;
+        return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius;
     }
 
     /**
-     * Returns whether the taskbar is visible. Returns false if the window is in immersive mode,
-     * since the user can swipe to show/hide the taskbar as an overlay.
+     * Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
      */
-    private boolean hasVisibleTaskbar(WindowState mainWindow) {
-        final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
-
-        return taskbarInsetsSource != null
-                && taskbarInsetsSource.isVisible();
+    @VisibleForTesting
+    @Nullable
+    InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) {
+        final InsetsSource taskbar = mainWindow.getInsetsState().peekSource(
+                InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        if (taskbar != null && taskbar.isVisible()
+                && taskbar.getFrame().height() >= mExpandedTaskBarHeight) {
+            return taskbar;
+        }
+        return null;
     }
 
-    private InsetsSource getTaskbarInsetsSource(WindowState mainWindow) {
-        final InsetsState insetsState = mainWindow.getInsetsState();
-        return insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
-    }
-
-    private void adjustBoundsForTaskbar(WindowState mainWindow, Rect bounds) {
+    private void adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds) {
         // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
         // an insets frame is equal to a navigation bar which shouldn't affect position of
         // rounded corners since apps are expected to handle navigation bar inset.
         // This condition checks whether the taskbar is visible.
         // Do not crop the taskbar inset if the window is in immersive mode - the user can
         // swipe to show/hide the taskbar as an overlay.
-        if (hasVisibleTaskbar(mainWindow)) {
-            adjustBoundsForTaskbarUnchecked(mainWindow, bounds);
+        // Adjust the bounds only in case there is an expanded taskbar,
+        // otherwise the rounded corners will be shown behind the navbar.
+        final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow);
+        if (expandedTaskbarOrNull != null) {
+            // Rounded corners should be displayed above the expanded taskbar.
+            bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top);
         }
     }
 
-    private void adjustBoundsForTaskbarUnchecked(WindowState mainWindow, Rect bounds) {
-        // Rounded corners should be displayed above the taskbar.
-        bounds.bottom =
-                Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
-        scaleIfNeeded(bounds);
-    }
-
     private int getInsetsStateCornerRadius(
                 InsetsState insetsState, @RoundedCorner.Position int position) {
         RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
@@ -875,6 +1267,9 @@
                 + mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps());
         pw.println(prefix + "  isSplitScreenAspectRatioForUnresizableAppsEnabled="
                 + mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+        pw.println(prefix + "  isDisplayAspectRatioEnabledForFixedOrientationLetterbox="
+                + mLetterboxConfiguration
+                .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
     }
 
     /**
@@ -935,7 +1330,7 @@
             int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
                     .getLetterboxPositionForHorizontalReachability(
                             isDisplayFullScreenAndInPosture(
-                                    DeviceStateController.FoldState.HALF_FOLDED,
+                                    DeviceStateController.DeviceState.HALF_FOLDED,
                                     false /* isTabletop */));
             positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
                     letterboxPositionForHorizontalReachability);
@@ -943,7 +1338,7 @@
             int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
                     .getLetterboxPositionForVerticalReachability(
                             isDisplayFullScreenAndInPosture(
-                                    DeviceStateController.FoldState.HALF_FOLDED,
+                                    DeviceStateController.DeviceState.HALF_FOLDED,
                                     true /* isTabletop */));
             positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
                     letterboxPositionForVerticalReachability);
@@ -998,19 +1393,19 @@
             mLetterboxConfigListener.onRemoved();
             clearInheritedConfig();
         }
-        // In case mActivityRecord.getCompatDisplayInsets() is not null we don't apply the
+        // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the
         // opaque activity constraints because we're expecting the activity is already letterboxed.
-        if (mActivityRecord.getTask() == null || mActivityRecord.getCompatDisplayInsets() != null
-                || mActivityRecord.fillsParent()) {
+        if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
+                || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
             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
-                || mActivityRecord.launchedFromUid != firstOpaqueActivityBeneath.getUid()) {
+        if (firstOpaqueActivityBeneath == null) {
             // We skip letterboxing if the translucent activity doesn't have any opaque
-            // activities beneath of if it's launched from a different user (e.g. notification)
+            // activities beneath
             return;
         }
         inheritConfiguration(firstOpaqueActivityBeneath);
@@ -1029,6 +1424,7 @@
                     // We need to initialize appBounds to avoid NPE. The actual value will
                     // be set ahead when resolving the Configuration for the activity.
                     mutatedConfiguration.windowConfiguration.setAppBounds(new Rect());
+                    inheritConfiguration(firstOpaqueActivityBeneath);
                     return mutatedConfiguration;
                 });
     }
@@ -1053,7 +1449,8 @@
         // To avoid wrong behaviour, we're not forcing orientation for activities with not
         // fixed orientation (e.g. permission dialogs).
         return hasInheritedLetterboxBehavior()
-                && mActivityRecord.mOrientation != SCREEN_ORIENTATION_UNSPECIFIED;
+                && mActivityRecord.getOverrideOrientation()
+                        != SCREEN_ORIENTATION_UNSPECIFIED;
     }
 
     float getInheritedMinAspectRatio() {
@@ -1068,19 +1465,41 @@
         return mInheritedAppCompatState;
     }
 
-    float getInheritedSizeCompatScale() {
-        return mInheritedSizeCompatScale;
-    }
-
     @Configuration.Orientation
     int getInheritedOrientation() {
         return mInheritedOrientation;
     }
 
-    public ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
+    ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
         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.
+     */
+    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
@@ -1093,8 +1512,6 @@
         }
         mInheritedOrientation = firstOpaque.getRequestedConfigurationOrientation();
         mInheritedAppCompatState = firstOpaque.getAppCompatState();
-        mIsInheritedInSizeCompatMode = firstOpaque.inSizeCompatMode();
-        mInheritedSizeCompatScale = firstOpaque.getCompatScale();
         mInheritedCompatDisplayInsets = firstOpaque.getCompatDisplayInsets();
     }
 
@@ -1104,24 +1521,6 @@
         mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
         mInheritedOrientation = Configuration.ORIENTATION_UNDEFINED;
         mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
-        mIsInheritedInSizeCompatMode = false;
-        mInheritedSizeCompatScale = 1f;
         mInheritedCompatDisplayInsets = null;
     }
-
-    private void scaleIfNeeded(Rect bounds) {
-        if (boundsNeedToScale()) {
-            bounds.scale(1.0f / mActivityRecord.getCompatScale());
-        }
-    }
-
-    private boolean boundsNeedToScale() {
-        if (hasInheritedLetterboxBehavior()) {
-            return mIsInheritedInSizeCompatMode
-                    && mInheritedSizeCompatScale < 1.0f;
-        } else {
-            return mActivityRecord.inSizeCompatMode()
-                    && mActivityRecord.getCompatScale() < 1.0f;
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index 30bdc34..2edb082 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -51,10 +51,10 @@
     /**
      *   Called by the DeviceStateManager callback when the state changes.
      */
-    void foldStateChanged(DeviceStateController.FoldState newFoldState) {
+    void foldStateChanged(DeviceStateController.DeviceState newDeviceState) {
         // Ignore transitions to/from half-folded.
-        if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return;
-        mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED;
+        if (newDeviceState == DeviceStateController.DeviceState.HALF_FOLDED) return;
+        mIsFolded = newDeviceState == DeviceStateController.DeviceState.FOLDED;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 4860762..67fd557 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -33,6 +33,8 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.os.Process.SYSTEM_UID;
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
@@ -215,10 +217,16 @@
             int y = (int) ev.getY();
             mService.mH.post(PooledLambda.obtainRunnable((nonArg) -> {
                 synchronized (mService.mGlobalLock) {
-                    // Unfreeze the task list once we touch down in a task
                     final RootWindowContainer rac = mService.mRootWindowContainer;
                     final DisplayContent dc = rac.getDisplayContent(displayId).mDisplayContent;
-                    if (dc.pointWithinAppWindow(x, y)) {
+                    final WindowState win = dc.getTouchableWinAtPointLocked((float) x, (float) y);
+                    if (win == null) {
+                        return;
+                    }
+                    // Unfreeze the task list once we touch down in a task
+                    final boolean isAppWindowTouch = FIRST_APPLICATION_WINDOW <= win.mAttrs.type
+                            && win.mAttrs.type <= LAST_APPLICATION_WINDOW;
+                    if (isAppWindowTouch) {
                         final Task stack = mService.getTopDisplayFocusedRootTask();
                         final Task topTask = stack != null ? stack.getTopMostTask() : null;
                         resetFreezeTaskListReordering(topTask);
@@ -976,7 +984,7 @@
                 continue;
             }
 
-            res.add(createRecentTaskInfo(task, true /* stripExtras */));
+            res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed));
         }
         return res;
     }
@@ -1895,7 +1903,8 @@
     /**
      * Creates a new RecentTaskInfo from a Task.
      */
-    ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) {
+    ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras,
+            boolean getTasksAllowed) {
         final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
         // If the recent Task is detached, we consider it will be re-attached to the default
         // TaskDisplayArea because we currently only support recent overview in the default TDA.
@@ -1907,6 +1916,9 @@
         rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID;
         rti.persistentId = rti.taskId;
         rti.lastSnapshotData.set(tr.mLastTaskSnapshotData);
+        if (!getTasksAllowed) {
+            Task.trimIneffectiveInfo(tr, rti);
+        }
 
         // Fill in organized child task info for the task created by organizer.
         if (tr.mCreatedByOrganizer) {
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index f3713eb..6b3c533 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -53,6 +53,8 @@
         }
     }
 
+    private final DisplayInfo mDisplayInfo;
+    private final Mode mDefaultMode;
     private final Mode mLowRefreshRateMode;
     private final PackageRefreshRate mNonHighRefreshRatePackages = new PackageRefreshRate();
     private final HighRefreshRateDenylist mHighRefreshRateDenylist;
@@ -83,7 +85,9 @@
 
     RefreshRatePolicy(WindowManagerService wmService, DisplayInfo displayInfo,
             HighRefreshRateDenylist denylist) {
-        mLowRefreshRateMode = findLowRefreshRateMode(displayInfo);
+        mDisplayInfo = displayInfo;
+        mDefaultMode = displayInfo.getDefaultMode();
+        mLowRefreshRateMode = findLowRefreshRateMode(displayInfo, mDefaultMode);
         mHighRefreshRateDenylist = denylist;
         mWmService = wmService;
     }
@@ -92,10 +96,9 @@
      * Finds the mode id with the lowest refresh rate which is >= 60hz and same resolution as the
      * default mode.
      */
-    private Mode findLowRefreshRateMode(DisplayInfo displayInfo) {
-        Mode mode = displayInfo.getDefaultMode();
+    private Mode findLowRefreshRateMode(DisplayInfo displayInfo, Mode defaultMode) {
         float[] refreshRates = displayInfo.getDefaultRefreshRates();
-        float bestRefreshRate = mode.getRefreshRate();
+        float bestRefreshRate = defaultMode.getRefreshRate();
         mMinSupportedRefreshRate = bestRefreshRate;
         mMaxSupportedRefreshRate = bestRefreshRate;
         for (int i = refreshRates.length - 1; i >= 0; i--) {
@@ -121,13 +124,39 @@
     }
 
     int getPreferredModeId(WindowState w) {
-        // If app is animating, it's not able to control refresh rate because we want the animation
-        // to run in default refresh rate.
-        if (w.isAnimating(TRANSITION | PARENTS)) {
+        final int preferredDisplayModeId = w.mAttrs.preferredDisplayModeId;
+        if (preferredDisplayModeId <= 0) {
+            // Unspecified, use default mode.
             return 0;
         }
 
-        return w.mAttrs.preferredDisplayModeId;
+        // If app is animating, it's not able to control refresh rate because we want the animation
+        // to run in default refresh rate. But if the display size of default mode is different
+        // from the using preferred mode, then still keep the preferred mode to avoid disturbing
+        // the animation.
+        if (w.isAnimating(TRANSITION | PARENTS)) {
+            Display.Mode preferredMode = null;
+            for (Display.Mode mode : mDisplayInfo.supportedModes) {
+                if (preferredDisplayModeId == mode.getModeId()) {
+                    preferredMode = mode;
+                    break;
+                }
+            }
+            if (preferredMode != null) {
+                final int pW = preferredMode.getPhysicalWidth();
+                final int pH = preferredMode.getPhysicalHeight();
+                if ((pW != mDefaultMode.getPhysicalWidth()
+                        || pH != mDefaultMode.getPhysicalHeight())
+                        && pW == mDisplayInfo.getNaturalWidth()
+                        && pH == mDisplayInfo.getNaturalHeight()) {
+                    // Prefer not to change display size when animating.
+                    return preferredDisplayModeId;
+                }
+            }
+            return 0;
+        }
+
+        return preferredDisplayModeId;
     }
 
     /**
@@ -165,12 +194,9 @@
         // of that mode id.
         final int preferredModeId = w.mAttrs.preferredDisplayModeId;
         if (preferredModeId > 0) {
-            DisplayInfo info = w.getDisplayInfo();
-            if (info != null) {
-                for (Display.Mode mode : info.supportedModes) {
-                    if (preferredModeId == mode.getModeId()) {
-                        return mode.getRefreshRate();
-                    }
+            for (Display.Mode mode : mDisplayInfo.supportedModes) {
+                if (preferredModeId == mode.getModeId()) {
+                    return mode.getRefreshRate();
                 }
             }
         }
diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java
index 33f019e..4e339f1 100644
--- a/services/core/java/com/android/server/wm/RunningTasks.java
+++ b/services/core/java/com/android/server/wm/RunningTasks.java
@@ -177,6 +177,10 @@
         }
         // Fill in some deprecated values
         rti.id = rti.taskId;
+
+        if (!mAllowed) {
+            Task.trimIneffectiveInfo(task, rti);
+        }
         return rti;
     }
 }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index fd8b614..ef45c22 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -800,6 +800,10 @@
                 if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
                     // It also invokes kill().
                     mDisplayContent.setRotationAnimation(null);
+                    if (mDisplayContent.mDisplayRotationCompatPolicy != null) {
+                        mDisplayContent.mDisplayRotationCompatPolicy
+                                .onScreenRotationAnimationFinished();
+                    }
                 } else {
                     kill();
                 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index ba89657..39b7ccd 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -609,7 +609,7 @@
      */
     ActivityRecord mChildPipActivity;
 
-    boolean mLastSurfaceShowing = true;
+    boolean mLastSurfaceShowing;
 
     /**
      * Tracks if a back gesture is in progress.
@@ -3327,12 +3327,17 @@
         // We intend to let organizer manage task visibility but it doesn't
         // have enough information until we finish shell transitions.
         // In the mean time we do an easy fix here.
-        final boolean show = isVisible() || isAnimating(TRANSITION | PARENTS | CHILDREN);
+        final boolean visible = isVisible();
+        final boolean show = visible || isAnimating(TRANSITION | PARENTS | CHILDREN);
         if (mSurfaceControl != null) {
             if (show != mLastSurfaceShowing) {
                 t.setVisibility(mSurfaceControl, show);
             }
         }
+        // Only show the overlay if the task has other visible children
+        if (mOverlayHost != null) {
+            mOverlayHost.setVisibility(t, visible);
+        }
         mLastSurfaceShowing = show;
     }
 
@@ -3451,6 +3456,11 @@
                 && top.getOrganizedTask() == this && top.isState(RESUMED);
         // Whether the direct top activity is in size compat mode on foreground.
         info.topActivityInSizeCompat = isTopActivityResumed && top.inSizeCompatMode();
+        if (info.topActivityInSizeCompat
+                && mWmService.mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
+            // We hide the restart button in case of transparent activities.
+            info.topActivityInSizeCompat = top.fillsParent();
+        }
         // Whether the direct top activity is eligible for letterbox education.
         info.topActivityEligibleForLetterboxEducation = isTopActivityResumed
                 && top.isEligibleForLetterboxEducation();
@@ -3468,6 +3478,54 @@
         info.isSleeping = shouldSleepActivities();
     }
 
+    /**
+     * Removes the activity info if the activity belongs to a different uid, which is
+     * different from the app that hosts the task.
+     */
+    static void trimIneffectiveInfo(Task task, TaskInfo info) {
+        final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing,
+                false /* traverseTopToBottom */);
+        final int baseActivityUid =
+                baseActivity != null ? baseActivity.getUid() : task.effectiveUid;
+
+        if (info.topActivityInfo != null
+                && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) {
+            // Making a copy to prevent eliminating the info in the original ActivityRecord.
+            info.topActivityInfo = new ActivityInfo(info.topActivityInfo);
+            info.topActivityInfo.applicationInfo =
+                    new ApplicationInfo(info.topActivityInfo.applicationInfo);
+
+            // Strip the sensitive info.
+            info.topActivity = new ComponentName("", "");
+            info.topActivityInfo.packageName = "";
+            info.topActivityInfo.taskAffinity = "";
+            info.topActivityInfo.processName = "";
+            info.topActivityInfo.name = "";
+            info.topActivityInfo.parentActivityName = "";
+            info.topActivityInfo.targetActivity = "";
+            info.topActivityInfo.splitName = "";
+            info.topActivityInfo.applicationInfo.className = "";
+            info.topActivityInfo.applicationInfo.credentialProtectedDataDir = "";
+            info.topActivityInfo.applicationInfo.dataDir = "";
+            info.topActivityInfo.applicationInfo.deviceProtectedDataDir = "";
+            info.topActivityInfo.applicationInfo.manageSpaceActivityName = "";
+            info.topActivityInfo.applicationInfo.nativeLibraryDir = "";
+            info.topActivityInfo.applicationInfo.nativeLibraryRootDir = "";
+            info.topActivityInfo.applicationInfo.processName = "";
+            info.topActivityInfo.applicationInfo.publicSourceDir = "";
+            info.topActivityInfo.applicationInfo.scanPublicSourceDir = "";
+            info.topActivityInfo.applicationInfo.scanSourceDir = "";
+            info.topActivityInfo.applicationInfo.sourceDir = "";
+            info.topActivityInfo.applicationInfo.taskAffinity = "";
+            info.topActivityInfo.applicationInfo.name = "";
+            info.topActivityInfo.applicationInfo.packageName = "";
+        }
+
+        if (task.effectiveUid != baseActivityUid) {
+            info.baseActivity = new ComponentName("", "");
+        }
+    }
+
     @Nullable PictureInPictureParams getPictureInPictureParams() {
         final Task topTask = getTopMostTask();
         if (topTask == null) return null;
@@ -4150,13 +4208,7 @@
 
     @Override
     boolean showSurfaceOnCreation() {
-        if (mCreatedByOrganizer) {
-            // Tasks created by the organizer are default visible because they can synchronously
-            // update the leash before new children are added to the task.
-            return true;
-        }
-        // Organized tasks handle their own surface visibility
-        return !canBeOrganized();
+        return false;
     }
 
     @Override
@@ -6364,6 +6416,11 @@
             return this;
         }
 
+        Builder setRemoveWithTaskOrganizer(boolean removeWithTaskOrganizer) {
+            mRemoveWithTaskOrganizer = removeWithTaskOrganizer;
+            return this;
+        }
+
         private Builder setUserId(int userId) {
             mUserId = userId;
             return this;
@@ -6561,7 +6618,7 @@
             mCallingPackage = mActivityInfo.packageName;
             mResizeMode = mActivityInfo.resizeMode;
             mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture();
-            if (mActivityOptions != null) {
+            if (!mRemoveWithTaskOrganizer && mActivityOptions != null) {
                 mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer();
             }
 
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 1f3193d..b3c3eb3 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1953,7 +1953,7 @@
                         1f);
                 mBackScreenshots.put(r.mActivityComponent.flattenToString(), backBuffer);
             }
-            child.asActivityRecord().inHistory = true;
+            addingActivity.inHistory = true;
             task.onDescendantActivityAdded(taskHadActivity, activityType, addingActivity);
         }
     }
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 90a0dff..49b2a4ef 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -540,9 +540,12 @@
         synchronized (mGlobalLock) {
             final TaskFragmentOrganizerState organizerState =
                     mTaskFragmentOrganizerState.get(organizer.asBinder());
-            return organizerState != null
-                    ? organizerState.mRemoteAnimationDefinition
-                    : null;
+            if (organizerState == null) {
+                Slog.e(TAG, "TaskFragmentOrganizer has been unregistered or died when trying"
+                        + " to play animation on its organized windows.");
+                return null;
+            }
+            return organizerState.mRemoteAnimationDefinition;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index d619547..d780cae 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -783,7 +783,8 @@
     }
 
     @Override
-    public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) {
+    public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie,
+            boolean removeWithTaskOrganizer) {
         enforceTaskPermission("createRootTask()");
         final long origId = Binder.clearCallingIdentity();
         try {
@@ -795,7 +796,7 @@
                     return;
                 }
 
-                createRootTask(display, windowingMode, launchCookie);
+                createRootTask(display, windowingMode, launchCookie, removeWithTaskOrganizer);
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -804,6 +805,12 @@
 
     @VisibleForTesting
     Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie) {
+        return createRootTask(display, windowingMode, launchCookie,
+                false /* removeWithTaskOrganizer */);
+    }
+
+    Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie,
+            boolean removeWithTaskOrganizer) {
         ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Create root task displayId=%d winMode=%d",
                 display.mDisplayId, windowingMode);
         // We want to defer the task appear signal until the task is fully created and attached to
@@ -816,6 +823,7 @@
                 .setDeferTaskAppear(true)
                 .setLaunchCookie(launchCookie)
                 .setParent(display.getDefaultTaskDisplayArea())
+                .setRemoveWithTaskOrganizer(removeWithTaskOrganizer)
                 .build();
         task.setDeferTaskAppear(false /* deferTaskAppear */);
         return task;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 00e3188..db8079a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -45,6 +45,7 @@
 import static android.view.WindowManager.TransitionFlags;
 import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
+import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
 import static android.window.TransitionInfo.FLAG_FILLS_TASK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
@@ -1736,7 +1737,7 @@
                         ? activityRecord.getOrganizedTaskFragment()
                         : taskFragment.getOrganizedTaskFragment();
                 if (organizedTf != null && organizedTf.getAnimationParams()
-                        .getAnimationBackgroundColor() != 0) {
+                        .getAnimationBackgroundColor() != DEFAULT_ANIMATION_BACKGROUND_COLOR) {
                     // This window is embedded and has an animation background color set on the
                     // TaskFragment. Pass this color with this window, so the handler can use it as
                     // the animation background color if needed,
@@ -1748,10 +1749,11 @@
                     final Task parentTask = activityRecord != null
                             ? activityRecord.getTask()
                             : taskFragment.getTask();
-                    backgroundColor = ColorUtils.setAlphaComponent(
-                            parentTask.getTaskDescription().getBackgroundColor(), 255);
+                    backgroundColor = parentTask.getTaskDescription().getBackgroundColor();
                 }
-                change.setBackgroundColor(backgroundColor);
+                // Set to opaque for animation background to prevent it from exposing the blank
+                // background or content below.
+                change.setBackgroundColor(ColorUtils.setAlphaComponent(backgroundColor, 255));
             }
 
             change.setRotation(info.mRotation, endRotation);
diff --git a/services/core/java/com/android/server/wm/TrustedOverlayHost.java b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
index 975b21c..88c410b 100644
--- a/services/core/java/com/android/server/wm/TrustedOverlayHost.java
+++ b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
@@ -80,6 +80,12 @@
         }
     }
 
+    void setVisibility(SurfaceControl.Transaction t, boolean visible) {
+        if (mSurfaceControl != null) {
+            t.setVisibility(mSurfaceControl, visible);
+        }
+    }
+
     void addOverlay(SurfaceControlViewHost.SurfacePackage p, SurfaceControl currentParent) {
         requireOverlaySurfaceControl();
         mOverlays.add(p);
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 1d25dbc..b2dab78b 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -371,7 +371,7 @@
 
     boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) {
         // Size of the display the wallpaper is rendered on.
-        final Rect lastWallpaperBounds = wallpaperWin.getLastReportedBounds();
+        final Rect lastWallpaperBounds = wallpaperWin.getParentFrame();
         // Full size of the wallpaper (usually larger than bounds above to parallax scroll when
         // swiping through Launcher pages).
         final Rect wallpaperFrame = wallpaperWin.getFrame();
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index fb584fe..ce03244 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -33,6 +33,7 @@
 import static android.view.SurfaceControl.Transaction;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
@@ -73,6 +74,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ActivityInfo.ScreenOrientation;
 import android.content.res.Configuration;
 import android.graphics.Color;
 import android.graphics.Point;
@@ -178,8 +180,9 @@
     protected final WindowList<E> mChildren = new WindowList<E>();
 
     // The specified orientation for this window container.
-    @ActivityInfo.ScreenOrientation
-    protected int mOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
+    // Shouldn't be accessed directly since subclasses can override getOverrideOrientation.
+    @ScreenOrientation
+    private int mOverrideOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
 
     /**
      * The window container which decides its orientation since the last time
@@ -1427,19 +1430,20 @@
 
     /**
      * Gets the configuration orientation by the requested screen orientation
-     * ({@link ActivityInfo.ScreenOrientation}) of this activity.
+     * ({@link ScreenOrientation}) of this activity.
      *
      * @return orientation in ({@link Configuration#ORIENTATION_LANDSCAPE},
      *         {@link Configuration#ORIENTATION_PORTRAIT},
      *         {@link Configuration#ORIENTATION_UNDEFINED}).
      */
+    @ScreenOrientation
     int getRequestedConfigurationOrientation() {
         return getRequestedConfigurationOrientation(false /* forDisplay */);
     }
 
     /**
      * Gets the configuration orientation by the requested screen orientation
-     * ({@link ActivityInfo.ScreenOrientation}) of this activity.
+     * ({@link ScreenOrientation}) of this activity.
      *
      * @param forDisplay whether it is the requested config orientation for display.
      *                   If {@code true}, we may reverse the requested orientation if the root is
@@ -1450,8 +1454,9 @@
      *         {@link Configuration#ORIENTATION_PORTRAIT},
      *         {@link Configuration#ORIENTATION_UNDEFINED}).
      */
+    @ScreenOrientation
     int getRequestedConfigurationOrientation(boolean forDisplay) {
-        int requestedOrientation = mOrientation;
+        int requestedOrientation = getOverrideOrientation();
         final RootDisplayArea root = getRootDisplayArea();
         if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) {
             // Reverse the requested orientation if the orientation of its root is different from
@@ -1461,7 +1466,7 @@
             // (portrait).
             // When an app below the DAG is requesting landscape, it should actually request the
             // display to be portrait, so that the DAG and the app will be in landscape.
-            requestedOrientation = reverseOrientation(mOrientation);
+            requestedOrientation = reverseOrientation(getOverrideOrientation());
         }
 
         if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
@@ -1486,7 +1491,7 @@
      *
      * @param orientation the specified orientation.
      */
-    void setOrientation(int orientation) {
+    void setOrientation(@ScreenOrientation int orientation) {
         setOrientation(orientation, null /* requestingContainer */);
     }
 
@@ -1494,17 +1499,17 @@
      * Sets the specified orientation of this container. It percolates this change upward along the
      * hierarchy to let each level of the hierarchy a chance to respond to it.
      *
-     * @param orientation the specified orientation. Needs to be one of {@link
-     *      android.content.pm.ActivityInfo.ScreenOrientation}.
+     * @param orientation the specified orientation. Needs to be one of {@link ScreenOrientation}.
      * @param requestingContainer the container which orientation request has changed. Mostly used
      *                            to ensure it gets correct configuration.
      */
-    void setOrientation(int orientation, @Nullable WindowContainer requestingContainer) {
-        if (mOrientation == orientation) {
+    void setOrientation(@ScreenOrientation int orientation,
+            @Nullable WindowContainer requestingContainer) {
+        if (getOverrideOrientation() == orientation) {
             return;
         }
 
-        mOrientation = orientation;
+        setOverrideOrientation(orientation);
         final WindowContainer parent = getParent();
         if (parent != null) {
             if (getConfiguration().orientation != getRequestedConfigurationOrientation()
@@ -1523,9 +1528,9 @@
         }
     }
 
-    @ActivityInfo.ScreenOrientation
+    @ScreenOrientation
     int getOrientation() {
-        return getOrientation(mOrientation);
+        return getOrientation(getOverrideOrientation());
     }
 
     /**
@@ -1539,7 +1544,8 @@
      *                  better match.
      * @return The orientation as specified by this branch or the window hierarchy.
      */
-    int getOrientation(int candidate) {
+    @ScreenOrientation
+    int getOrientation(@ScreenOrientation int candidate) {
         mLastOrientationSource = null;
         if (!providesOrientation()) {
             return SCREEN_ORIENTATION_UNSET;
@@ -1549,16 +1555,16 @@
         // specified; otherwise we prefer to use the orientation of its topmost child that has one
         // specified and fall back on this container's unset or unspecified value as a candidate
         // if none of the children have a better candidate for the orientation.
-        if (mOrientation != SCREEN_ORIENTATION_UNSET
-                && mOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+        if (getOverrideOrientation() != SCREEN_ORIENTATION_UNSET
+                && getOverrideOrientation() != SCREEN_ORIENTATION_UNSPECIFIED) {
             mLastOrientationSource = this;
-            return mOrientation;
+            return getOverrideOrientation();
         }
 
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final WindowContainer wc = mChildren.get(i);
 
-            // TODO: Maybe mOrientation should default to SCREEN_ORIENTATION_UNSET vs.
+            // TODO: Maybe mOverrideOrientation should default to SCREEN_ORIENTATION_UNSET vs.
             // SCREEN_ORIENTATION_UNSPECIFIED?
             final int orientation = wc.getOrientation(candidate == SCREEN_ORIENTATION_BEHIND
                     ? SCREEN_ORIENTATION_BEHIND : SCREEN_ORIENTATION_UNSET);
@@ -1590,6 +1596,20 @@
     }
 
     /**
+     * Returns orientation specified on this level of hierarchy without taking children into
+     * account, like {@link #getOrientation} does, allowing subclasses to override. See {@link
+     * ActivityRecord#getOverrideOrientation} for an example.
+     */
+    @ScreenOrientation
+    protected int getOverrideOrientation() {
+        return mOverrideOrientation;
+    }
+
+    protected void setOverrideOrientation(@ScreenOrientation int orientation) {
+        mOverrideOrientation = orientation;
+    }
+
+    /**
      * @return The deepest source which decides the orientation of this window container since the
      *         last time {@link #getOrientation(int) was called.
      */
@@ -2635,7 +2655,7 @@
 
         final long token = proto.start(fieldId);
         super.dumpDebug(proto, CONFIGURATION_CONTAINER, logLevel);
-        proto.write(ORIENTATION, mOrientation);
+        proto.write(ORIENTATION, mOverrideOrientation);
         proto.write(VISIBLE, isVisible);
         writeIdentifierToProto(proto, IDENTIFIER);
         if (mSurfaceAnimator.isAnimating()) {
@@ -3149,7 +3169,7 @@
                             ? activityRecord.getOrganizedTaskFragment()
                             : taskFragment.getOrganizedTaskFragment();
                     if (organizedTf != null && organizedTf.getAnimationParams()
-                            .getAnimationBackgroundColor() != 0) {
+                            .getAnimationBackgroundColor() != DEFAULT_ANIMATION_BACKGROUND_COLOR) {
                         // This window is embedded and has an animation background color set on the
                         // TaskFragment. Pass this color with this window, so the handler can use it
                         // as the animation background color if needed,
@@ -3162,11 +3182,14 @@
                         final Task parentTask = activityRecord != null
                                 ? activityRecord.getTask()
                                 : taskFragment.getTask();
-                        backgroundColorForTransition = ColorUtils.setAlphaComponent(
-                                parentTask.getTaskDescription().getBackgroundColor(), 255);
+                        backgroundColorForTransition = parentTask.getTaskDescription()
+                                .getBackgroundColor();
                     }
                 }
-                animationRunnerBuilder.setTaskBackgroundColor(backgroundColorForTransition);
+                // Set to opaque for animation background to prevent it from exposing the blank
+                // background or content below.
+                animationRunnerBuilder.setTaskBackgroundColor(ColorUtils.setAlphaComponent(
+                        backgroundColorForTransition, 255));
             }
 
             animationRunnerBuilder.build()
@@ -3197,11 +3220,11 @@
 
     private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter,
                                     boolean isVoiceInteraction) {
-        if (isOrganized()
+        if (AppTransitionController.isTaskViewTask(this) || (isOrganized()
                 // TODO(b/161711458): Clean-up when moved to shell.
                 && getWindowingMode() != WINDOWING_MODE_FULLSCREEN
                 && getWindowingMode() != WINDOWING_MODE_FREEFORM
-                && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) {
+                && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) {
             return null;
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4e32a7c..8931d80 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -113,6 +113,7 @@
 import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
 import static com.android.server.LockGuard.INDEX_WINDOW;
 import static com.android.server.LockGuard.installLock;
+import static com.android.server.policy.PhoneWindowManager.TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
 import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
@@ -167,7 +168,6 @@
 import android.app.IAssistDataReceiver;
 import android.app.WindowConfiguration;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -177,7 +177,6 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.TestUtilityService;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
@@ -357,6 +356,7 @@
 public class WindowManagerService extends IWindowManager.Stub
         implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowManagerService" : TAG_WM;
+    private static final int TRACE_MAX_SECTION_NAME_LENGTH = 127;
 
     static final int LAYOUT_REPEAT_THRESHOLD = 4;
 
@@ -4172,7 +4172,8 @@
      * <p>Note: this assumes that {@link #mGlobalLock} is held by the caller.
      */
     boolean isIgnoreOrientationRequestDisabled() {
-        return mIsIgnoreOrientationRequestDisabled;
+        return mIsIgnoreOrientationRequestDisabled
+                || !mLetterboxConfiguration.isIgnoreOrientationRequestAllowed();
     }
 
     @Override
@@ -5443,10 +5444,15 @@
 
                 case WAITING_FOR_DRAWN_TIMEOUT: {
                     Runnable callback = null;
-                    final WindowContainer container = (WindowContainer) msg.obj;
+                    final WindowContainer<?> container = (WindowContainer<?>) msg.obj;
                     synchronized (mGlobalLock) {
                         ProtoLog.w(WM_ERROR, "Timeout waiting for drawn: undrawn=%s",
                                 container.mWaitingForDrawn);
+                        if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                            for (int i = 0; i < container.mWaitingForDrawn.size(); i++) {
+                                traceEndWaitingForWindowDrawn(container.mWaitingForDrawn.get(i));
+                            }
+                        }
                         container.mWaitingForDrawn.clear();
                         callback = mWaitingForDrawnCallbacks.remove(container);
                     }
@@ -5839,6 +5845,11 @@
             if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) {
                 return displayContent.mInitialDisplayDensity;
             }
+
+            DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+            if (info != null && info.hasAccess(Binder.getCallingUid())) {
+                return info.logicalDensityDpi;
+            }
         }
         return -1;
     }
@@ -5870,6 +5881,11 @@
                 final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
                 if (displayContent != null) {
                     displayContent.setForcedDensity(density, targetUserId);
+                } else {
+                    DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+                    if (info != null) {
+                        mDisplayWindowSettings.setForcedDensity(info, density, userId);
+                    }
                 }
             }
         } finally {
@@ -5894,6 +5910,12 @@
                 if (displayContent != null) {
                     displayContent.setForcedDensity(displayContent.mInitialDisplayDensity,
                             callingUserId);
+                } else {
+                    DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+                    if (info != null) {
+                        mDisplayWindowSettings.setForcedDensity(info, info.logicalDensityDpi,
+                                userId);
+                    }
                 }
             }
         } finally {
@@ -6037,10 +6059,16 @@
                     // Window has been removed or hidden; no draw will now happen, so stop waiting.
                     ProtoLog.w(WM_DEBUG_SCREEN_ON, "Aborted waiting for drawn: %s", win);
                     container.mWaitingForDrawn.remove(win);
+                    if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                        traceEndWaitingForWindowDrawn(win);
+                    }
                 } else if (win.hasDrawn()) {
                     // Window is now drawn (and shown).
                     ProtoLog.d(WM_DEBUG_SCREEN_ON, "Window drawn win=%s", win);
                     container.mWaitingForDrawn.remove(win);
+                    if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                        traceEndWaitingForWindowDrawn(win);
+                    }
                 }
             }
             if (container.mWaitingForDrawn.isEmpty()) {
@@ -6051,6 +6079,22 @@
         });
     }
 
+    private void traceStartWaitingForWindowDrawn(WindowState window) {
+        final String traceName = TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD + "#"
+                + window.getWindowTag();
+        final String shortenedTraceName = traceName.substring(0, Math.min(
+                TRACE_MAX_SECTION_NAME_LENGTH, traceName.length()));
+        Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, shortenedTraceName, /* cookie= */ 0);
+    }
+
+    private void traceEndWaitingForWindowDrawn(WindowState window) {
+        final String traceName = TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD + "#"
+                + window.getWindowTag();
+        final String shortenedTraceName = traceName.substring(0, Math.min(
+                TRACE_MAX_SECTION_NAME_LENGTH, traceName.length()));
+        Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, shortenedTraceName, /* cookie= */ 0);
+    }
+
     void requestTraversal() {
         mWindowPlacerLocked.requestTraversal();
     }
@@ -7785,7 +7829,7 @@
 
         @Override
         public void waitForAllWindowsDrawn(Runnable callback, long timeout, int displayId) {
-            final WindowContainer container = displayId == INVALID_DISPLAY
+            final WindowContainer<?> container = displayId == INVALID_DISPLAY
                     ? mRoot : mRoot.getDisplayContent(displayId);
             if (container == null) {
                 // The waiting container doesn't exist, no need to wait to run the callback. Run and
@@ -7801,6 +7845,12 @@
                 if (container.mWaitingForDrawn.isEmpty()) {
                     allWindowsDrawn = true;
                 } else {
+                    if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                        for (int i = 0; i < container.mWaitingForDrawn.size(); i++) {
+                            traceStartWaitingForWindowDrawn(container.mWaitingForDrawn.get(i));
+                        }
+                    }
+
                     mWaitingForDrawnCallbacks.put(container, callback);
                     mH.sendNewMessageDelayed(H.WAITING_FOR_DRAWN_TIMEOUT, container, timeout);
                     checkDrawnWindowsLocked();
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 94a7cf7..d9eb29d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -971,6 +971,10 @@
                     runSetBooleanFlag(pw, mLetterboxConfiguration
                             ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
                     break;
+                case "--isDisplayAspectRatioEnabledForFixedOrientationLetterbox":
+                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                            ::setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox);
+                    break;
                 case "--isTranslucentLetterboxingEnabled":
                     runSetBooleanFlag(pw, mLetterboxConfiguration
                             ::setTranslucentLetterboxingOverrideEnabled);
@@ -1046,6 +1050,10 @@
                         mLetterboxConfiguration
                                 .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
                         break;
+                    case "IsDisplayAspectRatioEnabledForFixedOrientationLetterbox":
+                        mLetterboxConfiguration
+                                .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
+                        break;
                     case "isTranslucentLetterboxingEnabled":
                         mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
                         break;
@@ -1156,6 +1164,7 @@
             mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
             mLetterboxConfiguration.resetIsEducationEnabled();
             mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+            mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
             mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
             mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
             mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
@@ -1203,7 +1212,9 @@
             pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
                     + mLetterboxConfiguration
                             .getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
-
+            pw.println("Is using display aspect ratio as aspect ratio for all letterboxed apps: "
+                    + mLetterboxConfiguration
+                            .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
             pw.println("    Is activity \"refresh\" in camera compatibility treatment enabled: "
                     + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
             pw.println("    Refresh using \"stopped -> resumed\" cycle: "
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3187337..fd47753 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1949,12 +1949,21 @@
                     creationParams.getPairedPrimaryFragmentToken());
             final int pairedPosition = ownerTask.mChildren.indexOf(pairedPrimaryTaskFragment);
             position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP;
+        } else if (creationParams.getPairedActivityToken() != null) {
+            // When there is a paired Activity, we want to place the new TaskFragment right above
+            // the paired Activity to make sure the Activity position is not changed after reparent.
+            final ActivityRecord pairedActivity = ActivityRecord.forTokenLocked(
+                    creationParams.getPairedActivityToken());
+            final int pairedPosition = ownerTask.mChildren.indexOf(pairedActivity);
+            position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP;
         } else {
             position = POSITION_TOP;
         }
         ownerTask.addChild(taskFragment, position);
         taskFragment.setWindowingMode(creationParams.getWindowingMode());
         taskFragment.setBounds(creationParams.getInitialBounds());
+        // Record the initial relative embedded bounds.
+        taskFragment.updateRelativeEmbeddedBounds();
         mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment);
 
         if (transition != null) transition.collectExistenceChange(taskFragment);
diff --git a/services/core/java/com/android/server/wm/WindowOrientationListener.java b/services/core/java/com/android/server/wm/WindowOrientationListener.java
index 3e165e4..14c816d 100644
--- a/services/core/java/com/android/server/wm/WindowOrientationListener.java
+++ b/services/core/java/com/android/server/wm/WindowOrientationListener.java
@@ -88,14 +88,19 @@
 
     private final Object mLock = new Object();
 
+    @Surface.Rotation
+    private final int mDefaultRotation;
+
     /**
      * Creates a new WindowOrientationListener.
      *
      * @param context for the WindowOrientationListener.
      * @param handler Provides the Looper for receiving sensor updates.
+     * @param defaultRotation Default rotation of the display.
      */
-    public WindowOrientationListener(Context context, Handler handler) {
-        this(context, handler, SensorManager.SENSOR_DELAY_UI);
+    public WindowOrientationListener(Context context, Handler handler,
+            @Surface.Rotation int defaultRotation) {
+        this(context, handler, defaultRotation, SensorManager.SENSOR_DELAY_UI);
     }
 
     /**
@@ -103,7 +108,7 @@
      *
      * @param context for the WindowOrientationListener.
      * @param handler Provides the Looper for receiving sensor updates.
-     * @param wmService WindowManagerService to read the device config from.
+     * @param defaultRotation Default rotation of the display.
      * @param rate at which sensor events are processed (see also
      * {@link android.hardware.SensorManager SensorManager}). Use the default
      * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
@@ -111,10 +116,11 @@
      *
      * This constructor is private since no one uses it.
      */
-    private WindowOrientationListener(
-            Context context, Handler handler, int rate) {
+    private WindowOrientationListener(Context context, Handler handler,
+            @Surface.Rotation int defaultRotation, int rate) {
         mContext = context;
         mHandler = handler;
+        mDefaultRotation = defaultRotation;
         mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
         mRate = rate;
         List<Sensor> l = mSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION);
@@ -1159,7 +1165,7 @@
                                 "Reusing the last rotation resolution: " + mLastRotationResolution);
                         finalizeRotation(mLastRotationResolution);
                     } else {
-                        finalizeRotation(Surface.ROTATION_0);
+                        finalizeRotation(mDefaultRotation);
                     }
                     return;
                 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0469961..52f2b63 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -396,14 +396,6 @@
     int mPrepareSyncSeqId = 0;
 
     /**
-     * {@code true} when the client was still drawing for sync when the sync-set was finished or
-     * cancelled. This can happen if the window goes away during a sync. In this situation we need
-     * to make sure to still apply the postDrawTransaction when it finishes to prevent the client
-     * from getting stuck in a bad state.
-     */
-    boolean mClientWasDrawingForSync = false;
-
-    /**
      * Special mode that is intended only for the rounded corner overlay: during rotation
      * transition, we un-rotate the window token such that the window appears as it did before the
      * rotation.
@@ -2395,7 +2387,11 @@
         // IME parent may failed to attach to the app during rotating the screen.
         // See DisplayContent#shouldImeAttachedToApp, DisplayContent#isImeControlledByApp
         if (windowConfigChanged) {
-            getDisplayContent().updateImeControlTarget();
+            // If the window was the IME layering target, updates the IME surface parent in case
+            // the IME surface may be wrongly positioned when the window configuration affects the
+            // IME surface association. (e.g. Attach IME surface on the display instead of the
+            // app when the app bounds being letterboxed.)
+            mDisplayContent.updateImeControlTarget(isImeLayeringTarget() /* updateImeParent */);
         }
     }
 
@@ -3081,12 +3077,6 @@
         return mLastReportedConfiguration.getMergedConfiguration();
     }
 
-    /** Returns the last window configuration bounds reported to the client. */
-    Rect getLastReportedBounds() {
-        final Rect bounds = getLastReportedConfiguration().windowConfiguration.getBounds();
-        return !bounds.isEmpty() ? bounds : getBounds();
-    }
-
     void adjustStartingWindowFlags() {
         if (mAttrs.type == TYPE_BASE_APPLICATION && mActivityRecord != null
                 && mActivityRecord.mStartingWindow != null) {
@@ -4421,6 +4411,9 @@
             pw.print("null");
         }
 
+        if (mXOffset != 0 || mYOffset != 0) {
+            pw.println(prefix + "mXOffset=" + mXOffset + " mYOffset=" + mYOffset);
+        }
         if (mHScale != 1 || mVScale != 1) {
             pw.println(prefix + "mHScale=" + mHScale
                     + " mVScale=" + mVScale);
@@ -5573,7 +5566,7 @@
                 mSurfacePosition);
 
         if (mWallpaperScale != 1f) {
-            final Rect bounds = getLastReportedBounds();
+            final Rect bounds = getParentFrame();
             Matrix matrix = mTmpMatrix;
             matrix.setTranslate(mXOffset, mYOffset);
             matrix.postScale(mWallpaperScale, mWallpaperScale, bounds.exactCenterX(),
@@ -6019,9 +6012,6 @@
 
     @Override
     void finishSync(Transaction outMergedTransaction, boolean cancel) {
-        if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) {
-            mClientWasDrawingForSync = true;
-        }
         mPrepareSyncSeqId = 0;
         if (cancel) {
             // This is leaving sync so any buffers left in the sync have a chance of
@@ -6039,7 +6029,7 @@
             Slog.i(TAG, "finishDrawing of orientation change: " + this + " " + duration + "ms");
             mOrientationChangeRedrawRequestTime = 0;
         } else if (mActivityRecord != null && mActivityRecord.mRelaunchStartTime != 0
-                && mActivityRecord.findMainWindow() == this) {
+                && mActivityRecord.findMainWindow(false /* includeStartingApp */) == this) {
             final long duration =
                     SystemClock.elapsedRealtime() - mActivityRecord.mRelaunchStartTime;
             Slog.i(TAG, "finishDrawing of relaunch: " + this + " " + duration + "ms");
@@ -6089,9 +6079,7 @@
             layoutNeeded = onSyncFinishedDrawing();
         }
 
-        layoutNeeded |=
-                mWinAnimator.finishDrawingLocked(postDrawTransaction, mClientWasDrawingForSync);
-        mClientWasDrawingForSync = false;
+        layoutNeeded |= mWinAnimator.finishDrawingLocked(postDrawTransaction);
         // We always want to force a traversal after a finish draw for blast sync.
         return !skipLayout && (hasSyncHandlers || layoutNeeded);
     }
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index a0ba8fd..f364248 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -151,17 +151,6 @@
 
     int mAttrType;
 
-    /**
-     * Handles surface changes synchronized to after the client has drawn the surface. This
-     * transaction is currently used to reparent the old surface children to the new surface once
-     * the client has completed drawing to the new surface.
-     * This transaction is also used to merge transactions parceled in by the client. The client
-     * uses the transaction to update the relative z of its children from the old parent surface
-     * to the new parent surface once window manager reparents its children.
-     */
-    private final SurfaceControl.Transaction mPostDrawTransaction =
-            new SurfaceControl.Transaction();
-
     WindowStateAnimator(final WindowState win) {
         final WindowManagerService service = win.mWmService;
 
@@ -217,8 +206,7 @@
         }
     }
 
-    boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction,
-            boolean forceApplyNow) {
+    boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction) {
         final boolean startingWindow =
                 mWin.mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
         if (startingWindow) {
@@ -240,14 +228,7 @@
         }
 
         if (postDrawTransaction != null) {
-            // If there is no surface, the last draw was for the previous surface. We don't want to
-            // wait until the new surface is shown and instead just apply the transaction right
-            // away.
-            if (mLastHidden && mDrawState != NO_SURFACE && !forceApplyNow) {
-                mPostDrawTransaction.merge(postDrawTransaction);
-            } else {
-                mWin.getSyncTransaction().merge(postDrawTransaction);
-            }
+            mWin.getSyncTransaction().merge(postDrawTransaction);
             layoutNeeded = true;
         }
 
@@ -547,7 +528,6 @@
         if (!shown)
             return false;
 
-        t.merge(mPostDrawTransaction);
         return true;
     }
 
@@ -714,10 +694,6 @@
     }
 
     void destroySurface(SurfaceControl.Transaction t) {
-        // Since the SurfaceControl is getting torn down, it's safe to just clean up any
-        // pending transactions that were in mPostDrawTransaction, as well.
-        t.merge(mPostDrawTransaction);
-
         try {
             if (mSurfaceController != null) {
                 mSurfaceController.destroy(t);
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index f628fba..91a1138 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -464,6 +464,22 @@
     </xs:complexType>
 
     <xs:complexType name="refreshRateConfigs">
+        <xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger"
+                    minOccurs="0" maxOccurs="1">
+            <xs:annotation name="final"/>
+        </xs:element>
+        <xs:element name="defaultPeakRefreshRate" type="xs:nonNegativeInteger"
+                    minOccurs="0" maxOccurs="1">
+            <xs:annotation name="final"/>
+        </xs:element>
+        <xs:element name="defaultRefreshRateInHbmHdr" type="xs:nonNegativeInteger"
+                    minOccurs="0" maxOccurs="1">
+            <xs:annotation name="final"/>
+        </xs:element>
+        <xs:element name="defaultRefreshRateInHbmSunlight" type="xs:nonNegativeInteger"
+                    minOccurs="0" maxOccurs="1">
+            <xs:annotation name="final"/>
+        </xs:element>
         <xs:element name="lowerBlockingZoneConfigs" type="blockingZoneConfig"
                     minOccurs="0" maxOccurs="1">
             <xs:annotation name="final"/>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index cb08179..1110d86 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -186,8 +186,16 @@
 
   public class RefreshRateConfigs {
     ctor public RefreshRateConfigs();
+    method public final java.math.BigInteger getDefaultPeakRefreshRate();
+    method public final java.math.BigInteger getDefaultRefreshRate();
+    method public final java.math.BigInteger getDefaultRefreshRateInHbmHdr();
+    method public final java.math.BigInteger getDefaultRefreshRateInHbmSunlight();
     method public final com.android.server.display.config.BlockingZoneConfig getHigherBlockingZoneConfigs();
     method public final com.android.server.display.config.BlockingZoneConfig getLowerBlockingZoneConfigs();
+    method public final void setDefaultPeakRefreshRate(java.math.BigInteger);
+    method public final void setDefaultRefreshRate(java.math.BigInteger);
+    method public final void setDefaultRefreshRateInHbmHdr(java.math.BigInteger);
+    method public final void setDefaultRefreshRateInHbmSunlight(java.math.BigInteger);
     method public final void setHigherBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
     method public final void setLowerBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
   }
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index 9c9b363..7ed9599 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/incremental/TEST_MAPPING b/services/incremental/TEST_MAPPING
index 3976a70..be7feb5 100644
--- a/services/incremental/TEST_MAPPING
+++ b/services/incremental/TEST_MAPPING
@@ -16,13 +16,13 @@
     },
     {
       "name": "service.incremental_test"
+    },
+    {
+      "name": "CtsInstalledLoadingProgressHostTests"
     }
   ],
   "presubmit-large": [
     {
-      "name": "CtsInstalledLoadingProgressHostTests"
-    },
-    {
       "name": "CtsContentTestCases",
       "options": [
         {
diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
index 8a3a44ae..0993295 100644
--- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
+++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
@@ -21,6 +21,7 @@
 import android.annotation.WorkerThread;
 import android.content.Context;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
 import android.net.Uri;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
@@ -149,6 +150,8 @@
 
                 found = true;
             }
+        } catch (SQLiteException exception) {
+            Slog.w("SQLite exception when querying contacts.", exception);
         }
         if (found && lookupKey != null && hasPhoneNumber) {
             return queryPhoneNumber(lookupKey);
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index 693f3a0..37f0624 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -113,6 +113,7 @@
     private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L;
     @VisibleForTesting
     static final int MAX_CACHED_RECENT_SHORTCUTS = 30;
+    private static final int DEBOUNCE_LENGTH_MS = 500;
 
     private final Context mContext;
     private final Injector mInjector;
@@ -129,6 +130,7 @@
     private final List<PeopleService.ConversationsListener> mConversationsListeners =
             new ArrayList<>(1);
     private final Handler mHandler;
+    private final PerPackageThrottler mShortcutsThrottler;
 
     private ContentObserver mCallLogContentObserver;
     private ContentObserver mMmsSmsContentObserver;
@@ -140,14 +142,17 @@
     private ConversationStatusExpirationBroadcastReceiver mStatusExpReceiver;
 
     public DataManager(Context context) {
-        this(context, new Injector(), BackgroundThread.get().getLooper());
+        this(context, new Injector(), BackgroundThread.get().getLooper(),
+                new PerPackageThrottlerImpl(BackgroundThread.getHandler(), DEBOUNCE_LENGTH_MS));
     }
 
-    DataManager(Context context, Injector injector, Looper looper) {
+    DataManager(Context context, Injector injector, Looper looper,
+            PerPackageThrottler shortcutsThrottler) {
         mContext = context;
         mInjector = injector;
         mScheduledExecutor = mInjector.createScheduledExecutor();
         mHandler = new Handler(looper);
+        mShortcutsThrottler = shortcutsThrottler;
     }
 
     /** Initialization. Called when the system services are up running. */
@@ -271,22 +276,22 @@
     private ConversationChannel getConversationChannel(String packageName, int userId,
             String shortcutId, ConversationInfo conversationInfo) {
         ShortcutInfo shortcutInfo = getShortcut(packageName, userId, shortcutId);
-        return getConversationChannel(shortcutInfo, conversationInfo);
+        return getConversationChannel(
+                shortcutInfo, conversationInfo, packageName, userId, shortcutId);
     }
 
     @Nullable
     private ConversationChannel getConversationChannel(ShortcutInfo shortcutInfo,
-            ConversationInfo conversationInfo) {
+            ConversationInfo conversationInfo, String packageName, int userId, String shortcutId) {
         if (conversationInfo == null || conversationInfo.isDemoted()) {
             return null;
         }
         if (shortcutInfo == null) {
-            Slog.e(TAG, " Shortcut no longer found");
+            Slog.e(TAG, "Shortcut no longer found");
+            mInjector.getBackgroundExecutor().execute(
+                    () -> removeConversations(packageName, userId, Set.of(shortcutId)));
             return null;
         }
-        String packageName = shortcutInfo.getPackage();
-        String shortcutId = shortcutInfo.getId();
-        int userId = shortcutInfo.getUserId();
         int uid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
         NotificationChannel parentChannel =
                 mNotificationManagerInternal.getNotificationChannel(packageName, uid,
@@ -851,12 +856,12 @@
         // pair of <package name, conversation info>
         List<Pair<String, ConversationInfo>> cachedConvos = new ArrayList<>();
         userData.forAllPackages(packageData -> {
-                packageData.forAllConversations(conversationInfo -> {
-                    if (isEligibleForCleanUp(conversationInfo)) {
-                        cachedConvos.add(
-                                Pair.create(packageData.getPackageName(), conversationInfo));
-                    }
-                });
+            packageData.forAllConversations(conversationInfo -> {
+                if (isEligibleForCleanUp(conversationInfo)) {
+                    cachedConvos.add(
+                            Pair.create(packageData.getPackageName(), conversationInfo));
+                }
+            });
         });
         if (cachedConvos.size() <= targetCachedCount) {
             return;
@@ -867,8 +872,8 @@
                 numToUncache + 1,
                 Comparator.comparingLong((Pair<String, ConversationInfo> pair) ->
                         Math.max(
-                            pair.second.getLastEventTimestamp(),
-                            pair.second.getCreationTimestamp())).reversed());
+                                pair.second.getLastEventTimestamp(),
+                                pair.second.getCreationTimestamp())).reversed());
         for (Pair<String, ConversationInfo> cached : cachedConvos) {
             if (hasActiveNotifications(cached.first, userId, cached.second.getShortcutId())) {
                 continue;
@@ -1104,64 +1109,76 @@
         @Override
         public void onShortcutsAddedOrUpdated(@NonNull String packageName,
                 @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
-            mInjector.getBackgroundExecutor().execute(() -> {
-                PackageData packageData = getPackage(packageName, user.getIdentifier());
-                for (ShortcutInfo shortcut : shortcuts) {
-                    if (ShortcutHelper.isConversationShortcut(
-                            shortcut, mShortcutServiceInternal, user.getIdentifier())) {
-                        if (shortcut.isCached()) {
-                            ConversationInfo conversationInfo = packageData != null
-                                    ? packageData.getConversationInfo(shortcut.getId()) : null;
-                            if (conversationInfo == null
-                                    || !conversationInfo.isShortcutCachedForNotification()) {
-                                // This is a newly cached shortcut. Clean up the existing cached
-                                // shortcuts to ensure the cache size is under the limit.
-                                cleanupCachedShortcuts(user.getIdentifier(),
-                                        MAX_CACHED_RECENT_SHORTCUTS - 1);
+            mShortcutsThrottler.scheduleDebounced(
+                    new Pair<>(packageName, user.getIdentifier()),
+                    () -> {
+                        PackageData packageData = getPackage(packageName, user.getIdentifier());
+                        List<ShortcutInfo> queriedShortcuts = getShortcuts(packageName,
+                                user.getIdentifier(), null);
+                        boolean hasCachedShortcut = false;
+                        for (ShortcutInfo shortcut : queriedShortcuts) {
+                            if (ShortcutHelper.isConversationShortcut(
+                                    shortcut, mShortcutServiceInternal, user.getIdentifier())) {
+                                if (shortcut.isCached()) {
+                                    ConversationInfo info = packageData != null
+                                            ? packageData.getConversationInfo(shortcut.getId())
+                                            : null;
+                                    if (info == null
+                                            || !info.isShortcutCachedForNotification()) {
+                                        hasCachedShortcut = true;
+                                    }
+                                }
+                                addOrUpdateConversationInfo(shortcut);
                             }
                         }
-                        addOrUpdateConversationInfo(shortcut);
-                    }
-                }
-            });
+                        // Added at least one new conversation. Uncache older existing cached
+                        // shortcuts to ensure the cache size is under the limit.
+                        if (hasCachedShortcut) {
+                            cleanupCachedShortcuts(user.getIdentifier(),
+                                    MAX_CACHED_RECENT_SHORTCUTS);
+                        }
+                    });
         }
 
         @Override
         public void onShortcutsRemoved(@NonNull String packageName,
                 @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
             mInjector.getBackgroundExecutor().execute(() -> {
-                int uid = Process.INVALID_UID;
-                try {
-                    uid = mContext.getPackageManager().getPackageUidAsUser(
-                            packageName, user.getIdentifier());
-                } catch (PackageManager.NameNotFoundException e) {
-                    Slog.e(TAG, "Package not found: " + packageName, e);
-                }
-                PackageData packageData = getPackage(packageName, user.getIdentifier());
-                Set<String> shortcutIds = new HashSet<>();
+                HashSet<String> shortcutIds = new HashSet<>();
                 for (ShortcutInfo shortcutInfo : shortcuts) {
-                    if (packageData != null) {
-                        if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutInfo.getId());
-                        packageData.deleteDataForConversation(shortcutInfo.getId());
-                    }
                     shortcutIds.add(shortcutInfo.getId());
                 }
-                if (uid != Process.INVALID_UID) {
-                    mNotificationManagerInternal.onConversationRemoved(
-                            packageName, uid, shortcutIds);
-                }
+                removeConversations(packageName, user.getIdentifier(), shortcutIds);
             });
         }
     }
 
+    private void removeConversations(
+            @NonNull String packageName, @NonNull int userId, @NonNull Set<String> shortcutIds) {
+        PackageData packageData = getPackage(packageName, userId);
+        if (packageData != null) {
+            for (String shortcutId : shortcutIds) {
+                if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutId);
+                packageData.deleteDataForConversation(shortcutId);
+            }
+        }
+        try {
+            int uid = mContext.getPackageManager().getPackageUidAsUser(
+                    packageName, userId);
+            mNotificationManagerInternal.onConversationRemoved(packageName, uid, shortcutIds);
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(TAG, "Package not found when removing conversation: " + packageName, e);
+        }
+    }
+
     /** Listener for the notifications and their settings changes. */
     private class NotificationListener extends NotificationListenerService {
 
         private final int mUserId;
 
-        // Conversation package name + shortcut ID -> Number of active notifications
+        // Conversation package name + shortcut ID -> Keys of active notifications
         @GuardedBy("this")
-        private final Map<Pair<String, String>, Integer> mActiveNotifCounts = new ArrayMap<>();
+        private final Map<Pair<String, String>, Set<String>> mActiveNotifKeys = new ArrayMap<>();
 
         private NotificationListener(int userId) {
             mUserId = userId;
@@ -1175,8 +1192,10 @@
             String shortcutId = sbn.getNotification().getShortcutId();
             PackageData packageData = getPackageIfConversationExists(sbn, conversationInfo -> {
                 synchronized (this) {
-                    mActiveNotifCounts.merge(
-                            Pair.create(sbn.getPackageName(), shortcutId), 1, Integer::sum);
+                    Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent(
+                            Pair.create(sbn.getPackageName(), shortcutId),
+                            (unusedKey) -> new HashSet<>());
+                    notificationKeys.add(sbn.getKey());
                 }
             });
 
@@ -1215,12 +1234,12 @@
                 Pair<String, String> conversationKey =
                         Pair.create(sbn.getPackageName(), shortcutId);
                 synchronized (this) {
-                    int count = mActiveNotifCounts.getOrDefault(conversationKey, 0) - 1;
-                    if (count <= 0) {
-                        mActiveNotifCounts.remove(conversationKey);
+                    Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent(
+                            conversationKey, (unusedKey) -> new HashSet<>());
+                    notificationKeys.remove(sbn.getKey());
+                    if (notificationKeys.isEmpty()) {
+                        mActiveNotifKeys.remove(conversationKey);
                         cleanupCachedShortcuts(mUserId, MAX_CACHED_RECENT_SHORTCUTS);
-                    } else {
-                        mActiveNotifCounts.put(conversationKey, count);
                     }
                 }
             });
@@ -1286,7 +1305,7 @@
         }
 
         synchronized boolean hasActiveNotifications(String packageName, String shortcutId) {
-            return mActiveNotifCounts.containsKey(Pair.create(packageName, shortcutId));
+            return mActiveNotifKeys.containsKey(Pair.create(packageName, shortcutId));
         }
     }
 
@@ -1349,9 +1368,11 @@
     }
 
     private void updateConversationStoreThenNotifyListeners(ConversationStore cs,
-            ConversationInfo modifiedConv, ShortcutInfo shortcutInfo) {
+            ConversationInfo modifiedConv, @NonNull ShortcutInfo shortcutInfo) {
         cs.addOrUpdate(modifiedConv);
-        ConversationChannel channel = getConversationChannel(shortcutInfo, modifiedConv);
+        ConversationChannel channel = getConversationChannel(
+                shortcutInfo, modifiedConv, shortcutInfo.getPackage(), shortcutInfo.getUserId(),
+                shortcutInfo.getId());
         if (channel != null) {
             notifyConversationsListeners(Arrays.asList(channel));
         }
diff --git a/services/people/java/com/android/server/people/data/PerPackageThrottler.java b/services/people/java/com/android/server/people/data/PerPackageThrottler.java
new file mode 100644
index 0000000..3d6cd84
--- /dev/null
+++ b/services/people/java/com/android/server/people/data/PerPackageThrottler.java
@@ -0,0 +1,28 @@
+/*
+ * 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.server.people.data;
+
+import android.util.Pair;
+
+/** The interface for throttling expensive runnables per package. */
+interface PerPackageThrottler {
+    /**
+     * Schedule a runnable to run in the future, and debounce runnables for same {@code pkgUserId}
+     * that occur until that future has run.
+     */
+    void scheduleDebounced(Pair<String, Integer> pkgUserId, Runnable runnable);
+}
diff --git a/services/people/java/com/android/server/people/data/PerPackageThrottlerImpl.java b/services/people/java/com/android/server/people/data/PerPackageThrottlerImpl.java
new file mode 100644
index 0000000..fa5a67b
--- /dev/null
+++ b/services/people/java/com/android/server/people/data/PerPackageThrottlerImpl.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.people.data;
+
+import android.os.Handler;
+import android.util.Pair;
+
+import java.util.HashSet;
+
+/**
+ * A class that implements a per-package throttler that prevents a runnable from executing more than
+ * once every {@code debounceTime}.
+ */
+public class PerPackageThrottlerImpl implements PerPackageThrottler {
+    private final Handler mBackgroundHandler;
+    private final int mDebounceTime;
+    private final HashSet<Pair<String, Integer>> mPkgScheduledTasks = new HashSet<>();
+
+    PerPackageThrottlerImpl(Handler backgroundHandler, int debounceTime) {
+        mBackgroundHandler = backgroundHandler;
+        mDebounceTime = debounceTime;
+    }
+
+    @Override
+    public synchronized void scheduleDebounced(
+            Pair<String, Integer> pkgUserId, Runnable runnable) {
+        if (mPkgScheduledTasks.contains(pkgUserId)) {
+            return;
+        }
+        mPkgScheduledTasks.add(pkgUserId);
+        mBackgroundHandler.postDelayed(() -> {
+            synchronized (this) {
+                mPkgScheduledTasks.remove(pkgUserId);
+                runnable.run();
+            }
+        }, mDebounceTime);
+    }
+}
diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml
index 33ac735..ea0481e 100644
--- a/services/tests/mockingservicestests/AndroidManifest.xml
+++ b/services/tests/mockingservicestests/AndroidManifest.xml
@@ -43,6 +43,9 @@
     <!-- needed by TrustManagerServiceTest to access LockSettings' secure storage -->
     <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" />
 
+    <!-- needed by GameManagerServiceTest because GameManager creates a UidObserver -->
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+
     <application android:testOnly="true"
                  android:debuggable="true">
         <uses-library android:name="android.test.runner" />
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/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index fa4a9de..2d5f0b0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -29,13 +29,16 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.Manifest;
+import android.app.ActivityManager;
 import android.app.GameManager;
 import android.app.GameModeInfo;
 import android.app.GameState;
@@ -203,6 +206,23 @@
         LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
     }
 
+    private void mockAppCategory(String packageName, @ApplicationInfo.Category int category)
+            throws Exception {
+        reset(mMockPackageManager);
+        final ApplicationInfo gameApplicationInfo = new ApplicationInfo();
+        gameApplicationInfo.category = category;
+        gameApplicationInfo.packageName = packageName;
+        final PackageInfo pi = new PackageInfo();
+        pi.packageName = packageName;
+        pi.applicationInfo = gameApplicationInfo;
+        final List<PackageInfo> packages = new ArrayList<>();
+        packages.add(pi);
+        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
+            .thenReturn(packages);
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+            .thenReturn(gameApplicationInfo);
+    }
+
     @After
     public void tearDown() throws Exception {
         LocalServices.removeServiceForTest(PowerManagerInternal.class);
@@ -1597,4 +1617,113 @@
                 ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
                 ArgumentMatchers.eq(0.0f));
     }
+
+    private GameManagerService createServiceAndStartUser(int userId) {
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
+        startUser(gameManagerService, userId);
+        return gameManagerService;
+    }
+
+    @Test
+    public void testGamePowerMode_gamePackage() throws Exception {
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages = {mPackageName};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+    }
+
+    @Test
+    public void testGamePowerMode_twoGames() throws Exception {
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages1 = {mPackageName};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
+        String someGamePkg = "some.game";
+        String[] packages2 = {someGamePkg};
+        int somePackageId = DEFAULT_PACKAGE_UID + 1;
+        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        HashMap<Integer, Boolean> powerState = new HashMap<>();
+        doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1)))
+                .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean());
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        assertTrue(powerState.get(Mode.GAME));
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        assertTrue(powerState.get(Mode.GAME));
+        gameManagerService.mUidObserver.onUidStateChanged(
+                somePackageId, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        assertFalse(powerState.get(Mode.GAME));
+    }
+
+    @Test
+    public void testGamePowerMode_twoGamesOverlap() throws Exception {
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages1 = {mPackageName};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
+        String someGamePkg = "some.game";
+        String[] packages2 = {someGamePkg};
+        int somePackageId = DEFAULT_PACKAGE_UID + 1;
+        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                somePackageId, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+    }
+
+    @Test
+    public void testGamePowerMode_released() throws Exception {
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages = {mPackageName};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+    }
+
+    @Test
+    public void testGamePowerMode_noPackage() throws Exception {
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages = {};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+    }
+
+    @Test
+    public void testGamePowerMode_notAGamePackage() throws Exception {
+        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages = {"someapp"};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+    }
+
+    @Test
+    public void testGamePowerMode_notAGamePackageNotReleased() throws Exception {
+        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        String[] packages = {"someapp"};
+        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, false);
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
index e4f9eaf..9b23f8b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
@@ -1089,8 +1089,7 @@
             Consumer<Uri> consumer = invocation.getArgument(invocation.getArguments().length - 1);
             consumer.accept(Uri.parse("a/b.png"));
             return null;
-        }).when(mMockScreenshotHelper).provideScreenshot(
-                any(), any(), any(), anyInt(), anyInt(), any(), anyInt(), any(), any());
+        }).when(mMockScreenshotHelper).takeScreenshot(any(), any(), any());
         mGameServiceProviderInstance.start();
         startTask(taskId, GAME_A_MAIN_ACTIVITY);
         mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index c15f6a9..5792ecb 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -17,7 +17,6 @@
 package com.android.server.accessibility;
 
 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
-import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.is;
@@ -301,9 +300,7 @@
         mSystemActionPerformer.performSystemAction(
                 AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
         verify(mMockScreenshotHelper).takeScreenshot(
-                eq(TAKE_SCREENSHOT_FULLSCREEN),
-                eq(SCREENSHOT_ACCESSIBILITY_ACTIONS),
-                any(Handler.class), any());
+                eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any());
     }
 
     // PendingIntent is a final class and cannot be mocked. So we are using this
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index dad9fe8..31599ee 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -74,7 +74,7 @@
         mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
         mSpySystemServer = spy(new NoOpSystemServerAdapter());
         mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory,
-                mSpySystemServer);
+                mSpySystemServer, mSpyAudioSystem);
         mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
 
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 666d401..4915c64 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -35,13 +37,13 @@
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.content.ComponentName;
+import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.Fingerprint;
-import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -337,6 +339,21 @@
         showHideOverlay(c -> c.onLockoutPermanent());
     }
 
+    @Test
+    public void testPowerPressForwardsErrorMessage() throws RemoteException {
+        final FingerprintAuthenticationClient client = createClient();
+        final int testVendorPowerPressCode = 1;
+        when(mContext.getOrCreateTestableResources().getResources()
+                .getBoolean(R.bool.config_powerPressMapping)).thenReturn(true);
+        when(mContext.getOrCreateTestableResources().getResources()
+                .getInteger(R.integer.config_powerPressCode)).thenReturn(testVendorPowerPressCode);
+
+        client.onError(FINGERPRINT_ERROR_VENDOR, testVendorPowerPressCode);
+
+        verify(mClientMonitorCallbackConverter).onError(anyInt(), anyInt(),
+                eq(BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED), anyInt());
+    }
+
     private void showHideOverlay(Consumer<FingerprintAuthenticationClient> block)
             throws RemoteException {
         final FingerprintAuthenticationClient client = createClient();
@@ -369,274 +386,6 @@
         verify(mCancellationSignal).cancel();
     }
 
-    @Test
-    public void fingerprintPowerIgnoresAuthInWindow() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        client.onPowerPressed();
-        client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, 2 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.moveTimeForward(1000);
-        mLooper.dispatchAll();
-
-        verify(mCallback).onClientFinished(any(), eq(false));
-        verify(mCancellationSignal).cancel();
-    }
-
-    @Test
-    public void fingerprintAuthIgnoredWaitingForPower() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        client.onAuthenticated(new Fingerprint("friendly", 3 /* fingerId */, 4 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        client.onPowerPressed();
-        mLooper.moveTimeForward(1000);
-        mLooper.dispatchAll();
-
-        verify(mCallback).onClientFinished(any(), eq(false));
-        verify(mCancellationSignal).cancel();
-    }
-
-    @Test
-    public void fingerprintAuthFailsWhenAuthAfterPower() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        client.onPowerPressed();
-        mLooper.dispatchAll();
-        mLooper.moveTimeForward(1000);
-        mLooper.dispatchAll();
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-        mLooper.moveTimeForward(1000);
-        mLooper.dispatchAll();
-
-        verify(mCallback, never()).onClientFinished(any(), eq(true));
-        verify(mCallback).onClientFinished(any(), eq(false));
-        when(mHal.authenticateWithContext(anyLong(), any())).thenReturn(mCancellationSignal);
-    }
-
-    @Test
-    public void sideFingerprintDoesntSendAuthImmediately() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        mLooper.dispatchAll();
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-
-        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-    }
-
-    @Test
-    public void sideFingerprintSkipsWindowIfFingerUp() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        mLooper.dispatchAll();
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        client.onAcquired(FINGER_UP, 0);
-        mLooper.dispatchAll();
-
-        verify(mCallback).onClientFinished(any(), eq(true));
-    }
-
-    @Test
-    public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        final int vendorAcquireMessage = 1234;
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
-                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
-                vendorAcquireMessage);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        mLooper.dispatchAll();
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage);
-        mLooper.dispatchAll();
-
-        verify(mCallback).onClientFinished(any(), eq(true));
-    }
-
-    @Test
-    public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        final int vendorAcquireMessage = 1234;
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
-                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
-                vendorAcquireMessage);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        mLooper.dispatchAll();
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1);
-        mLooper.dispatchAll();
-
-        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-    }
-
-    @Test
-    public void sideFingerprintSendsAuthIfFingerUp() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        mLooper.dispatchAll();
-        client.onAcquired(FINGER_UP, 0);
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-
-        verify(mCallback).onClientFinished(any(), eq(true));
-    }
-
-    @Test
-    public void sideFingerprintShortCircuitExpires() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
-        final int timeBeforeAuthSent = 500;
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsKeyguardPowerPressWindow, timeBeforeAuthSent);
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        mLooper.dispatchAll();
-        client.onAcquired(FINGER_UP, 0);
-        mLooper.dispatchAll();
-
-        mLooper.moveTimeForward(500);
-        mLooper.dispatchAll();
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
-        mLooper.moveTimeForward(500);
-        mLooper.dispatchAll();
-        verify(mCallback).onClientFinished(any(), eq(true));
-    }
-
-    @Test
-    public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception {
-        final int powerWindow = 500;
-        final long authStart = 300;
-
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-
-        // Acquire start occurs at time = 0ms
-        when(mClock.millis()).thenReturn(0L);
-        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
-
-        // Auth occurs at time = 300
-        when(mClock.millis()).thenReturn(authStart);
-        // At this point the delay should be 500 - (300 - 0) == 200 milliseconds.
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
-        // After waiting 200 milliseconds, auth should succeed.
-        mLooper.moveTimeForward(powerWindow - authStart);
-        mLooper.dispatchAll();
-        verify(mCallback).onClientFinished(any(), eq(true));
-    }
-
-    @Test
-    public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception {
-        final int powerWindow = 500;
-
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-        // Acquire start occurs at time = 0ms
-        when(mClock.millis()).thenReturn(0L);
-        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
-
-        // Auth reject occurs at time = 300ms
-        when(mClock.millis()).thenReturn(300L);
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                false /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-
-        mLooper.moveTimeForward(300);
-        mLooper.dispatchAll();
-        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
-        when(mClock.millis()).thenReturn(1300L);
-        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
-
-        // If code is correct, the new acquired start timestamp should be used
-        // and the code should only have to wait 500 - (1500-1300)ms.
-        when(mClock.millis()).thenReturn(1500L);
-        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
-                true /* authenticated */, new ArrayList<>());
-        mLooper.dispatchAll();
-
-        mLooper.moveTimeForward(299);
-        mLooper.dispatchAll();
-        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
-        mLooper.moveTimeForward(1);
-        mLooper.dispatchAll();
-        verify(mCallback).onClientFinished(any(), eq(true));
-    }
-
-    @Test
-    public void sideFpsPowerPressCancelsIsntantly() throws Exception {
-        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
-        final FingerprintAuthenticationClient client = createClient(1);
-        client.start(mCallback);
-
-        client.onPowerPressed();
-        mLooper.dispatchAll();
-
-        verify(mCallback, never()).onClientFinished(any(), eq(true));
-        verify(mCallback).onClientFinished(any(), eq(false));
-    }
-
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index 837b553..7e29a76 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -28,10 +28,10 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.same;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.PointerContext;
@@ -48,6 +48,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.R;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.CallbackWithProbe;
@@ -66,7 +67,6 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
-import java.util.ArrayList;
 import java.util.function.Consumer;
 
 @Presubmit
@@ -258,11 +258,16 @@
     @Test
     public void testPowerPressForwardsAcquireMessage() throws RemoteException {
         final FingerprintEnrollClient client = createClient();
-        client.start(mCallback);
-        client.onPowerPressed();
+        final int testVendorPowerPressCode = 1;
+        when(mContext.getOrCreateTestableResources().getResources()
+                .getBoolean(R.bool.config_powerPressMapping)).thenReturn(true);
+        when(mContext.getOrCreateTestableResources().getResources()
+                .getInteger(R.integer.config_powerPressCode)).thenReturn(testVendorPowerPressCode);
+
+        client.onAcquired(FINGERPRINT_ACQUIRED_VENDOR, testVendorPowerPressCode);
 
         verify(mClientMonitorCallbackConverter).onAcquired(anyInt(),
-                eq(FINGERPRINT_ACQUIRED_POWER_PRESSED), anyInt());
+                eq(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED), anyInt());
     }
 
     private void showHideOverlay(Consumer<FingerprintEnrollClient> block)
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStatePolicyProviderTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStatePolicyProviderTest.java
index 0bd81b7..18dc35c 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStatePolicyProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStatePolicyProviderTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.devicestate;
 
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
@@ -24,8 +26,6 @@
 import android.content.res.Resources;
 import android.platform.test.annotations.Presubmit;
 
-import org.hamcrest.Matchers;
-import org.junit.Assert;
 import org.junit.Test;
 
 /**
@@ -39,37 +39,35 @@
 
     @Test
     public void test_emptyPolicyProvider() {
-        Assert.assertThat(DeviceStatePolicy.Provider.fromResources(resourcesWithProvider("")),
-                Matchers.instanceOf(DeviceStatePolicy.DefaultProvider.class));
+        assertThat(DeviceStatePolicy.Provider.fromResources(resourcesWithProvider("")),
+                instanceOf(DeviceStatePolicy.DefaultProvider.class));
     }
 
     @Test
     public void test_nullPolicyProvider() {
-        Assert.assertThat(DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(null)),
-                Matchers.instanceOf(DeviceStatePolicy.DefaultProvider.class));
+        assertThat(DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(null)),
+                instanceOf(DeviceStatePolicy.DefaultProvider.class));
     }
 
     @Test
     public void test_customPolicyProvider() {
-        Assert.assertThat(DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(
-                TestProvider.class.getName())),
-                Matchers.instanceOf(TestProvider.class));
+        assertThat(DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(
+                        TestProvider.class.getName())),
+                instanceOf(TestProvider.class));
     }
 
     @Test
     public void test_badPolicyProvider_notImplementingProviderInterface() {
-        assertThrows(IllegalStateException.class, () -> {
-            DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(
-                    Object.class.getName()));
-        });
+        assertThrows(IllegalStateException.class, () ->
+                DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(
+                        Object.class.getName())));
     }
 
     @Test
-    public void test_badPolicyProvider_doesntExist() {
-        assertThrows(IllegalStateException.class, () -> {
-            DeviceStatePolicy.Provider.fromResources(resourcesWithProvider(
-                    "com.android.devicestate.nonexistent.policy"));
-        });
+    public void test_badPolicyProvider_returnsDefault() {
+        assertThat(DeviceStatePolicy.Provider.fromResources(
+                        resourcesWithProvider("com.android.devicestate.nonexistent.policy")),
+                instanceOf(DeviceStatePolicy.DefaultProvider.class));
     }
 
     private static Resources resourcesWithProvider(String provider) {
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 86c5937..8f70617 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -51,6 +51,10 @@
 public final class DisplayDeviceConfigTest {
     private static final int DEFAULT_PEAK_REFRESH_RATE = 75;
     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};
@@ -150,8 +154,12 @@
 
         assertEquals("ProximitySensor123", mDisplayDeviceConfig.getProximitySensor().name);
         assertEquals("prox_type_1", mDisplayDeviceConfig.getProximitySensor().type);
-        assertEquals(75, mDisplayDeviceConfig.getDefaultLowRefreshRate());
-        assertEquals(90, mDisplayDeviceConfig.getDefaultHighRefreshRate());
+        assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate());
+        assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate());
+        assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate());
+        assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate());
+        assertEquals(82, mDisplayDeviceConfig.getDefaultRefreshRateInHbmHdr());
+        assertEquals(83, mDisplayDeviceConfig.getDefaultRefreshRateInHbmSunlight());
         assertArrayEquals(new int[]{45, 55},
                 mDisplayDeviceConfig.getLowDisplayBrightnessThresholds());
         assertArrayEquals(new int[]{50, 60},
@@ -230,8 +238,16 @@
                 mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
         assertArrayEquals(new float[]{29, 30, 31},
                 mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
-        assertEquals(mDisplayDeviceConfig.getDefaultLowRefreshRate(), DEFAULT_REFRESH_RATE);
-        assertEquals(mDisplayDeviceConfig.getDefaultHighRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
+        assertEquals(mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(),
+                DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE);
+        assertEquals(mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate(),
+                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(),
@@ -449,6 +465,10 @@
                 +       "<type>prox_type_1</type>\n"
                 +   "</proxSensor>\n"
                 +   "<refreshRate>\n"
+                +       "<defaultRefreshRate>45</defaultRefreshRate>\n"
+                +       "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n"
+                +       "<defaultRefreshRateInHbmHdr>82</defaultRefreshRateInHbmHdr>\n"
+                +       "<defaultRefreshRateInHbmSunlight>83</defaultRefreshRateInHbmSunlight>\n"
                 +       "<lowerBlockingZoneConfigs>\n"
                 +           "<defaultRefreshRate>75</defaultRefreshRate>\n"
                 +           "<blockingZoneThreshold>\n"
@@ -550,10 +570,14 @@
                 .thenReturn(new int[]{370, 380, 390});
 
         // Configs related to refresh rates and blocking zones
-        when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate))
+        when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate))
                 .thenReturn(DEFAULT_PEAK_REFRESH_RATE);
-        when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate))
+        when(mResources.getInteger(R.integer.config_defaultRefreshRate))
                 .thenReturn(DEFAULT_REFRESH_RATE);
+        when(mResources.getInteger(R.integer.config_fixedRefreshRateInHighZone))
+            .thenReturn(DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
+        when(mResources.getInteger(R.integer.config_defaultRefreshRateInZone))
+            .thenReturn(DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE);
         when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
                 .thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
         when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
@@ -564,6 +588,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/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 2edb909..52fade1 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -424,6 +424,37 @@
     }
 
     /**
+     * Tests that HighBrightnessModeMetadata is non-null on all display devices.
+     */
+    @Test
+    public void testHighBrightnessModeMetadataNonNull() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        // Add the FakeDisplayDevice
+        FakeDisplayDevice displayDevice = new FakeDisplayDevice("unique_hbm_device");
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+
+        LogicalDisplay logicalDisplay = new LogicalDisplay(1, 1, displayDevice);
+        HighBrightnessModeMetadata hbmMeta =
+                displayManager.getHighBrightnessModeMetadata(logicalDisplay);
+
+        assertNotNull(hbmMeta);
+
+        // Check is Hbm metadata is correctly added for the display device.
+        String uniqueId = displayDevice.getUniqueId();
+        assertTrue(uniqueId.equals("unique_hbm_device"));
+        assertTrue(displayManager.mHighBrightnessModeMetadataMap.containsKey(uniqueId));
+        HighBrightnessModeMetadata hbmMetaFromMap =
+                displayManager.mHighBrightnessModeMetadataMap.get(uniqueId);
+        assertEquals(hbmMeta, hbmMetaFromMap);
+    }
+
+    /**
      * Tests that we get a Runtime exception when we cannot initialize the default display.
      */
     @Test
@@ -1349,6 +1380,11 @@
             super(null, null, "", mContext);
         }
 
+        FakeDisplayDevice(String uniqueDeviceId) {
+            super(null, null, uniqueDeviceId, mContext);
+        }
+
+
         public void setDisplayDeviceInfo(DisplayDeviceInfo displayDeviceInfo) {
             mDisplayDeviceInfo = displayDeviceInfo;
         }
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 b133a2a..ff37564 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -1869,6 +1869,14 @@
             .thenReturn(75);
         when(resources.getInteger(R.integer.config_defaultRefreshRate))
             .thenReturn(45);
+        when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone))
+            .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))
@@ -1879,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
@@ -1888,6 +1909,10 @@
         assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 45, 0.0);
         assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 75,
                 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(),
@@ -1897,19 +1922,26 @@
         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.getDefaultLowRefreshRate()).thenReturn(50);
-        when(displayDeviceConfig.getDefaultHighRefreshRate()).thenReturn(55);
+        when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
+        when(displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
+        when(displayDeviceConfig.getDefaultRefreshRate()).thenReturn(60);
+        when(displayDeviceConfig.getDefaultPeakRefreshRate()).thenReturn(65);
         when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
         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(), 50, 0.0);
-        assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 55,
+        assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
+        assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 65,
                 0.0);
+        assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 55);
+        assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 50);
         assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
                 new int[]{210});
         assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -1918,20 +1950,27 @@
                 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();
         config.setDefaultPeakRefreshRate(60);
+        config.setRefreshRateInHighZone(65);
+        config.setRefreshRateInLowZone(70);
         config.setLowAmbientBrightnessThresholds(new int[]{20});
         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(), 50, 0.0);
+        assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
         assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 60,
                 0.0);
+        assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65);
+        assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 70);
         assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
                 new int[]{255});
         assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -1940,6 +1979,8 @@
                 new int[]{10});
         assertArrayEquals(director.getBrightnessObserver().getLowAmbientBrightnessThreshold(),
                 new int[]{20});
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmHdr(), 70);
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmSunlight(), 80);
     }
 
     @Test
@@ -1971,8 +2012,8 @@
                         any(Handler.class));
 
         DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class);
-        when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50);
-        when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55);
+        when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
+        when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
         when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
         when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
         when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
@@ -1997,6 +2038,74 @@
                 eq(lightSensorTwo), anyInt(), any(Handler.class));
     }
 
+    @Test
+    public void testAuthenticationPossibleSetsPhysicalRateRangesToMax() throws RemoteException {
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+        // don't call director.start(createMockSensorManager());
+        // DisplayObserver will reset mSupportedModesByDisplay
+        director.onBootCompleted();
+        ArgumentCaptor<IUdfpsHbmListener> captor =
+                ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+        verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+
+        captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+        assertThat(vote.refreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(vote.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+    }
+
+    @Test
+    public void testAuthenticationPossibleUnsetsVote() throws RemoteException {
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+        director.start(createMockSensorManager());
+        director.onBootCompleted();
+        ArgumentCaptor<IUdfpsHbmListener> captor =
+                ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+        verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+        captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+        captor.getValue().onAuthenticationPossible(DISPLAY_ID, false);
+
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+        assertNull(vote);
+    }
+
+    @Test
+    public void testUdfpsRequestSetsPhysicalRateRangesToMax() throws RemoteException {
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+        // don't call director.start(createMockSensorManager());
+        // DisplayObserver will reset mSupportedModesByDisplay
+        director.onBootCompleted();
+        ArgumentCaptor<IUdfpsHbmListener> captor =
+                ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+        verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+
+        captor.getValue().onHbmEnabled(DISPLAY_ID);
+
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+        assertThat(vote.refreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(vote.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+    }
+
+    @Test
+    public void testUdfpsRequestUnsetsUnsetsVote() throws RemoteException {
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+        director.start(createMockSensorManager());
+        director.onBootCompleted();
+        ArgumentCaptor<IUdfpsHbmListener> captor =
+                ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+        verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+        captor.getValue().onHbmEnabled(DISPLAY_ID);
+        captor.getValue().onHbmEnabled(DISPLAY_ID);
+
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+        assertNull(vote);
+    }
+
     private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
         return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
     }
diff --git a/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java
new file mode 100644
index 0000000..24fc348
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertEquals;
+
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class HbmEventTest {
+    private long mStartTimeMillis;
+    private long mEndTimeMillis;
+    private HbmEvent mHbmEvent;
+
+    @Before
+    public void setUp() {
+        mStartTimeMillis = 10;
+        mEndTimeMillis = 20;
+        mHbmEvent = new HbmEvent(mStartTimeMillis, mEndTimeMillis);
+    }
+
+    @Test
+    public void getCorrectValues() {
+        assertEquals(mHbmEvent.getStartTimeMillis(), mStartTimeMillis);
+        assertEquals(mHbmEvent.getEndTimeMillis(), mEndTimeMillis);
+    }
+
+    @Test
+    public void toStringGeneratesExpectedString() {
+        String actualString = mHbmEvent.toString();
+        String expectedString = "HbmEvent: {startTimeMillis:" + mStartTimeMillis
+                + ", endTimeMillis: " + mEndTimeMillis + "}, total: "
+                + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]";
+        assertEquals(actualString, expectedString);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
index 53fa3e2..da2e1be 100644
--- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
@@ -27,9 +27,7 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
 import static com.android.server.display.AutomaticBrightnessController
                                                       .AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE;
-
 import static com.android.server.display.DisplayDeviceConfig.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
-
 import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID;
 
 import static org.junit.Assert.assertEquals;
@@ -102,6 +100,7 @@
     private Binder mDisplayToken;
     private String mDisplayUniqueId;
     private Context mContextSpy;
+    private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
 
     @Rule
     public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
@@ -124,6 +123,7 @@
         mTestLooper = new TestLooper(mClock::now);
         mDisplayToken = null;
         mDisplayUniqueId = "unique_id";
+
         mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
         final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
         when(mContextSpy.getContentResolver()).thenReturn(resolver);
@@ -140,7 +140,8 @@
         initHandler(null);
         final HighBrightnessModeController hbmc = new HighBrightnessModeController(
                 mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
-                mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy);
+                mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {},
+                null, mContextSpy);
         assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
         assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f);
     }
@@ -150,7 +151,8 @@
         initHandler(null);
         final HighBrightnessModeController hbmc = new HighBrightnessModeController(
                 mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
-                mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy);
+                mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {},
+                null, mContextSpy);
         hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
         hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
         assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
@@ -705,9 +707,12 @@
     // Creates instance with standard initialization values.
     private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) {
         initHandler(clock);
+        if (mHighBrightnessModeMetadata == null) {
+            mHighBrightnessModeMetadata = new HighBrightnessModeMetadata();
+        }
         return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH,
                 DISPLAY_HEIGHT, mDisplayToken, mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX,
-                DEFAULT_HBM_DATA, null, () -> {}, mContextSpy);
+                DEFAULT_HBM_DATA, null, () -> {}, mHighBrightnessModeMetadata, mContextSpy);
     }
 
     private void initHandler(OffsettableClock clock) {
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
new file mode 100644
index 0000000..ede54e0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class HighBrightnessModeMetadataTest {
+    private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+
+    private long mRunningStartTimeMillis = -1;
+
+    @Before
+    public void setUp() {
+        mHighBrightnessModeMetadata = new HighBrightnessModeMetadata();
+    }
+
+    @Test
+    public void checkDefaultValues() {
+        assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(),
+                mRunningStartTimeMillis);
+        assertEquals(mHighBrightnessModeMetadata.getHbmEventQueue().size(), 0);
+    }
+
+    @Test
+    public void checkSetValues() {
+        mRunningStartTimeMillis = 10;
+        mHighBrightnessModeMetadata.setRunningStartTimeMillis(mRunningStartTimeMillis);
+        assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(),
+                mRunningStartTimeMillis);
+        HbmEvent expectedHbmEvent = new HbmEvent(10, 20);
+        mHighBrightnessModeMetadata.addHbmEvent(expectedHbmEvent);
+        HbmEvent actualHbmEvent  = mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst();
+        assertEquals(expectedHbmEvent.toString(), actualHbmEvent.toString());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 638637d..a7da2417 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -16,8 +16,10 @@
 
 package com.android.server.display;
 
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.DEFAULT_DISPLAY_GROUP;
+import static android.view.Display.TYPE_EXTERNAL;
 import static android.view.Display.TYPE_INTERNAL;
 import static android.view.Display.TYPE_VIRTUAL;
 
@@ -173,7 +175,7 @@
 
     @Test
     public void testDisplayDeviceAddAndRemove_NonInternalTypes() {
-        testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_EXTERNAL);
+        testDisplayDeviceAddAndRemove_NonInternal(TYPE_EXTERNAL);
         testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_WIFI);
         testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_OVERLAY);
         testDisplayDeviceAddAndRemove_NonInternal(TYPE_VIRTUAL);
@@ -218,7 +220,7 @@
 
     @Test
     public void testDisplayDeviceAddAndRemove_OneExternalDefault() {
-        DisplayDevice device = createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800,
+        DisplayDevice device = createDisplayDevice(TYPE_EXTERNAL, 600, 800,
                 FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         // add
@@ -268,7 +270,7 @@
     public void testGetDisplayIdsLocked() {
         add(createDisplayDevice(TYPE_INTERNAL, 600, 800,
                 FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, 0));
+        add(createDisplayDevice(TYPE_EXTERNAL, 600, 800, 0));
         add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0));
 
         int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID,
@@ -440,6 +442,11 @@
                 /* isOverrideActive= */false,
                 /* isInteractive= */true,
                 /* isBootCompleted= */true));
+        assertFalse(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED,
+                INVALID_DEVICE_STATE,
+                /* isOverrideActive= */false,
+                /* isInteractive= */true,
+                /* isBootCompleted= */true));
     }
 
     @Test
@@ -460,7 +467,7 @@
 
         Layout layout = new Layout();
         layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, true, true);
-        layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, false);
+        layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, true);
         when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
 
         layout = new Layout();
@@ -469,6 +476,8 @@
         when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout);
         when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout);
 
+        when(mDeviceStateToLayoutMapSpy.size()).thenReturn(4);
+
         LogicalDisplay display1 = add(device1);
         assertEquals(info(display1).address, info(device1).address);
         assertEquals(DEFAULT_DISPLAY, id(display1));
@@ -481,8 +490,15 @@
         mLogicalDisplayMapper.setDeviceStateLocked(0, false);
         mLooper.moveTimeForward(1000);
         mLooper.dispatchAll();
+        // The new state is not applied until the boot is completed
         assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
+
+        mLogicalDisplayMapper.onBootCompleted();
+        mLooper.moveTimeForward(1000);
+        mLooper.dispatchAll();
+        assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
+        assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
 
@@ -581,6 +597,7 @@
         // 2) Mark the displays as STATE_OFF so that it can continue with transition
         // 3) Send DISPLAY_DEVICE_EVENT_CHANGE to inform the mapper of the new display state
         // 4) Dispatch handler events.
+        mLogicalDisplayMapper.onBootCompleted();
         mLogicalDisplayMapper.setDeviceStateLocked(0, false);
         mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED);
         mLooper.moveTimeForward(1000);
@@ -623,6 +640,23 @@
         assertEquals(3, threeDisplaysEnabled.length);
     }
 
+    @Test
+    public void testCreateNewLogicalDisplay() {
+        DisplayDevice device1 = createDisplayDevice(TYPE_EXTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1);
+        LogicalDisplay display1 = add(device1);
+
+        assertTrue(display1.isEnabledLocked());
+
+        DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        when(mDeviceStateToLayoutMapSpy.size()).thenReturn(2);
+        LogicalDisplay display2 = add(device2);
+
+        assertFalse(display2.isEnabledLocked());
+    }
+
     /////////////////
     // Helper Methods
     /////////////////
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
index 303a370..1ef1197 100644
--- a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
@@ -99,7 +99,24 @@
         mLooper.dispatchAll();
 
         // Verify that dream service is called to attach.
-        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any());
+        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/,
+                eq(false) /*preview*/, any());
+    }
+
+    @Test
+    public void startDream_attachOnServiceConnectedInPreviewMode() throws RemoteException {
+        // Call dream controller to start dreaming.
+        mDreamController.startDream(mToken, mDreamName, true /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+        // Mock service connected.
+        final ServiceConnection serviceConnection = captureServiceConnection();
+        serviceConnection.onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Verify that dream service is called to attach.
+        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/,
+                eq(true) /*preview*/, any());
     }
 
     @Test
@@ -129,7 +146,7 @@
 
         // Mock second dream started.
         verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/,
-                mRemoteCallbackCaptor.capture());
+                eq(false) /*preview*/, mRemoteCallbackCaptor.capture());
         mRemoteCallbackCaptor.getValue().sendResult(null /*data*/);
         mLooper.dispatchAll();
 
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
new file mode 100644
index 0000000..6c73f71
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.server.dreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.dreams.DreamOverlayService;
+import android.service.dreams.IDreamOverlay;
+import android.service.dreams.IDreamOverlayCallback;
+import android.service.dreams.IDreamOverlayClient;
+import android.service.dreams.IDreamOverlayClientCallback;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * A collection of tests to exercise {@link DreamOverlayService}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamOverlayServiceTest {
+    private static final ComponentName FIRST_DREAM_COMPONENT =
+            ComponentName.unflattenFromString("com.foo.bar/.DreamService");
+    private static final ComponentName SECOND_DREAM_COMPONENT =
+            ComponentName.unflattenFromString("com.foo.baz/.DreamService");
+
+    @Mock
+    WindowManager.LayoutParams mLayoutParams;
+
+    @Mock
+    IDreamOverlayCallback mOverlayCallback;
+
+    /**
+     * {@link TestDreamOverlayService} is a simple {@link DreamOverlayService} implementation for
+     * tracking interactions across {@link IDreamOverlay} binder interface. The service reports
+     * interactions to a {@link Monitor} instance provided at construction.
+     */
+    private static class TestDreamOverlayService extends DreamOverlayService {
+        /**
+         * An interface implemented to be informed when the corresponding methods in
+         * {@link TestDreamOverlayService} are invoked.
+         */
+        interface Monitor {
+            void onStartDream();
+            void onEndDream();
+            void onWakeUp();
+        }
+
+        private final Monitor mMonitor;
+
+        TestDreamOverlayService(Monitor monitor) {
+            super();
+            mMonitor = monitor;
+        }
+
+        @Override
+        public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
+            mMonitor.onStartDream();
+        }
+
+        @Override
+        public void onEndDream() {
+            mMonitor.onEndDream();
+            super.onEndDream();
+        }
+
+        @Override
+        public void onWakeUp(@NonNull Runnable onCompleteCallback) {
+            mMonitor.onWakeUp();
+            super.onWakeUp(onCompleteCallback);
+        }
+    }
+
+    /**
+     * A {@link IDreamOverlayClientCallback} implementation that captures the requested client.
+     */
+    private static class OverlayClientCallback extends IDreamOverlayClientCallback.Stub {
+        public IDreamOverlayClient retrievedClient;
+        @Override
+        public void onDreamOverlayClient(IDreamOverlayClient client) throws RemoteException {
+            retrievedClient = client;
+        }
+    }
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    /**
+     * Verifies that only the currently started dream is able to affect the overlay.
+     */
+    @Test
+    public void testOverlayClientInteraction() throws RemoteException {
+        final TestDreamOverlayService.Monitor monitor = Mockito.mock(
+                TestDreamOverlayService.Monitor.class);
+        final TestDreamOverlayService service = new TestDreamOverlayService(monitor);
+        final IBinder binder = service.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder);
+
+        // Create two overlay clients and ensure they are unique.
+        final IDreamOverlayClient firstClient = getClient(overlay);
+        assertThat(firstClient).isNotNull();
+
+        final IDreamOverlayClient secondClient = getClient(overlay);
+        assertThat(secondClient).isNotNull();
+
+        assertThat(firstClient).isNotEqualTo(secondClient);
+
+        // Start a dream with the first client and ensure the dream is now active from the
+        // overlay's perspective.
+        firstClient.startDream(mLayoutParams, mOverlayCallback,
+                FIRST_DREAM_COMPONENT.flattenToString(), false);
+
+
+        verify(monitor).onStartDream();
+        assertThat(service.getDreamComponent()).isEqualTo(FIRST_DREAM_COMPONENT);
+
+        Mockito.clearInvocations(monitor);
+
+        // Start a dream from the second client and verify that the overlay has both cycled to
+        // the new dream (ended/started).
+        secondClient.startDream(mLayoutParams, mOverlayCallback,
+                SECOND_DREAM_COMPONENT.flattenToString(), false);
+
+        verify(monitor).onEndDream();
+        verify(monitor).onStartDream();
+        assertThat(service.getDreamComponent()).isEqualTo(SECOND_DREAM_COMPONENT);
+
+        Mockito.clearInvocations(monitor);
+
+        // Verify that interactions with the first, now inactive client don't affect the overlay.
+        firstClient.endDream();
+        verify(monitor, never()).onEndDream();
+
+        firstClient.wakeUp();
+        verify(monitor, never()).onWakeUp();
+    }
+
+    private static IDreamOverlayClient getClient(IDreamOverlay overlay) throws RemoteException {
+        final OverlayClientCallback callback = new OverlayClientCallback();
+        overlay.getClient(callback);
+        return callback.retrievedClient;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
index 96302b9..299f153 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
@@ -25,6 +25,7 @@
 
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteException;
 import android.net.Uri;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
@@ -63,6 +64,7 @@
     private MatrixCursor mContactsLookupCursor;
     private MatrixCursor mPhoneCursor;
     private ContactsQueryHelper mHelper;
+    private ContactsContentProvider contentProvider;
 
     @Before
     public void setUp() {
@@ -73,7 +75,7 @@
         mPhoneCursor = new MatrixCursor(PHONE_COLUMNS);
 
         MockContentResolver contentResolver = new MockContentResolver();
-        ContactsContentProvider contentProvider = new ContactsContentProvider();
+        contentProvider = new ContactsContentProvider();
         contentProvider.registerCursor(Contacts.CONTENT_URI, mContactsCursor);
         contentProvider.registerCursor(
                 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, mContactsLookupCursor);
@@ -89,6 +91,14 @@
     }
 
     @Test
+    public void testQueryException_returnsFalse() {
+        contentProvider.setThrowException(true);
+
+        Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY);
+        assertFalse(mHelper.query(contactUri.toString()));
+    }
+
+    @Test
     public void testQueryWithUri() {
         mContactsCursor.addRow(new Object[] {
                 /* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 1,
@@ -168,10 +178,15 @@
     private class ContactsContentProvider extends MockContentProvider {
 
         private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>();
+        private boolean throwException = false;
 
         @Override
         public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                 String sortOrder) {
+            if (throwException) {
+                throw new SQLiteException();
+            }
+
             for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) {
                 if (uri.isPathPrefixMatch(prefixUri)) {
                     return mUriPrefixToCursorMap.get(prefixUri);
@@ -180,6 +195,10 @@
             return mUriPrefixToCursorMap.get(uri);
         }
 
+        public void setThrowException(boolean throwException) {
+            this.throwException = throwException;
+        }
+
         private void registerCursor(Uri uriPrefix, Cursor cursor) {
             mUriPrefixToCursorMap.put(uriPrefix, cursor);
         }
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 66c3f07..454a23b 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -86,6 +86,7 @@
 import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
+import android.util.Pair;
 import android.util.Range;
 
 import com.android.internal.app.ChooserActivity;
@@ -186,6 +187,7 @@
     private ShortcutInfo mShortcutInfo;
     private TestInjector mInjector;
     private TestLooper mLooper;
+    private TestPerPackageThrottler mShortcutThrottler;
 
     @Before
     public void setUp() throws PackageManager.NameNotFoundException {
@@ -275,7 +277,9 @@
 
         mInjector = new TestInjector();
         mLooper = new TestLooper();
-        mDataManager = new DataManager(mContext, mInjector, mLooper.getLooper());
+        mShortcutThrottler = new TestPerPackageThrottler();
+        mDataManager = new DataManager(mContext, mInjector, mLooper.getLooper(),
+                mShortcutThrottler);
         mDataManager.initialize();
 
         when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(),
@@ -283,10 +287,7 @@
 
         mShortcutInfo = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
                 buildPerson());
-        when(mShortcutServiceInternal.getShortcuts(
-                anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
-                anyInt(), anyInt(), anyInt(), anyInt()))
-                .thenReturn(Collections.singletonList(mShortcutInfo));
+        mockGetShortcuts(Collections.singletonList(mShortcutInfo));
         verify(mShortcutServiceInternal).addShortcutChangeCallback(
                 mShortcutChangeCallbackCaptor.capture());
         mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue();
@@ -500,6 +501,7 @@
         // The cached conversations are above the limit because every conversation has active
         // notifications. To uncache one of them, the notifications for that conversation need to
         // be dismissed.
+        String notificationKey = "";
         for (int i = 0; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) {
             String shortcutId = TEST_SHORTCUT_ID + i;
             ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
@@ -507,11 +509,13 @@
             shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
             mDataManager.addOrUpdateConversationInfo(shortcut);
             when(mNotification.getShortcutId()).thenReturn(shortcutId);
-            sendGenericNotification();
+            notificationKey = String.format("notification-key-%d", i);
+            sendGenericNotificationWithKey(notificationKey);
         }
 
         // Post another notification for the last conversation.
-        sendGenericNotification();
+        String otherNotificationKey = "other-notification-key";
+        sendGenericNotificationWithKey(otherNotificationKey);
 
         // Removing one of the two notifications does not un-cache the shortcut.
         listenerService.onNotificationRemoved(mGenericSbn, null,
@@ -520,6 +524,7 @@
                 anyInt(), any(), anyString(), any(), anyInt(), anyInt());
 
         // Removing the second notification un-caches the shortcut.
+        when(mGenericSbn.getKey()).thenReturn(notificationKey);
         listenerService.onNotificationRemoved(mGenericSbn, null,
                 NotificationListenerService.REASON_CANCEL_ALL);
         verify(mShortcutServiceInternal).uncacheShortcuts(
@@ -687,6 +692,63 @@
     }
 
     @Test
+    public void testGetConversation_trackActiveConversations() {
+        mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+        assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+                TEST_SHORTCUT_ID)).isNull();
+        ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+                buildPerson());
+        shortcut.setCached(ShortcutInfo.FLAG_PINNED);
+        mDataManager.addOrUpdateConversationInfo(shortcut);
+        assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+                TEST_SHORTCUT_ID)).isNotNull();
+
+        sendGenericNotification();
+        sendGenericNotification();
+        ConversationChannel result = mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+                TEST_SHORTCUT_ID);
+        assertTrue(result.hasActiveNotifications());
+
+        // Both generic notifications have the same notification key, so a single dismiss will
+        // remove both of them.
+        NotificationListenerService listenerService =
+                mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
+        listenerService.onNotificationRemoved(mGenericSbn, null,
+                NotificationListenerService.REASON_CANCEL);
+        ConversationChannel resultTwo = mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+                TEST_SHORTCUT_ID);
+        assertFalse(resultTwo.hasActiveNotifications());
+    }
+
+    @Test
+    public void testGetConversation_unsyncedShortcut() {
+        mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+        ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+                buildPerson());
+        shortcut.setCached(ShortcutInfo.FLAG_PINNED);
+        mDataManager.addOrUpdateConversationInfo(shortcut);
+        assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+                TEST_SHORTCUT_ID)).isNotNull();
+        assertThat(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
+                .getConversationStore()
+                .getConversation(TEST_SHORTCUT_ID)).isNotNull();
+
+        when(mShortcutServiceInternal.getShortcuts(
+                anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
+                anyInt(), anyInt(), anyInt(), anyInt()))
+                .thenReturn(Collections.emptyList());
+        assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+                TEST_SHORTCUT_ID)).isNull();
+
+        // Conversation is removed from store as there is no matching shortcut in ShortcutManager
+        assertThat(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
+                .getConversationStore()
+                .getConversation(TEST_SHORTCUT_ID)).isNull();
+        verify(mNotificationManagerInternal)
+                .onConversationRemoved(TEST_PKG_NAME, TEST_PKG_UID, Set.of(TEST_SHORTCUT_ID));
+    }
+
+    @Test
     public void testOnNotificationChannelModified() {
         mDataManager.onUserUnlocked(USER_ID_PRIMARY);
         assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
@@ -911,6 +973,7 @@
                 buildPerson());
         ShortcutInfo shortcut3 = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, "sc3",
                 buildPerson());
+        mockGetShortcuts(List.of(shortcut1, shortcut2, shortcut3));
         mShortcutChangeCallback.onShortcutsAddedOrUpdated(TEST_PKG_NAME,
                 Arrays.asList(shortcut1, shortcut2, shortcut3), UserHandle.of(USER_ID_PRIMARY));
         mShortcutChangeCallback.onShortcutsRemoved(TEST_PKG_NAME,
@@ -1162,7 +1225,6 @@
                     eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
         }
     }
-
     @Test
     public void testUncacheOldestCachedShortcut_missingNotificationEvents() {
         mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -1172,6 +1234,7 @@
             ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
                     buildPerson());
             shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
+            mockGetShortcuts(Collections.singletonList(shortcut));
             mShortcutChangeCallback.onShortcutsAddedOrUpdated(
                     TEST_PKG_NAME,
                     Collections.singletonList(shortcut),
@@ -1191,7 +1254,6 @@
                     eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
         }
     }
-
     @Test
     public void testUncacheOldestCachedShortcut_legacyConversation() {
         mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -1213,6 +1275,7 @@
             ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
                     buildPerson());
             shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
+            mockGetShortcuts(Collections.singletonList(shortcut));
             mShortcutChangeCallback.onShortcutsAddedOrUpdated(
                     TEST_PKG_NAME,
                     Collections.singletonList(shortcut),
@@ -1250,7 +1313,8 @@
         mDataManager.reportShareTargetEvent(appTargetEvent, intentFilter);
         byte[] payload = mDataManager.getBackupPayload(USER_ID_PRIMARY);
 
-        DataManager dataManager = new DataManager(mContext, mInjector, mLooper.getLooper());
+        DataManager dataManager = new DataManager(
+                mContext, mInjector, mLooper.getLooper(), mShortcutThrottler);
         dataManager.onUserUnlocked(USER_ID_PRIMARY);
         dataManager.restore(USER_ID_PRIMARY, payload);
         ConversationInfo conversationInfo = dataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
@@ -1294,7 +1358,7 @@
 
         sendGenericNotification();
 
-       mDataManager.getRecentConversations(USER_ID_PRIMARY);
+        mDataManager.getRecentConversations(USER_ID_PRIMARY);
 
         verify(mShortcutServiceInternal).getShortcuts(
                 anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
@@ -1662,9 +1726,22 @@
         return (queryFlags & flag) != 0;
     }
 
+    private void mockGetShortcuts(List<ShortcutInfo> shortcutInfoList) {
+        when(mShortcutServiceInternal.getShortcuts(
+                anyInt(), anyString(), anyLong(), anyString(), any(), any(), any(),
+                anyInt(), anyInt(), anyInt(), anyInt()))
+                .thenReturn(shortcutInfoList);
+    }
+
     // "Sends" a notification to a non-customized notification channel - the notification channel
     // is something generic like "messages" and the notification has a  shortcut id
     private void sendGenericNotification() {
+        sendGenericNotificationWithKey(GENERIC_KEY);
+    }
+
+    // "Sends" a notification to a non-customized notification channel with the specified key.
+    private void sendGenericNotificationWithKey(String key) {
+        when(mGenericSbn.getKey()).thenReturn(key);
         when(mNotification.getChannelId()).thenReturn(PARENT_NOTIFICATION_CHANNEL_ID);
         doAnswer(invocationOnMock -> {
             NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking)
@@ -1678,9 +1755,9 @@
                     mParentNotificationChannel.getImportance(),
                     null, null,
                     mParentNotificationChannel, null, null, true, 0, false, -1, false, null, null,
-                    false, false, false, null, 0, false);
+                    false, false, false, null, 0, false, 0);
             return true;
-        }).when(mRankingMap).getRanking(eq(GENERIC_KEY),
+        }).when(mRankingMap).getRanking(eq(key),
                 any(NotificationListenerService.Ranking.class));
         NotificationListenerService listenerService =
                 mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
@@ -1704,7 +1781,7 @@
                     mNotificationChannel.getImportance(),
                     null, null,
                     mNotificationChannel, null, null, true, 0, false, -1, false, null, null, false,
-                    false, false, null, 0, false);
+                    false, false, null, 0, false, 0);
             return true;
         }).when(mRankingMap).getRanking(eq(CUSTOM_KEY),
                 any(NotificationListenerService.Ranking.class));
@@ -1881,4 +1958,11 @@
             return mUsageStatsQueryHelper;
         }
     }
+
+    private static class TestPerPackageThrottler implements PerPackageThrottler {
+        @Override
+        public void scheduleDebounced(Pair<String, Integer> pkgUserId, Runnable runnable) {
+            runnable.run();
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/people/data/PerPackageThrottlerImplTest.java b/services/tests/servicestests/src/com/android/server/people/data/PerPackageThrottlerImplTest.java
new file mode 100644
index 0000000..672cbb9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/people/data/PerPackageThrottlerImplTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.server.people.data;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.util.Pair;
+
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(JUnit4.class)
+public class PerPackageThrottlerImplTest {
+    private static final int DEBOUNCE_INTERVAL = 500;
+    private static final String PKG_ONE = "pkg_one";
+    private static final String PKG_TWO = "pkg_two";
+    private static final int USER_ID = 10;
+
+    private final OffsettableClock mClock = new OffsettableClock.Stopped();
+    private final TestHandler mTestHandler = new TestHandler(null, mClock);
+    private PerPackageThrottlerImpl mThrottler;
+
+    @Before
+    public void setUp() {
+        mThrottler = new PerPackageThrottlerImpl(mTestHandler, DEBOUNCE_INTERVAL);
+    }
+
+    @Test
+    public void scheduleDebounced() {
+        AtomicBoolean pkgOneRan = new AtomicBoolean();
+        AtomicBoolean pkgTwoRan = new AtomicBoolean();
+
+        mThrottler.scheduleDebounced(new Pair<>(PKG_ONE, USER_ID), () -> pkgOneRan.set(true));
+        mThrottler.scheduleDebounced(new Pair<>(PKG_ONE, USER_ID), () -> pkgOneRan.set(true));
+        mThrottler.scheduleDebounced(new Pair<>(PKG_TWO, USER_ID), () -> pkgTwoRan.set(true));
+        mThrottler.scheduleDebounced(new Pair<>(PKG_TWO, USER_ID), () -> pkgTwoRan.set(true));
+
+        assertFalse(pkgOneRan.get());
+        assertFalse(pkgTwoRan.get());
+        mClock.fastForward(DEBOUNCE_INTERVAL);
+        mTestHandler.timeAdvance();
+        assertTrue(pkgOneRan.get());
+        assertTrue(pkgTwoRan.get());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java
index 3de65c1..1b3a199 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java
@@ -132,6 +132,19 @@
         assertThat(mFinalizedRotation).isEqualTo(DEFAULT_SENSOR_ROTATION);
     }
 
+    @Test
+    public void testOnSensorChanged_screenLocked_doNotCallRotationResolverReturnDefaultRotation() {
+        mWindowOrientationListener = new TestableWindowOrientationListener(mMockContext,
+                mHandler, /* defaultRotation */ Surface.ROTATION_180);
+        mWindowOrientationListener.mRotationResolverService = mFakeRotationResolverInternal;
+        mWindowOrientationListener.mIsScreenLocked = true;
+
+        mWindowOrientationListener.mOrientationJudge.onSensorChanged(mFakeSensorEvent);
+
+        assertThat(mWindowOrientationListener.mIsOnProposedRotationChangedCalled).isFalse();
+        assertThat(mFinalizedRotation).isEqualTo(Surface.ROTATION_180);
+    }
+
     static final class TestableRotationResolver extends RotationResolverInternal {
         @Surface.Rotation
         RotationResolverCallbackInternal mCallback;
@@ -166,21 +179,17 @@
         }
     }
 
-    @Test
-    public void testOnSensorChanged_inLockScreen_doNotCallRotationResolver() {
-        mWindowOrientationListener.mIsScreenLocked = true;
-
-        mWindowOrientationListener.mOrientationJudge.onSensorChanged(mFakeSensorEvent);
-
-        assertThat(mWindowOrientationListener.mIsOnProposedRotationChangedCalled).isFalse();
-    }
-
     final class TestableWindowOrientationListener extends WindowOrientationListener {
         private boolean mIsOnProposedRotationChangedCalled = false;
         private boolean mIsScreenLocked;
 
         TestableWindowOrientationListener(Context context, Handler handler) {
-            super(context, handler);
+            this(context, handler, Surface.ROTATION_0);
+        }
+
+        TestableWindowOrientationListener(Context context, Handler handler,
+                @Surface.Rotation int defaultRotation) {
+            super(context, handler, defaultRotation);
             this.mOrientationJudge = new OrientationSensorJudge();
         }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 7986043..8b1384e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -1582,6 +1582,55 @@
     }
 
     @Test
+    public void testSetComponentState_differentUsers() throws Exception {
+        Context context = mock(Context.class);
+        PackageManager pm = mock(PackageManager.class);
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
+        when(context.getPackageName()).thenReturn(mContext.getPackageName());
+        when(context.getUserId()).thenReturn(mContext.getUserId());
+        when(context.getPackageManager()).thenReturn(pm);
+        when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        ComponentName cn = ComponentName.unflattenFromString("a/a");
+
+        addExpectedServices(service, Arrays.asList("a"), mZero.id);
+        addExpectedServices(service, Arrays.asList("a"), mTen.id);
+        when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            ServiceConnection sc = (ServiceConnection) args[1];
+            sc.onServiceConnected(cn, mock(IBinder.class));
+            return true;
+        });
+        service.addApprovedList("a/a", 0, true);
+        service.addApprovedList("a/a", 10, false);
+
+        service.registerService(cn, mZero.id);
+        assertTrue(service.isBound(cn, mZero.id));
+
+        service.onUserSwitched(mTen.id);
+        assertFalse(service.isBound(cn, mZero.id));
+        service.registerService(cn, mTen.id);
+        assertTrue(service.isBound(cn, mTen.id));
+
+        service.setComponentState(cn, mTen.id, false);
+        assertFalse(service.isBound(cn, mZero.id));
+        assertFalse(service.isBound(cn, mTen.id));
+
+        // Service should be rebound on user 0, since it was only disabled for user 10.
+        service.onUserSwitched(mZero.id);
+        assertTrue(service.isBound(cn, mZero.id));
+        assertFalse(service.isBound(cn, mTen.id));
+
+        // Service should stay unbound on going back to user 10.
+        service.onUserSwitched(mTen.id);
+        assertFalse(service.isBound(cn, mZero.id));
+        assertFalse(service.isBound(cn, mTen.id));
+    }
+    @Test
     public void testOnPackagesChanged_nullValuesPassed_noNullPointers() {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1847,7 +1896,7 @@
     }
 
     private void addExpectedServices(final ManagedServices service, final List<String> packages,
-            int userId) {
+            int userId) throws Exception {
         ManagedServices.Config config = service.getConfig();
         when(mPm.queryIntentServicesAsUser(any(), anyInt(), eq(userId))).
                 thenAnswer(new Answer<List<ResolveInfo>>() {
@@ -1876,6 +1925,20 @@
                         return new ArrayList<>();
                     }
                 });
+
+        when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+                (Answer<ServiceInfo>) invocation -> {
+                    ComponentName invocationCn = invocation.getArgument(0);
+                    if (invocationCn != null && packages.contains(invocationCn.getPackageName())) {
+                        ServiceInfo serviceInfo = new ServiceInfo();
+                        serviceInfo.packageName = invocationCn.getPackageName();
+                        serviceInfo.name = invocationCn.getClassName();
+                        serviceInfo.permission = service.getConfig().bindPermission;
+                        return serviceInfo;
+                    }
+                    return null;
+                }
+        );
     }
 
     private List<String> stringToList(String list) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index 12cd834..8a99c2c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -193,7 +193,8 @@
                 tweak.isConversation(),
                 tweak.getConversationShortcutInfo(),
                 tweak.getRankingAdjustment(),
-                tweak.isBubble()
+                tweak.isBubble(),
+                tweak.getProposedImportance()
         );
         assertNotEquals(nru, nru2);
     }
@@ -274,7 +275,8 @@
                     isConversation(i),
                     getShortcutInfo(i),
                     getRankingAdjustment(i),
-                    isBubble(i)
+                    isBubble(i),
+                    getProposedImportance(i)
             );
             rankings[i] = ranking;
         }
@@ -402,6 +404,10 @@
         return index % 3 - 1;
     }
 
+    private int getProposedImportance(int index) {
+        return index % 5 - 1;
+    }
+
     private boolean isBubble(int index) {
         return index % 4 == 0;
     }
@@ -443,6 +449,7 @@
         assertEquals(comment, a.getConversationShortcutInfo().getId(),
                 b.getConversationShortcutInfo().getId());
         assertActionsEqual(a.getSmartActions(), b.getSmartActions());
+        assertEquals(a.getProposedImportance(), b.getProposedImportance());
     }
 
     private void detailedAssertEquals(RankingMap a, RankingMap b) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 71dadd4..3e5e9d1 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -1645,6 +1645,46 @@
     }
 
     @Test
+    public void testEnqueueNotificationWithTag_nullAction_fixed() throws Exception {
+        Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .addAction(new Notification.Action.Builder(null, "one", null).build())
+                .addAction(new Notification.Action.Builder(null, "two", null).build())
+                .addAction(new Notification.Action.Builder(null, "three", null).build())
+                .build();
+        n.actions[1] = null;
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+        waitForIdle();
+
+        StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
+        assertThat(posted).hasLength(1);
+        assertThat(posted[0].getNotification().actions).hasLength(2);
+        assertThat(posted[0].getNotification().actions[0].title.toString()).isEqualTo("one");
+        assertThat(posted[0].getNotification().actions[1].title.toString()).isEqualTo("three");
+    }
+
+    @Test
+    public void testEnqueueNotificationWithTag_allNullActions_fixed() throws Exception {
+        Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .addAction(new Notification.Action.Builder(null, "one", null).build())
+                .addAction(new Notification.Action.Builder(null, "two", null).build())
+                .build();
+        n.actions[0] = null;
+        n.actions[1] = null;
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+        waitForIdle();
+
+        StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
+        assertThat(posted).hasLength(1);
+        assertThat(posted[0].getNotification().actions).isNull();
+    }
+
+    @Test
     public void testCancelNonexistentNotification() throws Exception {
         mBinderService.cancelNotificationWithTag(PKG, PKG,
                 "testCancelNonexistentNotification", 0, 0);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
new file mode 100644
index 0000000..87e86cb
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.server.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.notification.Adjustment;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
+
+    @Test
+    public void testHasDiffs_noDiffs() {
+        NotificationRecord r = generateRecord();
+
+        NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+                1,
+                r.getPackageVisibilityOverride(),
+                r.canShowBadge(),
+                r.canBubble(),
+                r.getNotification().isBubbleNotification(),
+                r.getChannel(),
+                r.getGroupKey(),
+                r.getPeopleOverride(),
+                r.getSnoozeCriteria(),
+                r.getUserSentiment(),
+                r.getSuppressedVisualEffects(),
+                r.getSystemGeneratedSmartActions(),
+                r.getSmartReplies(),
+                r.getImportance(),
+                r.getRankingScore(),
+                r.isConversation(),
+                r.getProposedImportance());
+
+        assertFalse(extractorData.hasDiffForRankingLocked(r, 1));
+        assertFalse(extractorData.hasDiffForLoggingLocked(r, 1));
+    }
+
+    @Test
+    public void testHasDiffs_proposedImportanceChange() {
+        NotificationRecord r = generateRecord();
+
+        NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+                1,
+                r.getPackageVisibilityOverride(),
+                r.canShowBadge(),
+                r.canBubble(),
+                r.getNotification().isBubbleNotification(),
+                r.getChannel(),
+                r.getGroupKey(),
+                r.getPeopleOverride(),
+                r.getSnoozeCriteria(),
+                r.getUserSentiment(),
+                r.getSuppressedVisualEffects(),
+                r.getSystemGeneratedSmartActions(),
+                r.getSmartReplies(),
+                r.getImportance(),
+                r.getRankingScore(),
+                r.isConversation(),
+                r.getProposedImportance());
+
+        Bundle signals = new Bundle();
+        signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH);
+        Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
+        r.addAdjustment(adjustment);
+        r.applyAdjustments();
+
+        assertTrue(extractorData.hasDiffForRankingLocked(r, 1));
+        assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
+    }
+
+    private NotificationRecord generateRecord() {
+        NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
+        final Notification.Builder builder = new Notification.Builder(getContext())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon);
+        Notification n = builder.build();
+        StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0,
+                0, n, UserHandle.ALL, null, System.currentTimeMillis());
+       return new NotificationRecord(getContext(), sbn, channel);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index 5468220..14b0048 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -19,6 +19,7 @@
 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
 import static android.service.notification.Adjustment.KEY_IMPORTANCE;
 import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -755,6 +756,24 @@
     }
 
     @Test
+    public void testProposedImportance() {
+        StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+
+        assertEquals(IMPORTANCE_UNSPECIFIED, record.getProposedImportance());
+
+        Bundle signals = new Bundle();
+        signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_DEFAULT);
+        record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId()));
+
+        record.applyAdjustments();
+
+        assertEquals(IMPORTANCE_DEFAULT, record.getProposedImportance());
+    }
+
+    @Test
     public void testAppImportance_returnsCorrectly() {
         StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
                 true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index d46530c..d824fee 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -1703,6 +1703,7 @@
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
         channel.setShowBadge(true);
         channel.setAllowBubbles(false);
+        channel.setImportantConversation(true);
         int lockMask = 0;
         for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
             lockMask |= NotificationChannel.LOCKABLE_FIELDS[i];
@@ -1718,6 +1719,7 @@
         assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights());
         assertFalse(savedChannel.canBypassDnd());
         assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility());
+        assertFalse(channel.isImportantConversation());
         assertEquals(channel.canShowBadge(), savedChannel.canShowBadge());
         assertEquals(channel.canBubble(), savedChannel.canBubble());
 
@@ -4396,7 +4398,7 @@
                 new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
         channel2.setConversationId(calls.getId(), convoId);
         channel2.setImportantConversation(true);
-        mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false);
 
         List<ConversationChannelWrapper> convos =
                 mHelper.getConversations(IntArray.wrap(new int[] {0}), false);
@@ -4473,7 +4475,7 @@
                 new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
         channel2.setConversationId(calls.getId(), convoId);
         channel2.setImportantConversation(true);
-        mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false);
 
         List<ConversationChannelWrapper> convos =
                 mHelper.getConversations(IntArray.wrap(new int[] {0}), false);
@@ -4501,13 +4503,13 @@
                 new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT);
         channel.setConversationId(messages.getId(), convoId);
         channel.setImportantConversation(true);
-        mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false);
 
         NotificationChannel diffConvo =
                 new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT);
         diffConvo.setConversationId(p.getId(), "different convo");
         diffConvo.setImportantConversation(true);
-        mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false);
+        mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, false, false);
 
         NotificationChannel channel2 =
                 new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
@@ -4534,7 +4536,7 @@
                 new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT);
         channel.setConversationId(messages.getId(), convoId);
         channel.setImportantConversation(true);
-        mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false);
 
         mHelper.permanentlyDeleteNotificationChannel(PKG_O, UID_O, "messages");
 
@@ -4935,7 +4937,7 @@
                 "conversation", IMPORTANCE_DEFAULT);
         friend.setConversationId(parent.getId(), "friend");
         friend.setImportantConversation(true);
-        mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O, friend, false, false);
 
         ArrayList<StatsEvent> events = new ArrayList<>();
         mHelper.pullPackageChannelPreferencesStats(events);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 949455a1..b2754d2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -32,22 +32,24 @@
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.EventInfo;
 import android.service.notification.ZenPolicy;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.util.TypedXmlPullParser;
 import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.UiServiceTestCase;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -189,8 +191,6 @@
 
     @Test
     public void testRuleXml() throws Exception {
-        String tag = "tag";
-
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = new ComponentName("a", "a");
         rule.component = new ComponentName("b", "b");
@@ -205,20 +205,11 @@
         rule.snoozing = true;
         rule.pkg = "b";
 
-        TypedXmlSerializer out = Xml.newFastSerializer();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        out.setOutput(new BufferedOutputStream(baos), "utf-8");
-        out.startDocument(null, true);
-        out.startTag(null, tag);
-        ZenModeConfig.writeRuleXml(rule, out);
-        out.endTag(null, tag);
-        out.endDocument();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
 
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(
-                new ByteArrayInputStream(baos.toByteArray())), null);
-        parser.nextTag();
-        ZenModeConfig.ZenRule fromXml = ZenModeConfig.readRuleXml(parser);
         assertEquals("b", fromXml.pkg);
         // always resets on reboot
         assertFalse(fromXml.snoozing);
@@ -237,75 +228,41 @@
 
     @Test
     public void testRuleXml_pkg_component() throws Exception {
-        String tag = "tag";
-
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = new ComponentName("a", "a");
         rule.component = new ComponentName("b", "b");
 
-        TypedXmlSerializer out = Xml.newFastSerializer();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        out.setOutput(new BufferedOutputStream(baos), "utf-8");
-        out.startDocument(null, true);
-        out.startTag(null, tag);
-        ZenModeConfig.writeRuleXml(rule, out);
-        out.endTag(null, tag);
-        out.endDocument();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
 
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(
-                new ByteArrayInputStream(baos.toByteArray())), null);
-        parser.nextTag();
-        ZenModeConfig.ZenRule fromXml = ZenModeConfig.readRuleXml(parser);
         assertEquals("b", fromXml.pkg);
     }
 
     @Test
     public void testRuleXml_pkg_configActivity() throws Exception {
-        String tag = "tag";
-
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = new ComponentName("a", "a");
 
-        TypedXmlSerializer out = Xml.newFastSerializer();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        out.setOutput(new BufferedOutputStream(baos), "utf-8");
-        out.startDocument(null, true);
-        out.startTag(null, tag);
-        ZenModeConfig.writeRuleXml(rule, out);
-        out.endTag(null, tag);
-        out.endDocument();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
 
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(
-                new ByteArrayInputStream(baos.toByteArray())), null);
-        parser.nextTag();
-        ZenModeConfig.ZenRule fromXml = ZenModeConfig.readRuleXml(parser);
         assertNull(fromXml.pkg);
     }
 
     @Test
     public void testRuleXml_getPkg_nullPkg() throws Exception {
-        String tag = "tag";
-
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.enabled = true;
         rule.configurationActivity = new ComponentName("a", "a");
 
-        TypedXmlSerializer out = Xml.newFastSerializer();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        out.setOutput(new BufferedOutputStream(baos), "utf-8");
-        out.startDocument(null, true);
-        out.startTag(null, tag);
-        ZenModeConfig.writeRuleXml(rule, out);
-        out.endTag(null, tag);
-        out.endDocument();
-
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(
-                new ByteArrayInputStream(baos.toByteArray())), null);
-        parser.nextTag();
-        ZenModeConfig.ZenRule fromXml = ZenModeConfig.readRuleXml(parser);
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
         assertEquals("a", fromXml.getPkg());
 
         fromXml.condition = new Condition(Uri.EMPTY, "", Condition.STATE_TRUE);
@@ -313,25 +270,26 @@
     }
 
     @Test
-    public void testZenPolicyXml_allUnset() throws Exception {
-        String tag = "tag";
+    public void testRuleXml_emptyConditionId() throws Exception {
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.conditionId = Uri.EMPTY;
 
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
+
+        assertEquals(rule.condition, fromXml.condition);
+    }
+
+    @Test
+    public void testZenPolicyXml_allUnset() throws Exception {
         ZenPolicy policy = new ZenPolicy.Builder().build();
 
-        TypedXmlSerializer out = Xml.newFastSerializer();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        out.setOutput(new BufferedOutputStream(baos), "utf-8");
-        out.startDocument(null, true);
-        out.startTag(null, tag);
-        ZenModeConfig.writeZenPolicyXml(policy, out);
-        out.endTag(null, tag);
-        out.endDocument();
-
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(
-                new ByteArrayInputStream(baos.toByteArray())), null);
-        parser.nextTag();
-        ZenPolicy fromXml = ZenModeConfig.readZenPolicyXml(parser);
+        writePolicyXml(policy, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenPolicy fromXml = readPolicyXml(bais);
 
         // nothing was set, so we should have nothing from the parser
         assertNull(fromXml);
@@ -339,8 +297,6 @@
 
     @Test
     public void testZenPolicyXml() throws Exception {
-        String tag = "tag";
-
         ZenPolicy policy = new ZenPolicy.Builder()
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
                 .allowMessages(ZenPolicy.PEOPLE_TYPE_NONE)
@@ -355,20 +311,10 @@
                 .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true)
                 .build();
 
-        TypedXmlSerializer out = Xml.newFastSerializer();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        out.setOutput(new BufferedOutputStream(baos), "utf-8");
-        out.startDocument(null, true);
-        out.startTag(null, tag);
-        ZenModeConfig.writeZenPolicyXml(policy, out);
-        out.endTag(null, tag);
-        out.endDocument();
-
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(
-                new ByteArrayInputStream(baos.toByteArray())), null);
-        parser.nextTag();
-        ZenPolicy fromXml = ZenModeConfig.readZenPolicyXml(parser);
+        writePolicyXml(policy, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenPolicy fromXml = readPolicyXml(bais);
 
         assertNotNull(fromXml);
         assertEquals(policy.getPriorityCategoryCalls(), fromXml.getPriorityCategoryCalls());
@@ -457,4 +403,45 @@
         config.suppressedVisualEffects = 0;
         return config;
     }
+
+    private void writeRuleXml(ZenModeConfig.ZenRule rule, ByteArrayOutputStream os)
+            throws IOException {
+        String tag = "tag";
+
+        TypedXmlSerializer out = Xml.newFastSerializer();
+        out.setOutput(new BufferedOutputStream(os), "utf-8");
+        out.startDocument(null, true);
+        out.startTag(null, tag);
+        ZenModeConfig.writeRuleXml(rule, out);
+        out.endTag(null, tag);
+        out.endDocument();
+    }
+
+    private ZenModeConfig.ZenRule readRuleXml(ByteArrayInputStream is)
+            throws XmlPullParserException, IOException {
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(is), null);
+        parser.nextTag();
+        return ZenModeConfig.readRuleXml(parser);
+    }
+
+    private void writePolicyXml(ZenPolicy policy, ByteArrayOutputStream os) throws IOException {
+        String tag = "tag";
+
+        TypedXmlSerializer out = Xml.newFastSerializer();
+        out.setOutput(new BufferedOutputStream(os), "utf-8");
+        out.startDocument(null, true);
+        out.startTag(null, tag);
+        ZenModeConfig.writeZenPolicyXml(policy, out);
+        out.endTag(null, tag);
+        out.endDocument();
+    }
+
+    private ZenPolicy readPolicyXml(ByteArrayInputStream is)
+            throws XmlPullParserException, IOException {
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(is), null);
+        parser.nextTag();
+        return ZenModeConfig.readZenPolicyXml(parser);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index a017bd6..ea50179 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2324,6 +2324,32 @@
     }
 
     @Test
+    public void testActivityServiceConnectionsHolder() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final ActivityServiceConnectionsHolder<Object> holder =
+                mAtm.mInternal.getServiceConnectionsHolder(activity.token);
+        assertNotNull(holder);
+        final Object connection = new Object();
+        holder.addConnection(connection);
+        assertTrue(holder.isActivityVisible());
+        final int[] count = new int[1];
+        final Consumer<Object> c = conn -> count[0]++;
+        holder.forEachConnection(c);
+        assertEquals(1, count[0]);
+
+        holder.removeConnection(connection);
+        holder.forEachConnection(c);
+        assertEquals(1, count[0]);
+
+        activity.setVisibleRequested(false);
+        activity.setState(STOPPED, "test");
+        assertFalse(holder.isActivityVisible());
+
+        activity.removeImmediately();
+        assertNull(mAtm.mInternal.getServiceConnectionsHolder(activity.token));
+    }
+
+    @Test
     public void testTransferLaunchCookieWhenFinishing() {
         final ActivityRecord activity1 = createActivityWithTask();
         final Binder launchCookie = new Binder();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index eed32d7..bb20244 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -249,6 +249,19 @@
     }
 
     /**
+     * Ensures it updates recent tasks order when the last resumed activity changed.
+     */
+    @Test
+    public void testUpdateRecentTasksForTopResumed() {
+        spyOn(mSupervisor.mRecentTasks);
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task task = activity.getTask();
+
+        mAtm.setLastResumedActivityUncheckLocked(activity, "test");
+        verify(mSupervisor.mRecentTasks).add(eq(task));
+    }
+
+    /**
      * Ensures that a trusted display can launch arbitrary activity and an untrusted display can't.
      */
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index e30e5db..74ea7d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -40,7 +40,7 @@
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.view.WindowManager;
-import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackNavigationInfo;
 import android.window.IOnBackInvokedCallback;
 import android.window.OnBackInvokedCallback;
@@ -242,11 +242,11 @@
     private IOnBackInvokedCallback createOnBackInvokedCallback() {
         return new IOnBackInvokedCallback.Stub() {
             @Override
-            public void onBackStarted(BackEvent backEvent) {
+            public void onBackStarted(BackMotionEvent backEvent) {
             }
 
             @Override
-            public void onBackProgressed(BackEvent backEvent) {
+            public void onBackProgressed(BackMotionEvent backEvent) {
             }
 
             @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
index 86732c9..2a28ae2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
@@ -16,13 +16,12 @@
 
 package com.android.server.wm;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -32,9 +31,10 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
+
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 import java.util.function.Consumer;
 
@@ -48,92 +48,76 @@
 @Presubmit
 public class DeviceStateControllerTests {
 
-    private DeviceStateController.FoldStateListener mFoldStateListener;
     private DeviceStateController mTarget;
     private DeviceStateControllerBuilder mBuilder;
 
     private Context mMockContext;
-    private Handler mMockHandler;
-    private Resources mMockRes;
     private DeviceStateManager mMockDeviceStateManager;
-
-    private Consumer<DeviceStateController.FoldState> mDelegate;
-    private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+    private DeviceStateController.DeviceState mCurrentState =
+            DeviceStateController.DeviceState.UNKNOWN;
 
     @Before
     public void setUp() {
         mBuilder = new DeviceStateControllerBuilder();
-        mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+        mCurrentState = DeviceStateController.DeviceState.UNKNOWN;
     }
 
-    private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception {
+    private void initialize(boolean supportFold, boolean supportHalfFold) {
         mBuilder.setSupportFold(supportFold, supportHalfFold);
-        mDelegate = (newFoldState) -> {
+        Consumer<DeviceStateController.DeviceState> delegate = (newFoldState) -> {
             mCurrentState = newFoldState;
         };
-        mBuilder.setDelegate(mDelegate);
+        mBuilder.setDelegate(delegate);
         mBuilder.build();
-        verifyFoldStateListenerRegistration(1);
+        verify(mMockDeviceStateManager).registerCallback(any(), any());
     }
 
     @Test
-    public void testInitialization() throws Exception {
+    public void testInitialization() {
         initialize(true /* supportFold */, true /* supportHalfFolded */);
-        mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+        mTarget.onStateChanged(mOpenDeviceStates[0]);
+        assertEquals(DeviceStateController.DeviceState.OPEN, mCurrentState);
     }
 
     @Test
-    public void testInitializationWithNoFoldSupport() throws Exception {
+    public void testInitializationWithNoFoldSupport() {
         initialize(false /* supportFold */, false /* supportHalfFolded */);
-        mFoldStateListener.onStateChanged(mFoldedStates[0]);
+        mTarget.onStateChanged(mFoldedStates[0]);
         // Note that the folded state is ignored.
-        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+        assertEquals(DeviceStateController.DeviceState.UNKNOWN, mCurrentState);
     }
 
     @Test
-    public void testWithFoldSupported() throws Exception {
+    public void testWithFoldSupported() {
         initialize(true /* supportFold */, false /* supportHalfFolded */);
-        mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
-        mFoldStateListener.onStateChanged(mFoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
-        mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored
+        mTarget.onStateChanged(mOpenDeviceStates[0]);
+        assertEquals(DeviceStateController.DeviceState.OPEN, mCurrentState);
+        mTarget.onStateChanged(mFoldedStates[0]);
+        assertEquals(DeviceStateController.DeviceState.FOLDED, mCurrentState);
+        mTarget.onStateChanged(mHalfFoldedStates[0]);
+        assertEquals(DeviceStateController.DeviceState.UNKNOWN, mCurrentState); // Ignored
     }
 
     @Test
-    public void testWithHalfFoldSupported() throws Exception {
+    public void testWithHalfFoldSupported() {
         initialize(true /* supportFold */, true /* supportHalfFolded */);
-        mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
-        mFoldStateListener.onStateChanged(mFoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
-        mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
-        assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED);
+        mTarget.onStateChanged(mOpenDeviceStates[0]);
+        assertEquals(DeviceStateController.DeviceState.OPEN, mCurrentState);
+        mTarget.onStateChanged(mFoldedStates[0]);
+        assertEquals(DeviceStateController.DeviceState.FOLDED, mCurrentState);
+        mTarget.onStateChanged(mHalfFoldedStates[0]);
+        assertEquals(DeviceStateController.DeviceState.HALF_FOLDED, mCurrentState);
     }
 
-
     private final int[] mFoldedStates = {0};
-    private final int[] mUnfoldedStates = {1};
+    private final int[] mOpenDeviceStates = {1};
     private final int[] mHalfFoldedStates = {2};
-
-
-    private void verifyFoldStateListenerRegistration(int numOfInvocation) {
-        final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor =
-                ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class);
-        verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback(
-                any(),
-                listenerCaptor.capture());
-        if (numOfInvocation > 0) {
-            mFoldStateListener = listenerCaptor.getValue();
-        }
-    }
+    private final int[] mRearDisplayStates = {3};
 
     private class DeviceStateControllerBuilder {
         private boolean mSupportFold = false;
         private boolean mSupportHalfFold = false;
-        private Consumer<DeviceStateController.FoldState> mDelegate;
+        private Consumer<DeviceStateController.DeviceState> mDelegate;
 
         DeviceStateControllerBuilder setSupportFold(
                 boolean supportFold, boolean supportHalfFold) {
@@ -143,34 +127,44 @@
         }
 
         DeviceStateControllerBuilder setDelegate(
-                Consumer<DeviceStateController.FoldState> delegate) {
+                Consumer<DeviceStateController.DeviceState> delegate) {
             mDelegate = delegate;
             return this;
         }
 
         private void mockFold(boolean enableFold, boolean enableHalfFold) {
+            if (enableFold || enableHalfFold) {
+                when(mMockContext.getResources()
+                        .getIntArray(R.array.config_openDeviceStates))
+                        .thenReturn(mOpenDeviceStates);
+                when(mMockContext.getResources()
+                        .getIntArray(R.array.config_rearDisplayDeviceStates))
+                        .thenReturn(mRearDisplayStates);
+            }
+
             if (enableFold) {
-                when(mMockContext.getResources().getIntArray(
-                        com.android.internal.R.array.config_foldedDeviceStates))
+                when(mMockContext.getResources()
+                        .getIntArray(R.array.config_foldedDeviceStates))
                         .thenReturn(mFoldedStates);
             }
             if (enableHalfFold) {
-                when(mMockContext.getResources().getIntArray(
-                        com.android.internal.R.array.config_halfFoldedDeviceStates))
+                when(mMockContext.getResources()
+                        .getIntArray(R.array.config_halfFoldedDeviceStates))
                         .thenReturn(mHalfFoldedStates);
             }
         }
 
-        private void build() throws Exception {
+        private void build() {
             mMockContext = mock(Context.class);
-            mMockRes = mock(Resources.class);
-            when(mMockContext.getResources()).thenReturn((mMockRes));
             mMockDeviceStateManager = mock(DeviceStateManager.class);
             when(mMockContext.getSystemService(DeviceStateManager.class))
                     .thenReturn(mMockDeviceStateManager);
+            Resources mockRes = mock(Resources.class);
+            when(mMockContext.getResources()).thenReturn((mockRes));
             mockFold(mSupportFold, mSupportHalfFold);
-            mMockHandler = mock(Handler.class);
-            mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate);
+            Handler mockHandler = mock(Handler.class);
+            mTarget = new DeviceStateController(mMockContext, mockHandler);
+            mTarget.registerDeviceStateCallback(mDelegate);
         }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 98e68ca..dc12469 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1736,7 +1736,7 @@
 
         // No need to apply rotation if the display ignores orientation request.
         doCallRealMethod().when(displayContent).rotationForActivityInDifferentOrientation(any());
-        pinnedActivity.mOrientation = SCREEN_ORIENTATION_LANDSCAPE;
+        pinnedActivity.setOverrideOrientation(SCREEN_ORIENTATION_LANDSCAPE);
         displayContent.setIgnoreOrientationRequest(true);
         assertEquals(WindowConfiguration.ROTATION_UNDEFINED,
                 displayContent.rotationForActivityInDifferentOrientation(pinnedActivity));
@@ -2064,7 +2064,8 @@
         // Update the forced size and density in settings and the unique id to simualate a display
         // remap.
         dc.mWmService.mDisplayWindowSettings.setForcedSize(dc, forcedWidth, forcedHeight);
-        dc.mWmService.mDisplayWindowSettings.setForcedDensity(dc, forcedDensity, 0 /* userId */);
+        dc.mWmService.mDisplayWindowSettings.setForcedDensity(displayInfo, forcedDensity,
+                0 /* userId */);
         dc.mCurrentUniqueDisplayId = mDisplayInfo.uniqueId + "-test";
         // Trigger display changed.
         dc.onDisplayChanged();
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 8bb79e3..c2b3783 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -30,7 +31,9 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 
 import static org.junit.Assert.assertEquals;
@@ -58,6 +61,8 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -128,6 +133,95 @@
     }
 
     @Test
+    public void testOpenedCameraInSplitScreen_showToast() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        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).showToast(
+                R.string.display_rotation_camera_compat_toast_in_split_screen);
+    }
+
+    @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()))
+                .thenReturn(false);
+        spyOn(mDisplayRotationCompatPolicy);
+
+        mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+        verify(mDisplayRotationCompatPolicy, never()).showToast(
+                R.string.display_rotation_camera_compat_toast_after_rotation);
+    }
+
+    @Test
+    public void testOnScreenRotationAnimationFinished_noOpenCamera_doNotShowToast() {
+        spyOn(mDisplayRotationCompatPolicy);
+
+        mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+        verify(mDisplayRotationCompatPolicy, never()).showToast(
+                R.string.display_rotation_camera_compat_toast_after_rotation);
+    }
+
+    @Test
+    public void testOnScreenRotationAnimationFinished_notFullscreen_doNotShowToast() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        doReturn(true).when(mActivity).inMultiWindowMode();
+        spyOn(mDisplayRotationCompatPolicy);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+        verify(mDisplayRotationCompatPolicy, never()).showToast(
+                R.string.display_rotation_camera_compat_toast_after_rotation);
+    }
+
+    @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);
+        spyOn(mDisplayRotationCompatPolicy);
+
+        mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+        verify(mDisplayRotationCompatPolicy).showToast(
+                R.string.display_rotation_camera_compat_toast_after_rotation);
+    }
+
+    @Test
     public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
         when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
                     /* checkDeviceConfig */ anyBoolean()))
@@ -155,6 +249,18 @@
     }
 
     @Test
+    public void testTreatmentDisabledPerApp_noForceRotationOrRefresh()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        when(mActivity.mLetterboxUiController.shouldForceRotateForCameraCompat())
+                .thenReturn(false);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
     public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent);
@@ -327,7 +433,21 @@
     }
 
     @Test
-    public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+    public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        when(mActivity.mLetterboxUiController.shouldRefreshActivityForCameraCompat())
+                .thenReturn(false);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+        assertActivityRefreshRequested(/* refreshRequested */ false);
+    }
+
+    @Test
+    public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh()
+            throws Exception {
         when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -362,6 +482,19 @@
         assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
     }
 
+    @Test
+    public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat())
+                .thenReturn(true);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+        assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+    }
+
     private void configureActivity(@ScreenOrientation int activityOrientation) {
         configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index 4ce43e1..ed2b0a3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -56,6 +56,7 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceStateManager;
 import android.os.PowerManagerInternal;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
@@ -111,6 +112,7 @@
     private ContentResolver mMockResolver;
     private FakeSettingsProvider mFakeSettingsProvider;
     private StatusBarManagerInternal mMockStatusBarManagerInternal;
+    private DeviceStateManager mMockDeviceStateManager;
 
     // Fields below are callbacks captured from test target.
     private ContentObserver mShowRotationSuggestionsObserver;
@@ -120,6 +122,7 @@
 
     private DisplayRotationBuilder mBuilder;
 
+    private DeviceStateController mDeviceStateController;
     private DisplayRotation mTarget;
 
     @BeforeClass
@@ -484,6 +487,34 @@
                 SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
     }
 
+    @Test
+    public void testReverseRotation() throws Exception {
+        mBuilder.build();
+        configureDisplayRotation(SCREEN_ORIENTATION_PORTRAIT, false, false);
+
+        when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()).thenReturn(true);
+
+        thawRotation();
+
+        enableOrientationSensor();
+
+        mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_90));
+        assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+        mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_270));
+        assertEquals(Surface.ROTATION_90, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+        mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
+        assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+        mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_180));
+        assertEquals(Surface.ROTATION_180, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_180));
+    }
+
     private boolean waitForUiHandler() {
         final CountDownLatch latch = new CountDownLatch(1);
         UiThread.getHandler().post(latch::countDown);
@@ -705,7 +736,7 @@
 
         enableOrientationSensor();
 
-        mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+        mTarget.foldStateChanged(DeviceStateController.DeviceState.OPEN);
         freezeRotation(Surface.ROTATION_270);
 
         mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
@@ -715,7 +746,7 @@
                 SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
 
         // ... until half-fold
-        mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED);
+        mTarget.foldStateChanged(DeviceStateController.DeviceState.HALF_FOLDED);
         assertTrue(waitForUiHandler());
         verify(sMockWm).updateRotation(false, false);
         assertTrue(waitForUiHandler());
@@ -723,7 +754,7 @@
                 SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
 
         // ... then transition back to flat
-        mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+        mTarget.foldStateChanged(DeviceStateController.DeviceState.OPEN);
         assertTrue(waitForUiHandler());
         verify(sMockWm, atLeast(1)).updateRotation(false, false);
         assertTrue(waitForUiHandler());
@@ -1097,8 +1128,14 @@
 
             mMockDisplayWindowSettings = mock(DisplayWindowSettings.class);
 
+            mMockDeviceStateManager = mock(DeviceStateManager.class);
+            when(mMockContext.getSystemService(eq(DeviceStateManager.class)))
+                    .thenReturn(mMockDeviceStateManager);
+
+            mDeviceStateController = mock(DeviceStateController.class);
             mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress,
-                    mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object()) {
+                    mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object(),
+                    mDeviceStateController) {
                 @Override
                 DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy(
                         WindowManagerService service, DisplayContent displayContent) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index c398a0a..fb4f2ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -236,8 +236,8 @@
 
     @Test
     public void testSetForcedDensity() {
-        mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay, 600 /* density */,
-                0 /* userId */);
+        mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(),
+                600 /* density */, 0 /* userId */);
         mDisplayWindowSettings.applySettingsToDisplayLocked(mSecondaryDisplay);
 
         assertEquals(600 /* density */, mSecondaryDisplay.mBaseDisplayDensity);
@@ -439,8 +439,9 @@
     public void testDisplayWindowSettingsAppliedOnDisplayReady() {
         // Set forced densities for two displays in DisplayWindowSettings
         final DisplayContent dc = createMockSimulatedDisplay();
-        mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay, 123, 0 /* userId */);
-        mDisplayWindowSettings.setForcedDensity(dc, 456, 0 /* userId */);
+        mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay.getDisplayInfo(), 123,
+                0 /* userId */);
+        mDisplayWindowSettings.setForcedDensity(dc.getDisplayInfo(), 456, 0 /* userId */);
 
         // Apply settings to displays - the settings will be stored, but config will not be
         // recalculated immediately.
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index ead1a86..e1fc0cf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -18,7 +18,6 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.server.wm.LetterboxConfiguration.DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
@@ -26,8 +25,6 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -37,7 +34,6 @@
 
 import android.content.Context;
 import android.platform.test.annotations.Presubmit;
-import android.provider.DeviceConfig;
 
 import androidx.test.filters.SmallTest;
 
@@ -51,7 +47,7 @@
  * Tests for the {@link LetterboxConfiguration} class.
  *
  * Build/Install/Run:
- *  atest WmTests:LetterboxConfigurationTests
+ *  atest WmTests:LetterboxConfigurationTest
  */
 @SmallTest
 @Presubmit
@@ -233,34 +229,6 @@
                 LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
     }
 
-    @Test
-    public void testIsCompatFakeFocusEnabledOnDevice() {
-        boolean wasFakeFocusEnabled = DeviceConfig
-                .getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, false);
-
-        // Set runtime flag to true and build time flag to false
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "true", false);
-        mLetterboxConfiguration.setIsCompatFakeFocusEnabled(false);
-        assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
-
-        // Set runtime flag to false and build time flag to true
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "false", false);
-        mLetterboxConfiguration.setIsCompatFakeFocusEnabled(true);
-        assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
-
-        // Set runtime flag to true so that both are enabled
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "true", false);
-        assertTrue(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
-
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, Boolean.toString(wasFakeFocusEnabled),
-                false);
-    }
-
     private void assertForHorizontalMove(int from, int expected, int expectedTime,
             boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
         // We are in the current position
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 6d778afe..656c07b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -16,27 +16,65 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+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.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 
+import android.annotation.Nullable;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.Property;
+import android.content.res.Resources;
+import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.RoundedCorner;
+import android.view.RoundedCorners;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
 import org.junit.Before;
@@ -55,14 +93,24 @@
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class LetterboxUiControllerTest extends WindowTestsBase {
+    private static final int TASKBAR_COLLAPSED_HEIGHT = 10;
+    private static final int TASKBAR_EXPANDED_HEIGHT = 20;
+    private static final int SCREEN_WIDTH = 200;
+    private static final int SCREEN_HEIGHT = 100;
+    private static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0,
+            SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
+    private static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0,
+            SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
 
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
 
     private ActivityRecord mActivity;
+    private Task mTask;
     private DisplayContent mDisplayContent;
     private LetterboxUiController mController;
     private LetterboxConfiguration mLetterboxConfiguration;
+    private final Rect mLetterboxedPortraitTaskBounds = new Rect();
 
     @Before
     public void setUp() throws Exception {
@@ -74,6 +122,8 @@
         mController = new LetterboxUiController(mWm, mActivity);
     }
 
+    // shouldIgnoreRequestedOrientation
+
     @Test
     @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
     public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
@@ -134,7 +184,7 @@
     }
 
     @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
     public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
         prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
         doReturn(false).when(mLetterboxConfiguration)
@@ -143,6 +193,546 @@
         assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
     }
 
+    // shouldRefreshActivityForCameraCompat
+
+    @Test
+    public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() {
+        doReturn(false).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+    public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+    public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    // shouldRefreshActivityViaPauseForCameraCompat
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+    public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() {
+        doReturn(false).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+    public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+    public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    @Test
+    public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    // shouldForceRotateForCameraCompat
+
+    @Test
+    public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() {
+        doReturn(false).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
+    public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
+    public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    public void testGetCropBoundsIfNeeded_noCrop() {
+        final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
+
+        // Do not apply crop if taskbar is collapsed
+        taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
+        assertNull(mController.getExpandedTaskbarOrNull(mainWindow));
+
+        mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4,
+                SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4);
+
+        final Rect noCrop = mController.getCropBoundsIfNeeded(mainWindow);
+        assertNotEquals(null, noCrop);
+        assertEquals(0, noCrop.left);
+        assertEquals(0, noCrop.top);
+        assertEquals(mLetterboxedPortraitTaskBounds.width(), noCrop.right);
+        assertEquals(mLetterboxedPortraitTaskBounds.height(), noCrop.bottom);
+    }
+
+    @Test
+    public void testGetCropBoundsIfNeeded_appliesCrop() {
+        final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
+
+        // Apply crop if taskbar is expanded
+        taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
+        assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow));
+
+        mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
+                SCREEN_HEIGHT);
+
+        final Rect crop = mController.getCropBoundsIfNeeded(mainWindow);
+        assertNotEquals(null, crop);
+        assertEquals(0, crop.left);
+        assertEquals(0, crop.top);
+        assertEquals(mLetterboxedPortraitTaskBounds.width(), crop.right);
+        assertEquals(mLetterboxedPortraitTaskBounds.height() - TASKBAR_EXPANDED_HEIGHT,
+                crop.bottom);
+    }
+
+    @Test
+    public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
+        final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
+        final float scaling = 2.0f;
+
+        // Apply crop if taskbar is expanded
+        taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
+        assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow));
+        // With SizeCompat scaling
+        doReturn(true).when(mActivity).inSizeCompatMode();
+        mainWindow.mInvGlobalScale = scaling;
+
+        mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
+                SCREEN_HEIGHT);
+
+        final int appWidth = mLetterboxedPortraitTaskBounds.width();
+        final int appHeight = mLetterboxedPortraitTaskBounds.height();
+
+        final Rect crop = mController.getCropBoundsIfNeeded(mainWindow);
+        assertNotEquals(null, crop);
+        assertEquals(0, crop.left);
+        assertEquals(0, crop.top);
+        assertEquals((int) (appWidth * scaling), crop.right);
+        assertEquals((int) ((appHeight - TASKBAR_EXPANDED_HEIGHT) * scaling), crop.bottom);
+    }
+
+    @Test
+    public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() {
+        final float invGlobalScale = 0.5f;
+        final int expectedRadius = 7;
+        final int configurationRadius = 15;
+
+        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
+        mainWindow.mInvGlobalScale = invGlobalScale;
+        final InsetsState insets = mainWindow.getInsetsState();
+
+        RoundedCorners roundedCorners = new RoundedCorners(
+                /*topLeft=*/ null,
+                /*topRight=*/ null,
+                /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT,
+                    configurationRadius, /*centerX=*/ 1, /*centerY=*/ 1),
+                /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT,
+                    configurationRadius * 2 /*2 is to test selection of the min radius*/,
+                    /*centerX=*/ 1, /*centerY=*/ 1)
+        );
+        doReturn(roundedCorners).when(insets).getRoundedCorners();
+        mLetterboxConfiguration.setLetterboxActivityCornersRadius(-1);
+
+        assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
+    }
+
+    @Test
+    public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() {
+        final float invGlobalScale = 0.5f;
+        final int expectedRadius = 7;
+        final int configurationRadius = 15;
+
+        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
+        mainWindow.mInvGlobalScale = invGlobalScale;
+        mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+
+        assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
+
+    }
+
+    @Test
+    public void testGetRoundedCornersRadius_noScalingApplied() {
+        final int configurationRadius = 15;
+
+        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
+        mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+
+        mainWindow.mInvGlobalScale = -1f;
+        assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
+
+        mainWindow.mInvGlobalScale = 0f;
+        assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
+
+        mainWindow.mInvGlobalScale = 1f;
+        assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
+    }
+
+    private WindowState mockForGetCropBoundsAndRoundedCorners(@Nullable InsetsSource taskbar) {
+        final WindowState mainWindow = mock(WindowState.class);
+        final InsetsState insets = mock(InsetsState.class);
+        final Resources resources = mWm.mContext.getResources();
+        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+
+        mainWindow.mInvGlobalScale = 1f;
+        spyOn(resources);
+        spyOn(mActivity);
+
+        if (taskbar != null) {
+            taskbar.setVisible(true);
+            doReturn(taskbar).when(insets).peekSource(taskbar.getType());
+        }
+        doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds();
+        doReturn(true).when(mActivity).isVisible();
+        doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+        doReturn(insets).when(mainWindow).getInsetsState();
+        doReturn(attrs).when(mainWindow).getAttrs();
+        doReturn(true).when(mainWindow).isDrawn();
+        doReturn(true).when(mainWindow).isOnScreen();
+        doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout();
+        doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed();
+        doReturn(true).when(mLetterboxConfiguration).isLetterboxActivityCornersRounded();
+        doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize(
+                R.dimen.taskbar_frame_height);
+
+        // Need to reinitialise due to the change in resources getDimensionPixelSize output.
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        return mainWindow;
+    }
+
+    // overrideOrientationIfNeeded
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+    public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait()
+            throws Exception {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_PORTRAIT);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+    public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_NOSENSOR);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+    public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_returnsUnchanged() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_PORTRAIT);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+    public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationPortraitOrUndefined_returnsUnchanged() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_PORTRAIT);
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+    public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationLandscape_returnsReverseLandscape() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_LANDSCAPE),
+                SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+    public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_returnsUnchanged() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_NOSENSOR), SCREEN_ORIENTATION_NOSENSOR);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+    public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_NOSENSOR), SCREEN_ORIENTATION_PORTRAIT);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
+    public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_NOSENSOR);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+    public void testOverrideOrientationIfNeeded_propertyIsFalse_returnsUnchanged()
+            throws Exception {
+        mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+            OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+    public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
+        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+        // Recreate DisplayContent with DisplayRotationCompatPolicy
+        mActivity = setUpActivityWithComponent();
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+        doReturn(false).when(mDisplayContent.mDisplayRotationCompatPolicy)
+                .isActivityEligibleForOrientationOverride(eq(mActivity));
+
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+            OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+    public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
+        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+        // Recreate DisplayContent with DisplayRotationCompatPolicy
+        mActivity = setUpActivityWithComponent();
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
+                .isActivityEligibleForOrientationOverride(eq(mActivity));
+
+        assertEquals(mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_PORTRAIT);
+    }
+
+    // shouldUseDisplayLandscapeNaturalOrientation
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+    public void testShouldUseDisplayLandscapeNaturalOrientation_override_returnsTrue() {
+        prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+        assertTrue(mController.shouldUseDisplayLandscapeNaturalOrientation());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+    public void testShouldUseDisplayLandscapeNaturalOrientation_overrideAndFalseProperty_returnsFalse()
+            throws Exception {
+        mockThatProperty(PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+        assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+    public void testShouldUseDisplayLandscapeNaturalOrientation_portraitNaturalOrientation_returnsFalse() {
+        prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+        doReturn(ORIENTATION_PORTRAIT).when(mDisplayContent).getNaturalOrientation();
+
+        assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+    public void testShouldUseDisplayLandscapeNaturalOrientation_disabledIgnoreOrientationRequest_returnsFalse() {
+        prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+        mDisplayContent.setIgnoreOrientationRequest(false);
+
+        assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+    public void testShouldUseDisplayLandscapeNaturalOrientation_inMultiWindowMode_returnsFalse() {
+        prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+
+        spyOn(mTask);
+        doReturn(true).when(mTask).inMultiWindowMode();
+
+        assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+    public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
+        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldSendFakeFocus());
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+    public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
+        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldSendFakeFocus());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+    public void testIsCompatFakeFocusEnabled_propertyDisabledAndOverrideEnabled_fakeFocusDisabled()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldSendFakeFocus());
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+    public void testIsCompatFakeFocusEnabled_propertyEnabled_noOverride_fakeFocusEnabled()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldSendFakeFocus());
+    }
+
+    @Test
+    public void testIsCompatFakeFocusEnabled_propertyDisabled_fakeFocusDisabled()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldSendFakeFocus());
+    }
+
+    @Test
+    public void testIsCompatFakeFocusEnabled_propertyEnabled_fakeFocusEnabled()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldSendFakeFocus());
+    }
+
     private void mockThatProperty(String propertyName, boolean value) throws Exception {
         Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
                  /* className */ "");
@@ -151,6 +741,12 @@
         doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
     }
 
+    private void prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation() {
+        spyOn(mDisplayContent);
+        doReturn(ORIENTATION_LANDSCAPE).when(mDisplayContent).getNaturalOrientation();
+        mDisplayContent.setIgnoreOrientationRequest(true);
+    }
+
     private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
         doReturn(true).when(mLetterboxConfiguration)
                 .isPolicyForIgnoringRequestedOrientationEnabled();
@@ -160,10 +756,10 @@
     private ActivityRecord setUpActivityWithComponent() {
         mDisplayContent = new TestDisplayContent
                 .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
-        Task task = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
+        mTask = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setOnTop(true)
-                .setTask(task)
+                .setTask(mTask)
                 // Set the component to be that of the test class in order to enable compat changes
                 .setComponent(ComponentName.createRelative(mContext,
                         com.android.server.wm.LetterboxUiControllerTest.class.getName()))
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index adf694c..db6ac0b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -30,6 +30,7 @@
 import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.os.Process.NOBODY_UID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -1220,20 +1221,35 @@
 
     @Test
     public void testCreateRecentTaskInfo_detachedTask() {
-        final Task task = createTaskBuilder(".Task").setCreateActivity(true).build();
+        final Task task = createTaskBuilder(".Task").build();
+        final ComponentName componentName = getUniqueComponentName();
+        new ActivityBuilder(mSupervisor.mService)
+                .setTask(task)
+                .setUid(NOBODY_UID)
+                .setComponent(componentName)
+                .build();
         final TaskDisplayArea tda = task.getDisplayArea();
 
         assertTrue(task.isAttached());
         assertTrue(task.supportsMultiWindow());
 
-        RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true);
+        RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertTrue(info.supportsMultiWindow);
 
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                false /* getTasksAllowed */);
+
+        assertFalse(info.topActivity.equals(componentName));
+        assertFalse(info.topActivityInfo.packageName.equals(componentName.getPackageName()));
+        assertFalse(info.baseActivity.equals(componentName));
+
         // The task can be put in split screen even if it is not attached now.
         task.removeImmediately();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true);
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertTrue(info.supportsMultiWindow);
 
@@ -1242,7 +1258,8 @@
         doReturn(false).when(tda).supportsNonResizableMultiWindow();
         doReturn(false).when(task).isResizeable();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true);
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertFalse(info.supportsMultiWindow);
 
@@ -1250,7 +1267,8 @@
         // the device supports it.
         doReturn(true).when(tda).supportsNonResizableMultiWindow();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true);
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertTrue(info.supportsMultiWindow);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index 4808474..06e3854 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -468,7 +468,7 @@
         mWm.setRecentsAnimationController(mController);
         spyOn(mDisplayContent.mFixedRotationTransitionListener);
         final ActivityRecord recents = mock(ActivityRecord.class);
-        recents.mOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+        recents.setOverrideOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
         doReturn(ORIENTATION_PORTRAIT).when(recents)
                 .getRequestedConfigurationOrientation(anyBoolean());
         mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recents);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 9d2eb26..63797778 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -21,7 +21,9 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.os.Parcel;
@@ -258,6 +260,14 @@
         assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+
+        // If there will be display size change when switching from preferred mode to default mode,
+        // then keep the current preferred mode during animating.
+        mDisplayInfo = spy(mDisplayInfo);
+        final Mode defaultMode = new Mode(4321 /* width */, 1234 /* height */, LOW_REFRESH_RATE);
+        doReturn(defaultMode).when(mDisplayInfo).getDefaultMode();
+        mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist);
+        assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
     }
 
     @Test
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 a8e9198..318fff2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -16,8 +16,10 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 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.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -53,8 +55,6 @@
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityRecord.State.STOPPED;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
-import static com.android.server.wm.LetterboxConfiguration.PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN;
-import static com.android.server.wm.LetterboxConfiguration.PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -98,7 +98,7 @@
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.statusbar.LetterboxDetails;
 import com.android.server.statusbar.StatusBarManagerInternal;
-import com.android.server.wm.DeviceStateController.FoldState;
+import com.android.server.wm.DeviceStateController.DeviceState;
 
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -197,27 +197,6 @@
     }
 
     @Test
-    public void testNotApplyStrategyToTranslucentActivitiesWithDifferentUid() {
-        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
-        setUpDisplaySizeWithApp(2000, 1000);
-        prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
-        mActivity.info.setMinAspectRatio(1.2f);
-        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        // Translucent Activity
-        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
-                .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
-                .setMinAspectRatio(1.1f)
-                .setMaxAspectRatio(3f)
-                .build();
-        doReturn(false).when(translucentActivity).fillsParent();
-        mTask.addChild(translucentActivity);
-        // We check bounds
-        final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
-        final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds();
-        assertNotEquals(opaqueBounds, translucentRequestedBounds);
-    }
-
-    @Test
     public void testApplyStrategyToMultipleTranslucentActivities() {
         mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
         setUpDisplaySizeWithApp(2000, 1000);
@@ -276,6 +255,57 @@
     }
 
     @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);
+        assertEquals(translucentActivity.getBounds(), mActivity.getBounds());
+
+        mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        spyOn(mActivity);
+
+        // Halffold
+        setFoldablePosture(translucentActivity, true /* isHalfFolded */,
+                false /* isTabletop */);
+        verify(mActivity).recomputeConfiguration();
+        assertEquals(translucentActivity.getBounds(), mActivity.getBounds());
+        clearInvocations(mActivity);
+
+        // Unfold
+        setFoldablePosture(translucentActivity, false /* isHalfFolded */,
+                false /* isTabletop */);
+        verify(mActivity).recomputeConfiguration();
+        assertEquals(translucentActivity.getBounds(), mActivity.getBounds());
+    }
+
+    @Test
     public void testRestartProcessIfVisible() {
         setUpDisplaySizeWithApp(1000, 2500);
         doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
@@ -478,7 +508,7 @@
 
         spyOn(mActivity.mLetterboxUiController);
         doReturn(true).when(mActivity.mLetterboxUiController)
-                .isSurfaceReadyAndVisible(any());
+                .isSurfaceVisible(any());
 
         assertTrue(mActivity.mLetterboxUiController.shouldShowLetterboxUi(
                 mActivity.findMainWindow()));
@@ -1410,6 +1440,65 @@
     }
 
     @Test
+    public void testGetLetterboxInnerBounds_noScalingApplied() {
+        // Set up a display in portrait and ignoring orientation request.
+        final int dw = 1400;
+        final int dh = 2800;
+        setUpDisplaySizeWithApp(dw, dh);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+        // Rotate display to landscape.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // Portrait fixed app without max aspect.
+        prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_LANDSCAPE);
+
+        // Need a window to call adjustBoundsForTaskbar with.
+        addWindowToActivity(mActivity);
+
+        // App should launch in fullscreen.
+        assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        assertFalse(mActivity.inSizeCompatMode());
+
+        // Activity inherits max bounds from TaskDisplayArea.
+        assertMaxBoundsInheritDisplayAreaBounds();
+
+        // Rotate display to portrait.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_0);
+
+        final Rect rotatedDisplayBounds = new Rect(mActivity.mDisplayContent.getBounds());
+        final Rect rotatedActivityBounds = new Rect(mActivity.getBounds());
+        assertTrue(rotatedDisplayBounds.width() < rotatedDisplayBounds.height());
+
+        // App should be in size compat.
+        assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        assertScaled();
+        assertThat(mActivity.inSizeCompatMode()).isTrue();
+        assertActivityMaxBoundsSandboxed();
+
+
+	final int scale = dh / dw;
+
+        // App bounds should be dh / scale x dw / scale
+        assertEquals(dw, rotatedDisplayBounds.width());
+        assertEquals(dh, rotatedDisplayBounds.height());
+
+        assertEquals(dh / scale, rotatedActivityBounds.width());
+        assertEquals(dw / scale, rotatedActivityBounds.height());
+
+        // Compute the frames of the window and invoke {@link ActivityRecord#layoutLetterbox}.
+        mActivity.mRootWindowContainer.performSurfacePlacement();
+
+        LetterboxDetails letterboxDetails = mActivity.mLetterboxUiController.getLetterboxDetails();
+
+        assertEquals(dh / scale, letterboxDetails.getLetterboxInnerBounds().width());
+        assertEquals(dw / scale, letterboxDetails.getLetterboxInnerBounds().height());
+
+        assertEquals(dw, letterboxDetails.getLetterboxFullBounds().width());
+        assertEquals(dh, letterboxDetails.getLetterboxFullBounds().height());
+    }
+
+    @Test
     public void testLaunchWithFixedRotationTransform() {
         final int dw = 1000;
         final int dh = 2500;
@@ -1897,6 +1986,43 @@
     }
 
     @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+    public void testOverrideRespectRequestedOrientationIsEnabled_orientationIsRespected() {
+        // Set up a display in landscape
+        setUpDisplaySizeWithApp(2800, 1400);
+
+        final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+                RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+        activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+        // Display should be rotated.
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+
+        // No size compat mode
+        assertFalse(activity.inSizeCompatMode());
+    }
+
+    @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+    public void testOverrideRespectRequestedOrientationIsEnabled_multiWindow_orientationIgnored() {
+        // Set up a display in landscape
+        setUpDisplaySizeWithApp(2800, 1400);
+
+        final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+                RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+        TaskFragment taskFragment = activity.getTaskFragment();
+        spyOn(taskFragment);
+        activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        doReturn(WINDOWING_MODE_MULTI_WINDOW).when(taskFragment).getWindowingMode();
+
+        // Display should not be rotated.
+        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+        // No size compat mode
+        assertFalse(activity.inSizeCompatMode());
+    }
+
+    @Test
     public void testSplitAspectRatioForUnresizableLandscapeApps() {
         // Set up a display in portrait and ignoring orientation request.
         int screenWidth = 1400;
@@ -1933,6 +2059,132 @@
     }
 
     @Test
+    public void testDisplayAspectRatioForResizablePortraitApps() {
+        // Set up a display in portrait and ignoring orientation request.
+        int displayWidth = 1400;
+        int displayHeight = 1600;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
+
+        // Enable display aspect ratio to take precedence before
+        // fixedOrientationLetterboxAspectRatio
+        mWm.mLetterboxConfiguration
+                .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+        // Set up resizable app in portrait
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, false /* isUnresizable */);
+
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+        // Move activity to split screen which takes half of the screen.
+        mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, displayWidth, getExpectedSplitSize(displayHeight));
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+        // App should launch in fixed orientation letterbox.
+        assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        // Checking that there is no size compat mode.
+        assertFitted();
+        // Check that the display aspect ratio is used by the app.
+        final float targetMinAspectRatio = 1f * displayHeight / displayWidth;
+        final float delta = 0.01f;
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(mActivity.getBounds()), delta);
+    }
+
+    @Test
+    public void testDisplayAspectRatioForResizableLandscapeApps() {
+        // Set up a display in landscape and ignoring orientation request.
+        int displayWidth = 1600;
+        int displayHeight = 1400;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
+
+        // Enable display aspect ratio to take precedence before
+        // fixedOrientationLetterboxAspectRatio
+        mWm.mLetterboxConfiguration
+                .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+        // Set up resizable app in landscape
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_LANDSCAPE, false /* isUnresizable */);
+
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+        // Move activity to split screen which takes half of the screen.
+        mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, getExpectedSplitSize(displayWidth), displayHeight);
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+        // App should launch in fixed orientation letterbox.
+        assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        // Checking that there is no size compat mode.
+        assertFitted();
+        // Check that the display aspect ratio is used by the app.
+        final float targetMinAspectRatio = 1f * displayWidth / displayHeight;
+        final float delta = 0.01f;
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(mActivity.getBounds()), delta);
+    }
+
+    @Test
+    public void testDisplayAspectRatioForUnresizableLandscapeApps() {
+        // Set up a display in portrait and ignoring orientation request.
+        int displayWidth = 1400;
+        int displayHeight = 1600;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        // Enable display aspect ratio to take precedence before
+        // fixedOrientationLetterboxAspectRatio
+        mWm.mLetterboxConfiguration
+                .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+        // App should launch in fixed orientation letterbox.
+        assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        // Checking that there is no size compat mode.
+        assertFitted();
+        // Check that the display aspect ratio is used by the app.
+        final float targetMinAspectRatio = 1f * displayHeight / displayWidth;
+        final float delta = 0.01f;
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(mActivity.getBounds()), delta);
+    }
+
+    @Test
+    public void testDisplayAspectRatioForUnresizablePortraitApps() {
+        // Set up a display in landscape and ignoring orientation request.
+        int displayWidth = 1600;
+        int displayHeight = 1400;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        // Enable display aspect ratio to take precedence before
+        // fixedOrientationLetterboxAspectRatio
+        mWm.mLetterboxConfiguration
+                .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+        // App should launch in fixed orientation letterbox.
+        assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        // Checking that there is no size compat mode.
+        assertFitted();
+        // Check that the display aspect ratio is used by the app.
+        final float targetMinAspectRatio = 1f * displayWidth / displayHeight;
+        final float delta = 0.01f;
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(mActivity.getBounds()), delta);
+    }
+
+    @Test
     public void
             testDisplayIgnoreOrientationRequest_orientationLetterboxBecameSizeCompatAfterRotate() {
         // Set up a display in landscape and ignoring orientation request.
@@ -2208,6 +2460,29 @@
     }
 
     @Test
+    public void testDisplayIgnoreOrientationRequest_disabledViaDeviceConfig_orientationRespected() {
+        // Set up a display in landscape
+        setUpDisplaySizeWithApp(2800, 1400);
+
+        final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+                RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+        activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+        spyOn(activity.mWmService.mLetterboxConfiguration);
+        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
+                .isIgnoreOrientationRequestAllowed();
+
+        // Display should not be rotated.
+        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+        doReturn(false).when(activity.mWmService.mLetterboxConfiguration)
+                .isIgnoreOrientationRequestAllowed();
+
+        // Display should be rotated.
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+    }
+
+    @Test
     public void testSandboxDisplayApis_unresizableAppNotSandboxed() {
         // Set up a display in landscape with an unresizable app.
         setUpDisplaySizeWithApp(2500, 1000);
@@ -2479,6 +2754,133 @@
     }
 
     @Test
+    public void testIsHorizontalReachabilityEnabled_splitScreen_false() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        setUpDisplaySizeWithApp(2800, 1000);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+        // Unresizable portrait-only activity.
+        prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT);
+
+        // Move activity to split screen which takes half of the screen.
+        mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, 1400, 1000);
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+        // Horizontal reachability is disabled because the app is in split screen.
+        assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+    }
+
+    @Test
+    public void testIsVerticalReachabilityEnabled_splitScreen_false() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        setUpDisplaySizeWithApp(1000, 2800);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+        // Unresizable landscape-only activity.
+        prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE);
+
+        // Move activity to split screen which takes half of the screen.
+        mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, 1000, 1400);
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+        // Vertical reachability is disabled because the app is in split screen.
+        assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+    }
+
+    @Test
+    public void testIsVerticalReachabilityEnabled_doesNotMatchParentWidth_false() {
+        setUpDisplaySizeWithApp(1000, 2800);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+
+        // Unresizable landscape-only activity.
+        prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE);
+
+        // Rotate to put activity in size compat mode.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // Activity now in size compat mode.
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Vertical reachability is disabled because the app does not match parent width
+        assertNotEquals(mActivity.getBounds().width(), mActivity.mDisplayContent.getBounds()
+                .width());
+        assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+    }
+
+    @Test
+    public void testIsHorizontalReachabilityEnabled_doesNotMatchParentHeight_false() {
+        setUpDisplaySizeWithApp(2800, 1000);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+
+        // Unresizable portrait-only activity.
+        prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT);
+
+        // Rotate to put activity in size compat mode.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // Activity now in size compat mode.
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Horizontal reachability is disabled because the app does not match parent height
+        assertNotEquals(mActivity.getBounds().height(), mActivity.mDisplayContent.getBounds()
+                .height());
+        assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+    }
+
+    @Test
+    public void testIsHorizontalReachabilityEnabled_inSizeCompatMode_matchesParentHeight_true() {
+        setUpDisplaySizeWithApp(1800, 2200);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+
+        // Unresizable portrait-only activity.
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+        // Rotate to put activity in size compat mode.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // Activity now in size compat mode.
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Horizontal reachability is enabled because the app matches parent height
+        assertEquals(mActivity.getBounds().height(), mActivity.mDisplayContent.getBounds()
+                .height());
+        assertTrue(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+    }
+
+    @Test
+    public void testIsVerticalReachabilityEnabled_inSizeCompatMode_matchesParentWidth_true() {
+        setUpDisplaySizeWithApp(2200, 1800);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+
+        // Unresizable landscape-only activity.
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+        // Rotate to put activity in size compat mode.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // Activity now in size compat mode.
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Vertical reachability is enabled because the app matches parent width
+        assertEquals(mActivity.getBounds().width(), mActivity.mDisplayContent.getBounds().width());
+        assertTrue(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+    }
+
+    @Test
     public void testLetterboxDetailsForStatusBar_noLetterbox() {
         setUpDisplaySizeWithApp(2800, 1000);
         addStatusBar(mActivity.mDisplayContent);
@@ -2589,7 +2991,7 @@
         mActivity.mRootWindowContainer.performSurfacePlacement();
 
         final ArgumentCaptor<Rect> cropCapturer = ArgumentCaptor.forClass(Rect.class);
-        verify(mTransaction, times(2)).setWindowCrop(
+        verify(mTransaction, times(2)).setCrop(
                 eq(w1.getSurfaceControl()),
                 cropCapturer.capture()
         );
@@ -2678,6 +3080,39 @@
     }
 
     @Test
+    public void testUpdateResolvedBoundsHorizontalPosition_leftInsets_appCentered() {
+        // Set up folded display
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1100, 2100)
+                .setCanRotate(true)
+                .build();
+        display.setIgnoreOrientationRequest(true);
+        final DisplayPolicy policy = display.getDisplayPolicy();
+        DisplayPolicy.DecorInsets.Info decorInfo = policy.getDecorInsetsInfo(ROTATION_90,
+                display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+        decorInfo.mNonDecorInsets.set(130, 0,  60, 0);
+        spyOn(policy);
+        doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90,
+                display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+
+        setUpApp(display);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+        // Resize the display to simulate unfolding in portrait
+        resizeDisplay(mTask.mDisplayContent, 2200, 1800);
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Simulate real display not taking non-decor insets into consideration
+        display.getWindowConfiguration().setAppBounds(0, 0, 2200, 1800);
+
+        // Rotate display to landscape
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // App is centered
+        assertEquals(mActivity.getBounds(), new Rect(350, 50, 1450, 2150));
+    }
+
+    @Test
     public void testUpdateResolvedBoundsHorizontalPosition_left() {
         // Display configured as (2800, 1400).
         assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity(
@@ -2823,6 +3258,20 @@
     }
 
     @Test
+    public void testApplyAspectRatio_containingRatioAlmostEqualToMaxRatio_boundsUnchanged() {
+        setUpDisplaySizeWithApp(1981, 2576);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+
+        final Rect originalBounds = new Rect(mActivity.getBounds());
+        prepareUnresizable(mActivity, 1.3f, SCREEN_ORIENTATION_UNSPECIFIED);
+
+        // The containing aspect ratio is now 1.3003534, while the desired aspect ratio is 1.3. The
+        // bounds of the activity should not be changed as the difference is too small
+        assertEquals(mActivity.getBounds(), originalBounds);
+    }
+
+    @Test
     public void testUpdateResolvedBoundsHorizontalPosition_activityFillParentWidth() {
         // When activity width equals parent width, multiplier shouldn't have any effect.
         assertHorizontalPositionForDifferentDisplayConfigsForLandscapeActivity(
@@ -2834,6 +3283,39 @@
     }
 
     @Test
+    public void testUpdateResolvedBoundsVerticalPosition_topInsets_appCentered() {
+        // Set up folded display
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 2100, 1100)
+                .setCanRotate(true)
+                .build();
+        display.setIgnoreOrientationRequest(true);
+        final DisplayPolicy policy = display.getDisplayPolicy();
+        DisplayPolicy.DecorInsets.Info decorInfo = policy.getDecorInsetsInfo(ROTATION_90,
+                display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+        decorInfo.mNonDecorInsets.set(0, 130,  0, 60);
+        spyOn(policy);
+        doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90,
+                display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+
+        setUpApp(display);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+        // Resize the display to simulate unfolding in portrait
+        resizeDisplay(mTask.mDisplayContent, 1800, 2200);
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Simulate real display not taking non-decor insets into consideration
+        display.getWindowConfiguration().setAppBounds(0, 0, 1800, 2200);
+
+        // Rotate display to landscape
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+        // App is centered
+        assertEquals(mActivity.getBounds(), new Rect(50, 350, 2150, 1450));
+    }
+
+    @Test
     public void testUpdateResolvedBoundsVerticalPosition_top() {
         // Display configured as (1400, 2800).
         assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
@@ -2951,14 +3433,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(FoldState.class), anyBoolean());
+        doReturn(false).when(r).isDeviceInPosture(any(DeviceState.class), anyBoolean());
         if (isHalfFolded) {
-            doReturn(true).when(r).isDeviceInPosture(FoldState.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
@@ -3303,7 +3791,8 @@
         assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
     }
 
-    private ActivityRecord setUpActivityForCompatFakeFocusTest() {
+    @Test
+    public void testShouldSendFakeFocus_compatFakeFocusEnabled() {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
                 .setOnTop(true)
@@ -3312,69 +3801,40 @@
                         com.android.server.wm.SizeCompatTests.class.getName()))
                 .build();
         final Task task = activity.getTask();
+        spyOn(activity.mLetterboxUiController);
+        doReturn(true).when(activity.mLetterboxUiController).shouldSendFakeFocus();
+
         task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        spyOn(activity.mWmService.mLetterboxConfiguration);
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
-                .isCompatFakeFocusEnabledOnDevice();
-        return activity;
-    }
-
-    @Test
-    @EnableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
-    public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
-        ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-
         assertTrue(activity.shouldSendCompatFakeFocus());
-    }
 
-    @Test
-    @DisableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
-    public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
-        ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
+        task.setWindowingMode(WINDOWING_MODE_PINNED);
+        assertFalse(activity.shouldSendCompatFakeFocus());
 
+        task.setWindowingMode(WINDOWING_MODE_FREEFORM);
         assertFalse(activity.shouldSendCompatFakeFocus());
     }
 
     @Test
-    @EnableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
-    public void testIsCompatFakeFocusEnabled_optOutPropertyAndOverrideEnabled_fakeFocusDisabled() {
-        ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
-                .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT));
+    public void testShouldSendFakeFocus_compatFakeFocusDisabled() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm)
+                .setCreateTask(true)
+                .setOnTop(true)
+                // Set the component to be that of the test class in order to enable compat changes
+                .setComponent(ComponentName.createRelative(mContext,
+                        com.android.server.wm.SizeCompatTests.class.getName()))
+                .build();
+        final Task task = activity.getTask();
+        spyOn(activity.mLetterboxUiController);
+        doReturn(false).when(activity.mLetterboxUiController).shouldSendFakeFocus();
 
-        assertFalse(activity.mWmService.mLetterboxConfiguration
-                .isCompatFakeFocusEnabled(activity.info));
-    }
+        task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        assertFalse(activity.shouldSendCompatFakeFocus());
 
-    @Test
-    @DisableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
-    public void testIsCompatFakeFocusEnabled_optInPropertyEnabled_noOverride_fakeFocusEnabled() {
-        ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
-                .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN));
+        task.setWindowingMode(WINDOWING_MODE_PINNED);
+        assertFalse(activity.shouldSendCompatFakeFocus());
 
-        assertTrue(activity.mWmService.mLetterboxConfiguration
-                .isCompatFakeFocusEnabled(activity.info));
-    }
-
-    @Test
-    public void testIsCompatFakeFocusEnabled_optOutPropertyEnabled_fakeFocusDisabled() {
-        ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
-                .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT));
-
-        assertFalse(activity.mWmService.mLetterboxConfiguration
-                .isCompatFakeFocusEnabled(activity.info));
-    }
-
-    @Test
-    public void testIsCompatFakeFocusEnabled_optInPropertyEnabled_fakeFocusEnabled() {
-        ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
-                .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN));
-
-        assertTrue(activity.mWmService.mLetterboxConfiguration
-                .isCompatFakeFocusEnabled(activity.info));
+        task.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        assertFalse(activity.shouldSendCompatFakeFocus());
     }
 
     private int getExpectedSplitSize(int dimensionToSplit) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 2420efc..8244f94 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -796,6 +796,72 @@
     }
 
     @Test
+    public void testApplyTransaction_createTaskFragment_overrideBounds() {
+        final Task task = createTask(mDisplayContent);
+        final ActivityRecord activityAtBottom = createActivityRecord(task);
+        final int uid = Binder.getCallingUid();
+        activityAtBottom.info.applicationInfo.uid = uid;
+        activityAtBottom.getTask().effectiveUid = uid;
+        mTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setFragmentToken(mFragmentToken)
+                .createActivityCount(1)
+                .build();
+        mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+        final IBinder fragmentToken1 = new Binder();
+        final Rect bounds = new Rect(100, 100, 500, 1000);
+        final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder(
+                mOrganizerToken, fragmentToken1, activityAtBottom.token)
+                .setPairedActivityToken(activityAtBottom.token)
+                .setInitialBounds(bounds)
+                .build();
+        mTransaction.setTaskFragmentOrganizer(mIOrganizer);
+        mTransaction.createTaskFragment(params);
+        assertApplyTransactionAllowed(mTransaction);
+
+        // Successfully created a TaskFragment.
+        final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(
+                fragmentToken1);
+        assertNotNull(taskFragment);
+        // The relative embedded bounds is updated to the initial requested bounds.
+        assertEquals(bounds, taskFragment.getRelativeEmbeddedBounds());
+    }
+
+    @Test
+    public void testApplyTransaction_createTaskFragment_withPairedActivityToken() {
+        final Task task = createTask(mDisplayContent);
+        final ActivityRecord activityAtBottom = createActivityRecord(task);
+        final int uid = Binder.getCallingUid();
+        activityAtBottom.info.applicationInfo.uid = uid;
+        activityAtBottom.getTask().effectiveUid = uid;
+        mTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setFragmentToken(mFragmentToken)
+                .createActivityCount(1)
+                .build();
+        mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+        final IBinder fragmentToken1 = new Binder();
+        final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder(
+                mOrganizerToken, fragmentToken1, activityAtBottom.token)
+                .setPairedActivityToken(activityAtBottom.token)
+                .build();
+        mTransaction.setTaskFragmentOrganizer(mIOrganizer);
+        mTransaction.createTaskFragment(params);
+        assertApplyTransactionAllowed(mTransaction);
+
+        // Successfully created a TaskFragment.
+        final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(
+                fragmentToken1);
+        assertNotNull(taskFragment);
+        // The new TaskFragment should be positioned right above the paired activity.
+        assertEquals(task.mChildren.indexOf(activityAtBottom) + 1,
+                task.mChildren.indexOf(taskFragment));
+        // The top TaskFragment should remain on top.
+        assertEquals(task.mChildren.indexOf(taskFragment) + 1,
+                task.mChildren.indexOf(mTaskFragment));
+    }
+
+    @Test
     public void testApplyTransaction_enforceHierarchyChange_reparentChildren() {
         doReturn(true).when(mTaskFragment).isAttached();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index c893255..132aa90 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -24,6 +24,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
@@ -603,4 +604,34 @@
         assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, tf1.getOrientation(SCREEN_ORIENTATION_UNSET));
         assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, task.getOrientation(SCREEN_ORIENTATION_UNSET));
     }
+
+    @Test
+    public void testUpdateImeParentForActivityEmbedding() {
+        // Setup two activities in ActivityEmbedding.
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(1)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        final TaskFragment tf1 = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(1)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        final ActivityRecord activity0 = tf0.getTopMostActivity();
+        final ActivityRecord activity1 = tf1.getTopMostActivity();
+        final WindowState win0 = createWindow(null, TYPE_BASE_APPLICATION, activity0, "win0");
+        final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity1, "win1");
+        doReturn(false).when(mDisplayContent).shouldImeAttachedToApp();
+
+        mDisplayContent.setImeInputTarget(win0);
+        mDisplayContent.setImeLayeringTarget(win1);
+
+        // The ImeParent should be the display.
+        assertEquals(mDisplayContent.getImeContainer().getParent().getSurfaceControl(),
+                mDisplayContent.computeImeParent());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index bf1d1fa..83be4f0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
+import static android.view.Surface.ROTATION_0;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -78,6 +79,12 @@
         final InputMonitor inputMonitor = getInputMonitor();
         spyOn(inputMonitor);
         doNothing().when(inputMonitor).resumeDispatchingLw(any());
+
+        // For devices that set the sysprop ro.bootanim.set_orientation_<display_id>
+        // See DisplayRotation#readDefaultDisplayRotation for context.
+        // Without that, meaning of height and width in context of the tests can be swapped if
+        // the default rotation is 90 or 270.
+        displayRotation.setRotation(ROTATION_0);
     }
 
     public static class Builder {
@@ -203,6 +210,7 @@
             }
 
             final int displayId = SystemServicesTestRule.sNextDisplayId++;
+            mInfo.displayId = displayId;
             final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
                     mInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
             final TestDisplayContent newDisplay = createInternal(display);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index d2cb7ba..e2db2e6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -222,7 +222,7 @@
     }
 
     @Override
-    public void onKeyguardOccludedChangedLw(boolean occluded) {
+    public void onKeyguardOccludedChangedLw(boolean occluded, boolean waitAppTransition) {
     }
 
     public void setSafeMode(boolean safeMode) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 06a79f4..1407cdd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -391,7 +391,7 @@
         dc.updateOrientation();
         dc.sendNewConfiguration();
         spyOn(wallpaperWindow);
-        doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getLastReportedBounds();
+        doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getParentFrame();
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index ed7d123..2446fc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -1557,7 +1557,7 @@
 
         @Override
         int getOrientation() {
-            return getOrientation(super.mOrientation);
+            return getOrientation(super.getOverrideOrientation());
         }
 
         @Override
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 0568f2a..219f441 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -544,7 +544,12 @@
         win.applyWithNextDraw(t -> handledT[0] = t);
         assertTrue(win.useBLASTSync());
         final SurfaceControl.Transaction drawT = new StubTransaction();
+        final SurfaceControl.Transaction currT = win.getSyncTransaction();
+        clearInvocations(currT);
+        win.mWinAnimator.mLastHidden = true;
         assertTrue(win.finishDrawing(drawT, Integer.MAX_VALUE));
+        // The draw transaction should be merged to current transaction even if the state is hidden.
+        verify(currT).merge(eq(drawT));
         assertEquals(drawT, handledT[0]);
         assertFalse(win.useBLASTSync());
 
@@ -1119,7 +1124,9 @@
         spyOn(app.getDisplayContent());
         app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        verify(app.getDisplayContent()).updateImeControlTarget();
+        // Expect updateImeParent will be invoked when the configuration of the IME control
+        // target has changed.
+        verify(app.getDisplayContent()).updateImeControlTarget(eq(true) /* updateImeParent */);
         assertEquals(mAppWindow, mDisplayContent.getImeTarget(IME_TARGET_CONTROL).getWindow());
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 268aa3e..f8b8094 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -253,6 +253,12 @@
         // device form factors.
         mAtm.mWindowManager.mLetterboxConfiguration
                 .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(false);
+        // Ensure aspect ratio for al apps isn't overridden on any device target.
+        // {@link com.android.internal.R.bool
+        // .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled}, may be set on
+        // some device form factors.
+        mAtm.mWindowManager.mLetterboxConfiguration
+                .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
 
         checkDeviceSpecificOverridesNotApplied();
     }
@@ -267,6 +273,8 @@
         mAtm.mWindowManager.mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
         mAtm.mWindowManager.mLetterboxConfiguration
                 .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+        mAtm.mWindowManager.mLetterboxConfiguration
+                .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
     }
 
     /**
diff --git a/services/usage/java/com/android/server/usage/TEST_MAPPING b/services/usage/java/com/android/server/usage/TEST_MAPPING
index 1c0c71b..a3fe6f2 100644
--- a/services/usage/java/com/android/server/usage/TEST_MAPPING
+++ b/services/usage/java/com/android/server/usage/TEST_MAPPING
@@ -18,9 +18,7 @@
           "exclude-filter": "com.android.server.usage.StorageStatsServiceTest"
         }
       ]
-    }
-  ],
-  "presubmit-large": [
+    },
     {
       "name": "CtsUsageStatsTestCases",
       "options": [
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
index 42a5af7..17c354a 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
@@ -36,6 +36,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 
 /**
@@ -106,6 +107,12 @@
         return false;
     }
 
+    /**
+     * List of connected MIDI devices
+     */
+    private final HashMap<String, UsbMidiDevice>
+            mMidiDevices = new HashMap<String, UsbMidiDevice>();
+
     // UsbMidiDevice for USB peripheral mode (gadget) device
     private UsbMidiDevice mPeripheralMidiDevice = null;
 
@@ -249,6 +256,8 @@
             }
         }
 
+        addMidiDevice(deviceAddress, usbDevice, parser, cardRec);
+
         logDevices("deviceAdded()");
 
         if (DEBUG) {
@@ -256,6 +265,54 @@
         }
     }
 
+    private void addMidiDevice(String deviceAddress, UsbDevice usbDevice,
+            UsbDescriptorParser parser, AlsaCardsParser.AlsaCardRecord cardRec) {
+        boolean hasMidi = parser.hasMIDIInterface();
+        // UsbHostManager will create UsbDirectMidiDevices instead if MIDI 2 is supported.
+        boolean hasMidi2 = parser.containsUniversalMidiDeviceEndpoint();
+        if (DEBUG) {
+            Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature);
+            Slog.d(TAG, "hasMidi2: " + hasMidi2);
+        }
+        if (mHasMidiFeature && hasMidi && !hasMidi2) {
+            Bundle properties = new Bundle();
+            String manufacturer = usbDevice.getManufacturerName();
+            String product = usbDevice.getProductName();
+            String version = usbDevice.getVersion();
+            String name;
+            if (manufacturer == null || manufacturer.isEmpty()) {
+                name = product;
+            } else if (product == null || product.isEmpty()) {
+                name = manufacturer;
+            } else {
+                name = manufacturer + " " + product;
+            }
+            properties.putString(MidiDeviceInfo.PROPERTY_NAME, name);
+            properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer);
+            properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product);
+            properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version);
+            properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER,
+                    usbDevice.getSerialNumber());
+            properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, cardRec.getCardNum());
+            properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, 0 /*deviceNum*/);
+            properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
+
+            int numLegacyMidiInputs = parser.calculateNumLegacyMidiInputs();
+            int numLegacyMidiOutputs = parser.calculateNumLegacyMidiOutputs();
+            if (DEBUG) {
+                Slog.d(TAG, "numLegacyMidiInputs: " + numLegacyMidiInputs);
+                Slog.d(TAG, "numLegacyMidiOutputs:" + numLegacyMidiOutputs);
+            }
+
+            UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
+                    cardRec.getCardNum(), 0 /*device*/, numLegacyMidiInputs,
+                    numLegacyMidiOutputs);
+            if (usbMidiDevice != null) {
+                mMidiDevices.put(deviceAddress, usbMidiDevice);
+            }
+        }
+    }
+
     /* package */ synchronized void usbDeviceRemoved(String deviceAddress/*UsbDevice usbDevice*/) {
         if (DEBUG) {
             Slog.d(TAG, "deviceRemoved(" + deviceAddress + ")");
@@ -269,6 +326,13 @@
             selectDefaultDevice(); // if there any external devices left, select one of them
         }
 
+        // MIDI
+        UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress);
+        if (usbMidiDevice != null) {
+            Slog.i(TAG, "USB MIDI Device Removed: " + deviceAddress);
+            IoUtils.closeQuietly(usbMidiDevice);
+        }
+
         logDevices("usbDeviceRemoved()");
 
     }
@@ -324,6 +388,12 @@
             usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES);
         }
 
+        for (String deviceAddr : mMidiDevices.keySet()) {
+            // A UsbMidiDevice does not have a handle to the UsbDevice anymore
+            mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices",
+                    UsbAlsaManagerProto.MIDI_DEVICES);
+        }
+
         dump.end(token);
     }
 
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 1b92699..e256f54 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -535,7 +535,6 @@
         private boolean mInHostModeWithNoAccessoryConnected;
         private boolean mSourcePower;
         private boolean mSinkPower;
-        private boolean mConfigured;
         private boolean mAudioAccessoryConnected;
         private boolean mAudioAccessorySupported;
 
@@ -568,7 +567,12 @@
         private final UsbPermissionManager mPermissionManager;
         private NotificationManager mNotificationManager;
 
+        /**
+         * Do not debounce for the first disconnect after resetUsbGadget.
+         */
+        protected boolean mResetUsbGadgetDisableDebounce;
         protected boolean mConnected;
+        protected boolean mConfigured;
         protected long mScreenUnlockedFunctions;
         protected boolean mBootCompleted;
         protected boolean mCurrentFunctionsApplied;
@@ -713,15 +717,29 @@
                 Slog.e(TAG, "unknown state " + state);
                 return;
             }
-            if (configured == 0) removeMessages(MSG_UPDATE_STATE);
             if (connected == 1) removeMessages(MSG_FUNCTION_SWITCH_TIMEOUT);
             Message msg = Message.obtain(this, MSG_UPDATE_STATE);
             msg.arg1 = connected;
             msg.arg2 = configured;
-            // debounce disconnects to avoid problems bringing up USB tethering
-            sendMessageDelayed(msg,
+            if (DEBUG) {
+                Slog.i(TAG, "mResetUsbGadgetDisableDebounce:" + mResetUsbGadgetDisableDebounce
+                       + " connected:" + connected + "configured:" + configured);
+            }
+            if (mResetUsbGadgetDisableDebounce) {
+                // Do not debounce disconnect after resetUsbGadget.
+                sendMessage(msg);
+                if (connected == 1) mResetUsbGadgetDisableDebounce = false;
+            } else {
+                if (configured == 0) {
+                    removeMessages(MSG_UPDATE_STATE);
+                    if (DEBUG) Slog.i(TAG, "removeMessages MSG_UPDATE_STATE");
+                }
+                if (connected == 1) removeMessages(MSG_FUNCTION_SWITCH_TIMEOUT);
+                // debounce disconnects to avoid problems bringing up USB tethering.
+                sendMessageDelayed(msg,
                     (connected == 0) ? (mScreenLocked ? DEVICE_STATE_UPDATE_DELAY
                                                       : DEVICE_STATE_UPDATE_DELAY_EXT) : 0);
+            }
         }
 
         public void updateHostState(UsbPort port, UsbPortStatus status) {
@@ -971,7 +989,10 @@
                     int operationId = sUsbOperationCount.incrementAndGet();
                     mConnected = (msg.arg1 == 1);
                     mConfigured = (msg.arg2 == 1);
-
+                    if (DEBUG) {
+                        Slog.i(TAG, "handleMessage MSG_UPDATE_STATE " + "mConnected:" + mConnected
+                               + " mConfigured:" + mConfigured);
+                    }
                     updateUsbNotification(false);
                     updateAdbNotification(false);
                     if (mBootCompleted) {
@@ -2116,9 +2137,16 @@
                         }
 
                         try {
+                            // MSG_ACCESSORY_MODE_ENTER_TIMEOUT has to be removed to allow exiting
+                            // AOAP mode during resetUsbGadget.
+                            removeMessages(MSG_ACCESSORY_MODE_ENTER_TIMEOUT);
+                            if (mConfigured) {
+                                mResetUsbGadgetDisableDebounce = true;
+                            }
                             mUsbGadgetHal.reset();
                         } catch (Exception e) {
                             Slog.e(TAG, "reset Usb Gadget failed", e);
+                            mResetUsbGadgetDisableDebounce = false;
                         }
                     }
                     break;
diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java
index f389276..b3eb285 100644
--- a/services/usb/java/com/android/server/usb/UsbHostManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHostManager.java
@@ -444,14 +444,19 @@
                         } else {
                             Slog.e(TAG, "Universal Midi Device is null.");
                         }
-                    }
-                    if (parser.containsLegacyMidiDeviceEndpoint()) {
-                        UsbDirectMidiDevice midiDevice = UsbDirectMidiDevice.create(mContext,
-                                newDevice, parser, false, uniqueUsbDeviceIdentifier);
-                        if (midiDevice != null) {
-                            midiDevices.add(midiDevice);
-                        } else {
-                            Slog.e(TAG, "Legacy Midi Device is null.");
+
+                        // Use UsbDirectMidiDevice only if this supports MIDI 2.0 as well.
+                        // ALSA removes the audio sound card if MIDI interfaces are removed.
+                        // This means that as long as ALSA is used for audio, MIDI 1.0 USB
+                        // devices should use the ALSA path for MIDI.
+                        if (parser.containsLegacyMidiDeviceEndpoint()) {
+                            midiDevice = UsbDirectMidiDevice.create(mContext,
+                                    newDevice, parser, false, uniqueUsbDeviceIdentifier);
+                            if (midiDevice != null) {
+                                midiDevices.add(midiDevice);
+                            } else {
+                                Slog.e(TAG, "Legacy Midi Device is null.");
+                            }
                         }
                     }
 
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java
index 3f2d8c8..c6ea228 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java
@@ -79,6 +79,10 @@
         mInterfaceDescriptors.add(interfaceDesc);
     }
 
+    ArrayList<UsbInterfaceDescriptor> getInterfaceDescriptors() {
+        return mInterfaceDescriptors;
+    }
+
     private boolean isAudioInterface(UsbInterfaceDescriptor descriptor) {
         return descriptor.getUsbClass() == UsbDescriptor.CLASSID_AUDIO
                 && descriptor.getUsbSubclass() == UsbDescriptor.AUDIO_AUDIOSTREAMING;
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
index cd6ea68..626ce89 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
@@ -40,6 +40,7 @@
     private UsbDeviceDescriptor mDeviceDescriptor;
     private UsbConfigDescriptor mCurConfigDescriptor;
     private UsbInterfaceDescriptor mCurInterfaceDescriptor;
+    private UsbEndpointDescriptor mCurEndpointDescriptor;
 
     // The AudioClass spec implemented by the AudioClass Interfaces
     // This may well be different than the overall USB Spec.
@@ -165,7 +166,7 @@
                 break;
 
             case UsbDescriptor.DESCRIPTORTYPE_ENDPOINT:
-                descriptor = new UsbEndpointDescriptor(length, type);
+                descriptor = mCurEndpointDescriptor = new UsbEndpointDescriptor(length, type);
                 if (mCurInterfaceDescriptor != null) {
                     mCurInterfaceDescriptor.addEndpointDescriptor(
                             (UsbEndpointDescriptor) descriptor);
@@ -265,6 +266,9 @@
                                     + Integer.toHexString(subClass));
                             break;
                     }
+                    if (mCurEndpointDescriptor != null && descriptor != null) {
+                        mCurEndpointDescriptor.setClassSpecificEndpointDescriptor(descriptor);
+                    }
                 }
                 break;
 
@@ -798,6 +802,84 @@
     /**
      * @hide
      */
+    private int calculateNumLegacyMidiPorts(boolean isOutput) {
+        // Only look at the first config.
+        UsbConfigDescriptor configDescriptor = null;
+        for (UsbDescriptor descriptor : mDescriptors) {
+            if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_CONFIG) {
+                if (descriptor instanceof UsbConfigDescriptor) {
+                    configDescriptor = (UsbConfigDescriptor) descriptor;
+                    break;
+                } else {
+                    Log.w(TAG, "Unrecognized Config l: " + descriptor.getLength()
+                            + " t:0x" + Integer.toHexString(descriptor.getType()));
+                }
+            }
+        }
+        if (configDescriptor == null) {
+            Log.w(TAG, "Config not found");
+            return 0;
+        }
+
+        ArrayList<UsbInterfaceDescriptor> legacyMidiInterfaceDescriptors =
+                new ArrayList<UsbInterfaceDescriptor>();
+        for (UsbInterfaceDescriptor interfaceDescriptor
+                : configDescriptor.getInterfaceDescriptors()) {
+            if (interfaceDescriptor.getUsbClass() == UsbDescriptor.CLASSID_AUDIO) {
+                if (interfaceDescriptor.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) {
+                    UsbDescriptor midiHeaderDescriptor =
+                            interfaceDescriptor.getMidiHeaderInterfaceDescriptor();
+                    if (midiHeaderDescriptor != null) {
+                        if (midiHeaderDescriptor instanceof UsbMSMidiHeader) {
+                            UsbMSMidiHeader midiHeader =
+                                    (UsbMSMidiHeader) midiHeaderDescriptor;
+                            if (midiHeader.getMidiStreamingClass() == MS_MIDI_1_0) {
+                                legacyMidiInterfaceDescriptors.add(interfaceDescriptor);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        int count = 0;
+        for (UsbInterfaceDescriptor interfaceDescriptor : legacyMidiInterfaceDescriptors) {
+            for (int i = 0; i < interfaceDescriptor.getNumEndpoints(); i++) {
+                UsbEndpointDescriptor endpoint =
+                        interfaceDescriptor.getEndpointDescriptor(i);
+                // 0 is output, 1 << 7 is input.
+                if ((endpoint.getDirection() == 0) == isOutput) {
+                    UsbDescriptor classSpecificEndpointDescriptor =
+                            endpoint.getClassSpecificEndpointDescriptor();
+                    if (classSpecificEndpointDescriptor != null
+                            && (classSpecificEndpointDescriptor instanceof UsbACMidi10Endpoint)) {
+                        UsbACMidi10Endpoint midiEndpoint =
+                                (UsbACMidi10Endpoint) classSpecificEndpointDescriptor;
+                        count += midiEndpoint.getNumJacks();
+                    }
+                }
+            }
+        }
+        return count;
+    }
+
+    /**
+     * @hide
+     */
+    public int calculateNumLegacyMidiInputs() {
+        return calculateNumLegacyMidiPorts(false /*isOutput*/);
+    }
+
+    /**
+     * @hide
+     */
+    public int calculateNumLegacyMidiOutputs() {
+        return calculateNumLegacyMidiPorts(true /*isOutput*/);
+    }
+
+    /**
+     * @hide
+     */
     public float getInputHeadsetProbability() {
         if (hasMIDIInterface()) {
             return 0.0f;
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
index ab07ce7..1f448ac 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
@@ -79,6 +79,8 @@
     private byte mRefresh;
     private byte mSyncAddress;
 
+    private UsbDescriptor mClassSpecificEndpointDescriptor;
+
     public UsbEndpointDescriptor(int length, byte type) {
         super(length, type);
         mHierarchyLevel = 4;
@@ -112,6 +114,14 @@
         return mEndpointAddress & UsbEndpointDescriptor.MASK_ENDPOINT_DIRECTION;
     }
 
+    void setClassSpecificEndpointDescriptor(UsbDescriptor descriptor) {
+        mClassSpecificEndpointDescriptor = descriptor;
+    }
+
+    UsbDescriptor getClassSpecificEndpointDescriptor() {
+        return mClassSpecificEndpointDescriptor;
+    }
+
     /**
     * Returns a UsbEndpoint that this UsbEndpointDescriptor is describing.
     */
diff --git a/services/voiceinteraction/TEST_MAPPING b/services/voiceinteraction/TEST_MAPPING
index 22a6445..2ebd4f4 100644
--- a/services/voiceinteraction/TEST_MAPPING
+++ b/services/voiceinteraction/TEST_MAPPING
@@ -1,5 +1,5 @@
 {
-  "presubmit-large": [
+  "presubmit": [
     {
       "name": "CtsVoiceInteractionTestCases",
       "options": [
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index d0a536b..04b08d4 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -55,6 +55,7 @@
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1152,6 +1153,11 @@
                     Slog.w(TAG, "Failed to report onError status: " + e);
                 }
             }
+            // Can improve to log exit reason if needed
+            HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+                    mDetectorType,
+                    HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH,
+                    mVoiceInteractionServiceUid);
         }
 
         @Override
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 983d82b..3d4bcef 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -2043,6 +2043,14 @@
      * {@link #getPhoneAccount}. Self-managed {@link ConnectionService}s must have
      * {@link android.Manifest.permission#MANAGE_OWN_CALLS} to add a new incoming call.
      * <p>
+     * Specify the address associated with the incoming call using
+     * {@link #EXTRA_INCOMING_CALL_ADDRESS}.  If an incoming call is from an anonymous source, omit
+     * this extra and ensure you specify a valid number presentation via
+     * {@link Connection#setAddress(Uri, int)} on the {@link Connection} instance you return in
+     * your
+     * {@link ConnectionService#onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)}
+     * implementation.
+     * <p>
      * The incoming call you are adding is assumed to have a video state of
      * {@link VideoProfile#STATE_AUDIO_ONLY}, unless the extra value
      * {@link #EXTRA_INCOMING_VIDEO_STATE} is specified.
diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
index de70dcb9..5179bab 100644
--- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
+++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
@@ -192,4 +192,22 @@
         // This is the error case. The well-defined value for UNKNOWN is -1.
         return "UNKNOWN(" + state + ")";
     }
+
+    /**
+     * Convert mobile data policy to string.
+     *
+     * @param mobileDataPolicy The mobile data policy.
+     * @return The mobile data policy in string format.
+     */
+    public static @NonNull String mobileDataPolicyToString(
+            @TelephonyManager.MobileDataPolicy int mobileDataPolicy) {
+        switch (mobileDataPolicy) {
+            case TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL:
+                return "DATA_ON_NON_DEFAULT_DURING_VOICE_CALL";
+            case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED:
+                return "MMS_ALWAYS_ALLOWED";
+            default:
+                return "UNKNOWN(" + mobileDataPolicy + ")";
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/NetworkScanRequest.java b/telephony/java/android/telephony/NetworkScanRequest.java
index 326f417..65c2146 100644
--- a/telephony/java/android/telephony/NetworkScanRequest.java
+++ b/telephony/java/android/telephony/NetworkScanRequest.java
@@ -26,7 +26,7 @@
 import java.util.Arrays;
 
 /**
- * Defines a request to peform a network scan.
+ * Defines a request to perform a network scan.
  *
  * This class defines whether the network scan will be performed only once or periodically until
  * cancelled, when the scan is performed periodically, the time interval is not controlled by the
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index cb985bf..e055f63 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -18,9 +18,9 @@
 
 import static android.text.TextUtils.formatSimple;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
-import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -33,10 +33,12 @@
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.Typeface;
-import android.os.Build;
 import android.os.Parcel;
 import android.os.ParcelUuid;
 import android.os.Parcelable;
+import android.telephony.SubscriptionManager.ProfileClass;
+import android.telephony.SubscriptionManager.SimDisplayNameSource;
+import android.telephony.SubscriptionManager.SubscriptionType;
 import android.telephony.SubscriptionManager.UsageSetting;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
@@ -55,7 +57,6 @@
  * A Parcelable class for Subscription Information.
  */
 public class SubscriptionInfo implements Parcelable {
-
     /**
      * Size of text to render on the icon.
      */
@@ -65,162 +66,161 @@
      * Subscription Identifier, this is a device unique number
      * and not an index into an array
      */
-    private int mId;
+    private final int mId;
 
     /**
-     * The GID for a SIM that maybe associated with this subscription, empty if unknown
+     * The ICCID of the SIM that is associated with this subscription, empty if unknown.
      */
-    private String mIccId;
+    @NonNull
+    private final String mIccId;
 
     /**
-     * The index of the slot that currently contains the subscription
-     * and not necessarily unique and maybe INVALID_SLOT_ID if unknown
+     * The index of the SIM slot that currently contains the subscription and not necessarily unique
+     * and maybe {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if unknown or the subscription
+     * is inactive.
      */
-    private int mSimSlotIndex;
+    private final int mSimSlotIndex;
 
     /**
-     * The name displayed to the user that identifies this subscription
+     * The name displayed to the user that identifies this subscription. This name is used
+     * in Settings page and can be renamed by the user.
      */
-    private CharSequence mDisplayName;
+    @NonNull
+    private final CharSequence mDisplayName;
 
     /**
-     * String that identifies SPN/PLMN
-     * TODO : Add a new field that identifies only SPN for a sim
+     * The name displayed to the user that identifies subscription provider name. This name is the
+     * SPN displayed in status bar and many other places. Can't be renamed by the user.
      */
-    private CharSequence mCarrierName;
+    @NonNull
+    private final CharSequence mCarrierName;
 
     /**
-     * The subscription carrier id.
-     * @see TelephonyManager#getSimCarrierId()
+     * The source of the {@link #mDisplayName}.
      */
-    private int mCarrierId;
+    @SimDisplayNameSource
+    private final int mDisplayNameSource;
 
     /**
-     * The source of the name, NAME_SOURCE_DEFAULT_SOURCE, NAME_SOURCE_SIM_SPN,
-     * NAME_SOURCE_SIM_PNN, or NAME_SOURCE_USER_INPUT.
+     * The color to be used for tinting the icon when displaying to the user.
      */
-    private int mNameSource;
+    private final int mIconTint;
 
     /**
-     * The color to be used for tinting the icon when displaying to the user
+     * The number presented to the user identify this subscription.
      */
-    private int mIconTint;
+    @NonNull
+    private final String mNumber;
 
     /**
-     * A number presented to the user identify this subscription
+     * Whether user enables data roaming for this subscription or not. Either
+     * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or
+     * {@link SubscriptionManager#DATA_ROAMING_DISABLE}.
      */
-    private String mNumber;
+    private final int mDataRoaming;
 
     /**
-     * Data roaming state, DATA_ROAMING_ENABLE, DATA_ROAMING_DISABLE
-     */
-    private int mDataRoaming;
-
-    /**
-     * SIM icon bitmap cache
-     */
-    @Nullable private Bitmap mIconBitmap;
-
-    /**
-     * Mobile Country Code
-     */
-    private String mMcc;
-
-    /**
-     * Mobile Network Code
-     */
-    private String mMnc;
-
-    /**
-     * EHPLMNs associated with the subscription
-     */
-    private String[] mEhplmns;
-
-    /**
-     * HPLMNs associated with the subscription
-     */
-    private String[] mHplmns;
-
-    /**
-     * ISO Country code for the subscription's provider
-     */
-    private String mCountryIso;
-
-    /**
-     * Whether the subscription is an embedded one.
-     */
-    private boolean mIsEmbedded;
-
-    /**
-     * The access rules for this subscription, if it is embedded and defines any.
-     * This does not include access rules for non-embedded subscriptions.
+     * Mobile Country Code.
      */
     @Nullable
-    private UiccAccessRule[] mNativeAccessRules;
+    private final String mMcc;
+
+    /**
+     * Mobile Network Code.
+     */
+    @Nullable
+    private final String mMnc;
+
+    /**
+     * EHPLMNs associated with the subscription.
+     */
+    @NonNull
+    private final String[] mEhplmns;
+
+    /**
+     * HPLMNs associated with the subscription.
+     */
+    @NonNull
+    private final String[] mHplmns;
+
+    /**
+     * Whether the subscription is from eSIM.
+     */
+    private final boolean mIsEmbedded;
+
+    /**
+     * The string ID of the SIM card. It is the ICCID of the active profile for a UICC card and the
+     * EID for an eUICC card.
+     */
+    @NonNull
+    private final String mCardString;
+
+    /**
+     * The access rules for this subscription, if it is embedded and defines any. This does not
+     * include access rules for non-embedded subscriptions.
+     */
+    @Nullable
+    private final UiccAccessRule[] mNativeAccessRules;
 
     /**
      * The carrier certificates for this subscription that are saved in carrier configs.
      * This does not include access rules from the Uicc, whether embedded or non-embedded.
      */
     @Nullable
-    private UiccAccessRule[] mCarrierConfigAccessRules;
-
-    /**
-     * The string ID of the SIM card. It is the ICCID of the active profile for a UICC card and the
-     * EID for an eUICC card.
-     */
-    private String mCardString;
-
-    /**
-     * The card ID of the SIM card. This maps uniquely to the card string.
-     */
-    private int mCardId;
+    private final UiccAccessRule[] mCarrierConfigAccessRules;
 
     /**
      * Whether the subscription is opportunistic.
      */
-    private boolean mIsOpportunistic;
+    private final boolean mIsOpportunistic;
 
     /**
-     * A UUID assigned to the subscription group. It returns null if not assigned.
-     * Check {@link SubscriptionManager#createSubscriptionGroup(List)} for more details.
+     * A UUID assigned to the subscription group. {@code null} if not assigned.
+     *
+     * @see SubscriptionManager#createSubscriptionGroup(List)
      */
     @Nullable
-    private ParcelUuid mGroupUUID;
+    private final ParcelUuid mGroupUuid;
 
     /**
-     * A package name that specifies who created the group. Null if mGroupUUID is null.
+     * ISO Country code for the subscription's provider.
      */
-    private String mGroupOwner;
+    @NonNull
+    private final String mCountryIso;
 
     /**
-     * Whether group of the subscription is disabled.
-     * This is only useful if it's a grouped opportunistic subscription. In this case, if all
-     * primary (non-opportunistic) subscriptions in the group are deactivated (unplugged pSIM
-     * or deactivated eSIM profile), we should disable this opportunistic subscription.
+     * The subscription carrier id.
+     *
+     * @see TelephonyManager#getSimCarrierId()
      */
-    private boolean mIsGroupDisabled = false;
+    private final int mCarrierId;
 
     /**
-     * Profile class, PROFILE_CLASS_TESTING, PROFILE_CLASS_OPERATIONAL
-     * PROFILE_CLASS_PROVISIONING, or PROFILE_CLASS_UNSET.
-     * A profile on the eUICC can be defined as test, operational, provisioning, or unset.
-     * The profile class will be populated from the profile metadata if present. Otherwise,
-     * the profile class defaults to unset if there is no profile metadata or the subscription
-     * is not on an eUICC ({@link #isEmbedded} returns false).
+     * The profile class populated from the profile metadata if present. Otherwise,
+     * the profile class defaults to {@link SubscriptionManager#PROFILE_CLASS_UNSET} if there is no
+     * profile metadata or the subscription is not on an eUICC ({@link #isEmbedded} returns
+     * {@code false}).
      */
-    private int mProfileClass;
+    @ProfileClass
+    private final int mProfileClass;
 
     /**
-     * Type of subscription
+     * Type of the subscription.
      */
-    private int mSubscriptionType;
+    @SubscriptionType
+    private final int mType;
+
+    /**
+     * A package name that specifies who created the group. Empty if not available.
+     */
+    @NonNull
+    private final String mGroupOwner;
 
     /**
      * Whether uicc applications are configured to enable or disable.
      * By default it's true.
      */
-    private boolean mAreUiccApplicationsEnabled = true;
+    private final boolean mAreUiccApplicationsEnabled;
 
     /**
      * The port index of the Uicc card.
@@ -230,25 +230,37 @@
     /**
      * Subscription's preferred usage setting.
      */
-    private @UsageSetting int mUsageSetting = SubscriptionManager.USAGE_SETTING_UNKNOWN;
+    @UsageSetting
+    private final int mUsageSetting;
+
+    // Below are the fields that do not exist in the database.
 
     /**
-     * Public copy constructor.
-     * @hide
+     * SIM icon bitmap cache.
      */
-    public SubscriptionInfo(SubscriptionInfo info) {
-        this(info.mId, info.mIccId, info.mSimSlotIndex, info.mDisplayName, info.mCarrierName,
-                info.mNameSource, info.mIconTint, info.mNumber, info.mDataRoaming, info.mIconBitmap,
-                info.mMcc, info.mMnc, info.mCountryIso, info.mIsEmbedded, info.mNativeAccessRules,
-                info.mCardString, info.mCardId, info.mIsOpportunistic,
-                info.mGroupUUID == null ? null : info.mGroupUUID.toString(), info.mIsGroupDisabled,
-                info.mCarrierId, info.mProfileClass, info.mSubscriptionType, info.mGroupOwner,
-                info.mCarrierConfigAccessRules, info.mAreUiccApplicationsEnabled);
-    }
+    @Nullable
+    private Bitmap mIconBitmap;
+
+    /**
+     * The card ID of the SIM card. This maps uniquely to {@link #mCardString}.
+     */
+    private final int mCardId;
+
+    /**
+     * Whether group of the subscription is disabled. This is only useful if it's a grouped
+     * opportunistic subscription. In this case, if all primary (non-opportunistic) subscriptions
+     * in the group are deactivated (unplugged pSIM or deactivated eSIM profile), we should disable
+     * this opportunistic subscription.
+     */
+    private final boolean mIsGroupDisabled;
 
     /**
      * @hide
+     *
+     * @deprecated Use {@link SubscriptionInfo.Builder}.
      */
+    // TODO: Clean up after external usages moved to builder model.
+    @Deprecated
     public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
             CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
             Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
@@ -262,7 +274,11 @@
 
     /**
      * @hide
+     *
+     * @deprecated Use {@link SubscriptionInfo.Builder}.
      */
+    // TODO: Clean up after external usages moved to builder model.
+    @Deprecated
     public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
             CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
             Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
@@ -276,7 +292,11 @@
 
     /**
      * @hide
+     *
+     * @deprecated Use {@link SubscriptionInfo.Builder}.
      */
+    // TODO: Clean up after external usages moved to builder model.
+    @Deprecated
     public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
             CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
             Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
@@ -293,16 +313,20 @@
 
     /**
      * @hide
+     *
+     * @deprecated Use {@link SubscriptionInfo.Builder}.
      */
+    // TODO: Clean up after external usages moved to builder model.
+    @Deprecated
     public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
-            CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
-            Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
+            CharSequence carrierName, int displayNameSource, int iconTint, String number,
+            int roaming, Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
             @Nullable UiccAccessRule[] nativeAccessRules, String cardString, int cardId,
             boolean isOpportunistic, @Nullable String groupUUID, boolean isGroupDisabled,
             int carrierId, int profileClass, int subType, @Nullable String groupOwner,
             @Nullable UiccAccessRule[] carrierConfigAccessRules,
             boolean areUiccApplicationsEnabled, int portIndex) {
-        this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
+        this(id, iccId, simSlotIndex, displayName, carrierName, displayNameSource, iconTint, number,
                 roaming, icon, mcc, mnc, countryIso, isEmbedded, nativeAccessRules, cardString,
                 cardId, isOpportunistic, groupUUID, isGroupDisabled, carrierId, profileClass,
                 subType, groupOwner, carrierConfigAccessRules, areUiccApplicationsEnabled,
@@ -311,49 +335,94 @@
 
     /**
      * @hide
+     *
+     * @deprecated Use {@link SubscriptionInfo.Builder}.
      */
+    // TODO: Clean up after external usages moved to builder model.
+    @Deprecated
     public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
             CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
             Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded,
             @Nullable UiccAccessRule[] nativeAccessRules, String cardString, int cardId,
-            boolean isOpportunistic, @Nullable String groupUUID, boolean isGroupDisabled,
+            boolean isOpportunistic, @Nullable String groupUuid, boolean isGroupDisabled,
             int carrierId, int profileClass, int subType, @Nullable String groupOwner,
             @Nullable UiccAccessRule[] carrierConfigAccessRules,
             boolean areUiccApplicationsEnabled, int portIndex, @UsageSetting int usageSetting) {
         this.mId = id;
         this.mIccId = iccId;
         this.mSimSlotIndex = simSlotIndex;
-        this.mDisplayName = displayName;
+        this.mDisplayName =  displayName;
         this.mCarrierName = carrierName;
-        this.mNameSource = nameSource;
+        this.mDisplayNameSource = nameSource;
         this.mIconTint = iconTint;
         this.mNumber = number;
         this.mDataRoaming = roaming;
         this.mIconBitmap = icon;
-        this.mMcc = mcc;
-        this.mMnc = mnc;
-        this.mCountryIso = countryIso;
+        this.mMcc = TextUtils.emptyIfNull(mcc);
+        this.mMnc = TextUtils.emptyIfNull(mnc);
+        this.mHplmns = null;
+        this.mEhplmns = null;
+        this.mCountryIso = TextUtils.emptyIfNull(countryIso);
         this.mIsEmbedded = isEmbedded;
         this.mNativeAccessRules = nativeAccessRules;
-        this.mCardString = cardString;
+        this.mCardString = TextUtils.emptyIfNull(cardString);
         this.mCardId = cardId;
         this.mIsOpportunistic = isOpportunistic;
-        this.mGroupUUID = groupUUID == null ? null : ParcelUuid.fromString(groupUUID);
+        this.mGroupUuid = groupUuid == null ? null : ParcelUuid.fromString(groupUuid);
         this.mIsGroupDisabled = isGroupDisabled;
         this.mCarrierId = carrierId;
         this.mProfileClass = profileClass;
-        this.mSubscriptionType = subType;
-        this.mGroupOwner = groupOwner;
+        this.mType = subType;
+        this.mGroupOwner = TextUtils.emptyIfNull(groupOwner);
         this.mCarrierConfigAccessRules = carrierConfigAccessRules;
         this.mAreUiccApplicationsEnabled = areUiccApplicationsEnabled;
         this.mPortIndex = portIndex;
         this.mUsageSetting = usageSetting;
     }
+
     /**
-     * @return the subscription ID.
+     * Constructor from builder.
+     *
+     * @param builder Builder of {@link SubscriptionInfo}.
+     */
+    private SubscriptionInfo(@NonNull Builder builder) {
+        this.mId = builder.mId;
+        this.mIccId = builder.mIccId;
+        this.mSimSlotIndex = builder.mSimSlotIndex;
+        this.mDisplayName = builder.mDisplayName;
+        this.mCarrierName = builder.mCarrierName;
+        this.mDisplayNameSource = builder.mDisplayNameSource;
+        this.mIconTint = builder.mIconTint;
+        this.mNumber = builder.mNumber;
+        this.mDataRoaming = builder.mDataRoaming;
+        this.mIconBitmap = builder.mIconBitmap;
+        this.mMcc = builder.mMcc;
+        this.mMnc = builder.mMnc;
+        this.mEhplmns = builder.mEhplmns;
+        this.mHplmns = builder.mHplmns;
+        this.mCountryIso = builder.mCountryIso;
+        this.mIsEmbedded = builder.mIsEmbedded;
+        this.mNativeAccessRules = builder.mNativeAccessRules;
+        this.mCardString = builder.mCardString;
+        this.mCardId = builder.mCardId;
+        this.mIsOpportunistic = builder.mIsOpportunistic;
+        this.mGroupUuid = builder.mGroupUuid;
+        this.mIsGroupDisabled = builder.mIsGroupDisabled;
+        this.mCarrierId = builder.mCarrierId;
+        this.mProfileClass = builder.mProfileClass;
+        this.mType = builder.mType;
+        this.mGroupOwner = builder.mGroupOwner;
+        this.mCarrierConfigAccessRules = builder.mCarrierConfigAccessRules;
+        this.mAreUiccApplicationsEnabled = builder.mAreUiccApplicationsEnabled;
+        this.mPortIndex = builder.mPortIndex;
+        this.mUsageSetting = builder.mUsageSetting;
+    }
+
+    /**
+     * @return The subscription ID.
      */
     public int getSubscriptionId() {
-        return this.mId;
+        return mId;
     }
 
     /**
@@ -370,78 +439,55 @@
      * @return the ICC ID, or an empty string if one of these requirements is not met
      */
     public String getIccId() {
-        return this.mIccId;
+        return mIccId;
     }
 
     /**
-     * @hide
-     */
-    public void clearIccId() {
-        this.mIccId = "";
-    }
-
-    /**
-     * @return the slot index of this Subscription's SIM card.
+     * @return The index of the SIM slot that currently contains the subscription and not
+     * necessarily unique and maybe {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if unknown or
+     * the subscription is inactive.
      */
     public int getSimSlotIndex() {
-        return this.mSimSlotIndex;
+        return mSimSlotIndex;
     }
 
     /**
-     * @return the carrier id of this Subscription carrier.
+     * @return The carrier id of this subscription carrier.
+     *
      * @see TelephonyManager#getSimCarrierId()
      */
     public int getCarrierId() {
-        return this.mCarrierId;
+        return mCarrierId;
     }
 
     /**
-     * @return the name displayed to the user that identifies this subscription
+     * @return The name displayed to the user that identifies this subscription. This name is
+     * used in Settings page and can be renamed by the user.
+     *
+     * @see #getCarrierName()
      */
     public CharSequence getDisplayName() {
-        return this.mDisplayName;
+        return mDisplayName;
     }
 
     /**
-     * Sets the name displayed to the user that identifies this subscription
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public void setDisplayName(CharSequence name) {
-        this.mDisplayName = name;
-    }
-
-    /**
-     * @return the name displayed to the user that identifies Subscription provider name
+     * @return The name displayed to the user that identifies subscription provider name. This name
+     * is the SPN displayed in status bar and many other places. Can't be renamed by the user.
+     *
+     * @see #getDisplayName()
      */
     public CharSequence getCarrierName() {
-        return this.mCarrierName;
+        return mCarrierName;
     }
 
     /**
-     * Sets the name displayed to the user that identifies Subscription provider name
+     * @return The source of the {@link #getDisplayName()}.
+     *
      * @hide
      */
-    public void setCarrierName(CharSequence name) {
-        this.mCarrierName = name;
-    }
-
-    /**
-     * @return the source of the name, eg NAME_SOURCE_DEFAULT_SOURCE, NAME_SOURCE_SIM_SPN or
-     * NAME_SOURCE_USER_INPUT.
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public int getNameSource() {
-        return this.mNameSource;
-    }
-
-    /**
-     * @hide
-     */
-    public void setAssociatedPlmns(String[] ehplmns, String[] hplmns) {
-        mEhplmns = ehplmns;
-        mHplmns = hplmns;
+    @SimDisplayNameSource
+    public int getDisplayNameSource() {
+        return mDisplayNameSource;
     }
 
     /**
@@ -499,15 +545,6 @@
     }
 
     /**
-     * Sets the color displayed to the user that identifies this subscription
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public void setIconTint(int iconTint) {
-        this.mIconTint = iconTint;
-    }
-
-    /**
      * Returns the number of this subscription.
      *
      * Starting with API level 30, returns the number of this subscription if the calling app meets
@@ -533,28 +570,23 @@
     }
 
     /**
-     * @hide
-     */
-    public void clearNumber() {
-        mNumber = "";
-    }
-
-    /**
-     * @return the data roaming state for this subscription, either
-     * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or {@link SubscriptionManager#DATA_ROAMING_DISABLE}.
+     * Whether user enables data roaming for this subscription or not. Either
+     * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or
+     * {@link SubscriptionManager#DATA_ROAMING_DISABLE}.
      */
     public int getDataRoaming() {
-        return this.mDataRoaming;
+        return mDataRoaming;
     }
 
     /**
-     * @return the MCC.
+     * @return The mobile country code.
+     *
      * @deprecated Use {@link #getMccString()} instead.
      */
     @Deprecated
     public int getMcc() {
         try {
-            return this.mMcc == null ? 0 : Integer.valueOf(this.mMcc);
+            return mMcc == null ? 0 : Integer.parseInt(mMcc);
         } catch (NumberFormatException e) {
             Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number");
             return 0;
@@ -562,13 +594,14 @@
     }
 
     /**
-     * @return the MNC.
+     * @return The mobile network code.
+     *
      * @deprecated Use {@link #getMncString()} instead.
      */
     @Deprecated
     public int getMnc() {
         try {
-            return this.mMnc == null ? 0 : Integer.valueOf(this.mMnc);
+            return mMnc == null ? 0 : Integer.parseInt(mMnc);
         } catch (NumberFormatException e) {
             Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number");
             return 0;
@@ -576,36 +609,40 @@
     }
 
     /**
-     * @return The MCC, as a string.
+     * @return The mobile country code.
      */
-    public @Nullable String getMccString() {
-        return this.mMcc;
+    @Nullable
+    public String getMccString() {
+        return mMcc;
     }
 
     /**
-     * @return The MNC, as a string.
+     * @return The mobile network code.
      */
-    public @Nullable String getMncString() {
-        return this.mMnc;
+    @Nullable
+    public String getMncString() {
+        return mMnc;
     }
 
     /**
-     * @return the ISO country code
+     * @return The ISO country code. Empty if not available.
      */
     public String getCountryIso() {
-        return this.mCountryIso;
+        return mCountryIso;
     }
 
-    /** @return whether the subscription is an eUICC one. */
+    /**
+     * @return {@code true} if the subscription is from eSIM.
+     */
     public boolean isEmbedded() {
-        return this.mIsEmbedded;
+        return mIsEmbedded;
     }
 
     /**
      * An opportunistic subscription connects to a network that is
      * limited in functionality and / or coverage.
      *
-     * @return whether subscription is opportunistic.
+     * @return Whether subscription is opportunistic.
      */
     public boolean isOpportunistic() {
         return mIsOpportunistic;
@@ -617,60 +654,66 @@
      * Such that those subscriptions will have some affiliated behaviors such as opportunistic
      * subscription may be invisible to the user.
      *
-     * @return group UUID a String of group UUID if it belongs to a group. Otherwise
-     * it will return null.
+     * @return Group UUID a String of group UUID if it belongs to a group. Otherwise
+     * {@code null}.
      */
-    public @Nullable ParcelUuid getGroupUuid() {
-        return mGroupUUID;
+    @Nullable
+    public ParcelUuid getGroupUuid() {
+        return mGroupUuid;
     }
 
     /**
      * @hide
      */
-    public void clearGroupUuid() {
-        this.mGroupUUID = null;
-    }
-
-    /**
-     * @hide
-     */
+    @NonNull
     public List<String> getEhplmns() {
-        return mEhplmns == null ? Collections.emptyList() : Arrays.asList(mEhplmns);
+        return Collections.unmodifiableList(mEhplmns == null
+                ? Collections.emptyList() : Arrays.asList(mEhplmns));
     }
 
     /**
      * @hide
      */
+    @NonNull
     public List<String> getHplmns() {
-        return mHplmns == null ? Collections.emptyList() : Arrays.asList(mHplmns);
+        return Collections.unmodifiableList(mHplmns == null
+                ? Collections.emptyList() : Arrays.asList(mHplmns));
     }
 
     /**
-     * Return owner package of group the subscription belongs to.
+     * @return The owner package of group the subscription belongs to.
      *
      * @hide
      */
-    public @Nullable String getGroupOwner() {
+    @NonNull
+    public String getGroupOwner() {
         return mGroupOwner;
     }
 
     /**
-     * @return the profile class of this subscription.
+     * @return The profile class populated from the profile metadata if present. Otherwise,
+     * the profile class defaults to {@link SubscriptionManager#PROFILE_CLASS_UNSET} if there is no
+     * profile metadata or the subscription is not on an eUICC ({@link #isEmbedded} return
+     * {@code false}).
+     *
      * @hide
      */
     @SystemApi
-    public @SubscriptionManager.ProfileClass int getProfileClass() {
-        return this.mProfileClass;
+    @ProfileClass
+    public int getProfileClass() {
+        return mProfileClass;
     }
 
     /**
      * This method returns the type of a subscription. It can be
      * {@link SubscriptionManager#SUBSCRIPTION_TYPE_LOCAL_SIM} or
      * {@link SubscriptionManager#SUBSCRIPTION_TYPE_REMOTE_SIM}.
-     * @return the type of subscription
+     *
+     * @return The type of the subscription.
      */
-    public @SubscriptionManager.SubscriptionType int getSubscriptionType() {
-        return mSubscriptionType;
+    @SubscriptionType
+    public int getSubscriptionType() {
+        return mType;
     }
 
     /**
@@ -679,7 +722,7 @@
      * returns true).
      *
      * @param context Context of the application to check.
-     * @return whether the app is authorized to manage this subscription per its metadata.
+     * @return Whether the app is authorized to manage this subscription per its metadata.
      * @hide
      * @deprecated - Do not use.
      */
@@ -700,7 +743,7 @@
      */
     @Deprecated
     public boolean canManageSubscription(Context context, String packageName) {
-        List<UiccAccessRule> allAccessRules = getAllAccessRules();
+        List<UiccAccessRule> allAccessRules = getAccessRules();
         if (allAccessRules == null) {
             return false;
         }
@@ -723,32 +766,22 @@
     }
 
     /**
-     * @return the {@link UiccAccessRule}s that are stored in Uicc, dictating who
-     * is authorized to manage this subscription.
-     * TODO and fix it properly in R / master: either deprecate this and have 3 APIs
-     *  native + carrier + all, or have this return all by default.
+     * @return The {@link UiccAccessRule}s that are stored in Uicc, dictating who is authorized to
+     * manage this subscription.
+     *
      * @hide
      */
     @SystemApi
-    public @Nullable List<UiccAccessRule> getAccessRules() {
-        if (mNativeAccessRules == null) return null;
-        return Arrays.asList(mNativeAccessRules);
-    }
-
-    /**
-     * @return the {@link UiccAccessRule}s that are both stored on Uicc and in carrierConfigs
-     * dictating who is authorized to manage this subscription.
-     * @hide
-     */
-    public @Nullable List<UiccAccessRule> getAllAccessRules() {
+    @Nullable
+    public List<UiccAccessRule> getAccessRules() {
         List<UiccAccessRule> merged = new ArrayList<>();
         if (mNativeAccessRules != null) {
-            merged.addAll(getAccessRules());
+            merged.addAll(Arrays.asList(mNativeAccessRules));
         }
         if (mCarrierConfigAccessRules != null) {
             merged.addAll(Arrays.asList(mCarrierConfigAccessRules));
         }
-        return merged.isEmpty() ? null : merged;
+        return merged.isEmpty() ? null : Collections.unmodifiableList(merged);
     }
 
     /**
@@ -762,50 +795,38 @@
      * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile
      * owner access is deprecated and will be removed in a future release.
      *
-     * @return the card string of the SIM card which contains the subscription or an empty string
+     * @return The card string of the SIM card which contains the subscription or an empty string
      * if these requirements are not met. The card string is the ICCID for UICCs or the EID for
      * eUICCs.
+     *
      * @hide
-     * //TODO rename usages in LPA: UiccSlotUtil.java, UiccSlotsManager.java, UiccSlotInfoTest.java
      */
+    @NonNull
     public String getCardString() {
-        return this.mCardString;
+        return mCardString;
     }
 
     /**
-     * @hide
-     */
-    public void clearCardString() {
-        this.mCardString = "";
-    }
-
-    /**
-     * Returns the card ID of the SIM card which contains the subscription (see
-     * {@link UiccCardInfo#getCardId()}.
-     * @return the cardId
+     * @return The card ID of the SIM card which contains the subscription.
+     *
+     * @see UiccCardInfo#getCardId().
      */
     public int getCardId() {
-        return this.mCardId;
+        return mCardId;
     }
     /**
-     * Returns the port index of the SIM card which contains the subscription.
-     *
-     * @return the portIndex
+     * @return The port index of the SIM card which contains the subscription.
      */
     public int getPortIndex() {
-        return this.mPortIndex;
+        return mPortIndex;
     }
 
     /**
-     * Set whether the subscription's group is disabled.
-     * @hide
-     */
-    public void setGroupDisabled(boolean isGroupDisabled) {
-        this.mIsGroupDisabled = isGroupDisabled;
-    }
-
-    /**
-     * Return whether the subscription's group is disabled.
+     * @return {@code true} if the group of the subscription is disabled. This is only useful if
+     * it's a grouped opportunistic subscription. In this case, if all primary (non-opportunistic)
+     * subscriptions in the group are deactivated (unplugged pSIM or deactivated eSIM profile), we
+     * should disable this opportunistic subscription.
+     *
      * @hide
      */
     @SystemApi
@@ -814,7 +835,7 @@
     }
 
     /**
-     * Return whether uicc applications are set to be enabled or disabled.
+     * @return {@code true} if Uicc applications are set to be enabled or disabled.
      * @hide
      */
     @SystemApi
@@ -825,56 +846,50 @@
     /**
      * Get the usage setting for this subscription.
      *
-     * @return the usage setting used for this subscription.
+     * @return The usage setting used for this subscription.
      */
-    public @UsageSetting int getUsageSetting() {
+    @UsageSetting
+    public int getUsageSetting() {
         return mUsageSetting;
     }
 
-    public static final @android.annotation.NonNull
-            Parcelable.Creator<SubscriptionInfo> CREATOR =
-                    new Parcelable.Creator<SubscriptionInfo>() {
+    @NonNull
+    public static final Parcelable.Creator<SubscriptionInfo> CREATOR =
+            new Parcelable.Creator<SubscriptionInfo>() {
         @Override
         public SubscriptionInfo createFromParcel(Parcel source) {
-            int id = source.readInt();
-            String iccId = source.readString();
-            int simSlotIndex = source.readInt();
-            CharSequence displayName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
-            CharSequence carrierName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
-            int nameSource = source.readInt();
-            int iconTint = source.readInt();
-            String number = source.readString();
-            int dataRoaming = source.readInt();
-            String mcc = source.readString();
-            String mnc = source.readString();
-            String countryIso = source.readString();
-            boolean isEmbedded = source.readBoolean();
-            UiccAccessRule[] nativeAccessRules = source.createTypedArray(UiccAccessRule.CREATOR);
-            String cardString = source.readString();
-            int cardId = source.readInt();
-            int portId = source.readInt();
-            boolean isOpportunistic = source.readBoolean();
-            String groupUUID = source.readString();
-            boolean isGroupDisabled = source.readBoolean();
-            int carrierid = source.readInt();
-            int profileClass = source.readInt();
-            int subType = source.readInt();
-            String[] ehplmns = source.createStringArray();
-            String[] hplmns = source.createStringArray();
-            String groupOwner = source.readString();
-            UiccAccessRule[] carrierConfigAccessRules = source.createTypedArray(
-                UiccAccessRule.CREATOR);
-            boolean areUiccApplicationsEnabled = source.readBoolean();
-            int usageSetting = source.readInt();
-
-            SubscriptionInfo info = new SubscriptionInfo(id, iccId, simSlotIndex, displayName,
-                    carrierName, nameSource, iconTint, number, dataRoaming, /* icon= */ null,
-                    mcc, mnc, countryIso, isEmbedded, nativeAccessRules, cardString, cardId,
-                    isOpportunistic, groupUUID, isGroupDisabled, carrierid, profileClass, subType,
-                    groupOwner, carrierConfigAccessRules, areUiccApplicationsEnabled,
-                    portId, usageSetting);
-            info.setAssociatedPlmns(ehplmns, hplmns);
-            return info;
+            return new Builder()
+                    .setId(source.readInt())
+                    .setIccId(source.readString())
+                    .setSimSlotIndex(source.readInt())
+                    .setDisplayName(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source))
+                    .setCarrierName(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source))
+                    .setDisplayNameSource(source.readInt())
+                    .setIconTint(source.readInt())
+                    .setNumber(source.readString())
+                    .setDataRoaming(source.readInt())
+                    .setMcc(source.readString())
+                    .setMnc(source.readString())
+                    .setCountryIso(source.readString())
+                    .setEmbedded(source.readBoolean())
+                    .setNativeAccessRules(source.createTypedArray(UiccAccessRule.CREATOR))
+                    .setCardString(source.readString())
+                    .setCardId(source.readInt())
+                    .setPortIndex(source.readInt())
+                    .setOpportunistic(source.readBoolean())
+                    .setGroupUuid(source.readString8())
+                    .setGroupDisabled(source.readBoolean())
+                    .setCarrierId(source.readInt())
+                    .setProfileClass(source.readInt())
+                    .setType(source.readInt())
+                    .setEhplmns(source.createStringArray())
+                    .setHplmns(source.createStringArray())
+                    .setGroupOwner(source.readString())
+                    .setCarrierConfigAccessRules(source.createTypedArray(
+                            UiccAccessRule.CREATOR))
+                    .setUiccApplicationsEnabled(source.readBoolean())
+                    .setUsageSetting(source.readInt())
+                    .build();
         }
 
         @Override
@@ -890,7 +905,7 @@
         dest.writeInt(mSimSlotIndex);
         TextUtils.writeToParcel(mDisplayName, dest, 0);
         TextUtils.writeToParcel(mCarrierName, dest, 0);
-        dest.writeInt(mNameSource);
+        dest.writeInt(mDisplayNameSource);
         dest.writeInt(mIconTint);
         dest.writeString(mNumber);
         dest.writeInt(mDataRoaming);
@@ -904,11 +919,11 @@
         dest.writeInt(mCardId);
         dest.writeInt(mPortIndex);
         dest.writeBoolean(mIsOpportunistic);
-        dest.writeString(mGroupUUID == null ? null : mGroupUUID.toString());
+        dest.writeString8(mGroupUuid == null ? null : mGroupUuid.toString());
         dest.writeBoolean(mIsGroupDisabled);
         dest.writeInt(mCarrierId);
         dest.writeInt(mProfileClass);
-        dest.writeInt(mSubscriptionType);
+        dest.writeInt(mType);
         dest.writeStringArray(mEhplmns);
         dest.writeStringArray(mHplmns);
         dest.writeString(mGroupOwner);
@@ -923,6 +938,11 @@
     }
 
     /**
+     * Get ICCID stripped PII information on user build.
+     *
+     * @param iccId The original ICCID.
+     * @return The stripped string.
+     *
      * @hide
      */
     public static String givePrintableIccid(String iccId) {
@@ -941,75 +961,701 @@
     public String toString() {
         String iccIdToPrint = givePrintableIccid(mIccId);
         String cardStringToPrint = givePrintableIccid(mCardString);
-        return "{id=" + mId + " iccId=" + iccIdToPrint + " simSlotIndex=" + mSimSlotIndex
-                + " carrierId=" + mCarrierId + " displayName=" + mDisplayName
-                + " carrierName=" + mCarrierName + " nameSource=" + mNameSource
+        return "[SubscriptionInfo: id=" + mId
+                + " iccId=" + iccIdToPrint
+                + " simSlotIndex=" + mSimSlotIndex
+                + " portIndex=" + mPortIndex
+                + " isEmbedded=" + mIsEmbedded
+                + " carrierId=" + mCarrierId
+                + " displayName=" + mDisplayName
+                + " carrierName=" + mCarrierName
+                + " isOpportunistic=" + mIsOpportunistic
+                + " groupUuid=" + mGroupUuid
+                + " groupOwner=" + mGroupOwner
+                + " isGroupDisabled=" + mIsGroupDisabled
+                + " displayNameSource="
+                + SubscriptionManager.displayNameSourceToString(mDisplayNameSource)
                 + " iconTint=" + mIconTint
                 + " number=" + Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mNumber)
-                + " dataRoaming=" + mDataRoaming + " iconBitmap=" + mIconBitmap + " mcc=" + mMcc
-                + " mnc=" + mMnc + " countryIso=" + mCountryIso + " isEmbedded=" + mIsEmbedded
-                + " nativeAccessRules=" + Arrays.toString(mNativeAccessRules)
-                + " cardString=" + cardStringToPrint + " cardId=" + mCardId
-                + " portIndex=" + mPortIndex
-                + " isOpportunistic=" + mIsOpportunistic + " groupUUID=" + mGroupUUID
-                + " isGroupDisabled=" + mIsGroupDisabled
-                + " profileClass=" + mProfileClass
+                + " dataRoaming=" + mDataRoaming
+                + " mcc=" + mMcc
+                + " mnc=" + mMnc
                 + " ehplmns=" + Arrays.toString(mEhplmns)
                 + " hplmns=" + Arrays.toString(mHplmns)
-                + " subscriptionType=" + mSubscriptionType
-                + " groupOwner=" + mGroupOwner
+                + " cardString=" + cardStringToPrint
+                + " cardId=" + mCardId
+                + " nativeAccessRules=" + Arrays.toString(mNativeAccessRules)
                 + " carrierConfigAccessRules=" + Arrays.toString(mCarrierConfigAccessRules)
+                + " countryIso=" + mCountryIso
+                + " profileClass=" + mProfileClass
+                + " mType=" + SubscriptionManager.subscriptionTypeToString(mType)
                 + " areUiccApplicationsEnabled=" + mAreUiccApplicationsEnabled
-                + " usageSetting=" + mUsageSetting + "}";
+                + " usageSetting=" + SubscriptionManager.usageSettingToString(mUsageSetting)
+                + "]";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SubscriptionInfo that = (SubscriptionInfo) o;
+        return mId == that.mId && mSimSlotIndex == that.mSimSlotIndex
+                && mDisplayNameSource == that.mDisplayNameSource && mIconTint == that.mIconTint
+                && mDataRoaming == that.mDataRoaming && mIsEmbedded == that.mIsEmbedded
+                && mIsOpportunistic == that.mIsOpportunistic && mCarrierId == that.mCarrierId
+                && mProfileClass == that.mProfileClass && mType == that.mType
+                && mAreUiccApplicationsEnabled == that.mAreUiccApplicationsEnabled
+                && mPortIndex == that.mPortIndex && mUsageSetting == that.mUsageSetting
+                && mCardId == that.mCardId && mIsGroupDisabled == that.mIsGroupDisabled
+                && mIccId.equals(that.mIccId) && mDisplayName.equals(that.mDisplayName)
+                && mCarrierName.equals(that.mCarrierName) && mNumber.equals(that.mNumber)
+                && Objects.equals(mMcc, that.mMcc) && Objects.equals(mMnc,
+                that.mMnc) && Arrays.equals(mEhplmns, that.mEhplmns)
+                && Arrays.equals(mHplmns, that.mHplmns) && mCardString.equals(
+                that.mCardString) && Arrays.equals(mNativeAccessRules,
+                that.mNativeAccessRules) && Arrays.equals(mCarrierConfigAccessRules,
+                that.mCarrierConfigAccessRules) && Objects.equals(mGroupUuid, that.mGroupUuid)
+                && mCountryIso.equals(that.mCountryIso) && mGroupOwner.equals(that.mGroupOwner);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mId, mSimSlotIndex, mNameSource, mIconTint, mDataRoaming, mIsEmbedded,
-                mIsOpportunistic, mGroupUUID, mIccId, mNumber, mMcc, mMnc, mCountryIso, mCardString,
-                mCardId, mDisplayName, mCarrierName, Arrays.hashCode(mNativeAccessRules),
-                mIsGroupDisabled, mCarrierId, mProfileClass, mGroupOwner,
-                mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting);
+        int result = Objects.hash(mId, mIccId, mSimSlotIndex, mDisplayName, mCarrierName,
+                mDisplayNameSource, mIconTint, mNumber, mDataRoaming, mMcc, mMnc, mIsEmbedded,
+                mCardString, mIsOpportunistic, mGroupUuid, mCountryIso, mCarrierId, mProfileClass,
+                mType, mGroupOwner, mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting, mCardId,
+                mIsGroupDisabled);
+        result = 31 * result + Arrays.hashCode(mEhplmns);
+        result = 31 * result + Arrays.hashCode(mHplmns);
+        result = 31 * result + Arrays.hashCode(mNativeAccessRules);
+        result = 31 * result + Arrays.hashCode(mCarrierConfigAccessRules);
+        return result;
     }
 
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == null) return false;
-        if (obj == this) return true;
+    /**
+     * The builder class of {@link SubscriptionInfo}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        /**
+         * The subscription id.
+         */
+        private int mId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
-        SubscriptionInfo toCompare;
-        try {
-            toCompare = (SubscriptionInfo) obj;
-        } catch (ClassCastException ex) {
-            return false;
+        /**
+         * The ICCID of the SIM that is associated with this subscription, empty if unknown.
+         */
+        @NonNull
+        private String mIccId = "";
+
+        /**
+         * The index of the SIM slot that currently contains the subscription and not necessarily
+         * unique and maybe {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if unknown or the
+         * subscription is inactive.
+         */
+        private int mSimSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+
+        /**
+         * The name displayed to the user that identifies this subscription. This name is used
+         * in Settings page and can be renamed by the user.
+         */
+        @NonNull
+        private CharSequence mDisplayName = "";
+
+        /**
+         * The name displayed to the user that identifies subscription provider name. This name
+         * is the SPN displayed in status bar and many other places. Can't be renamed by the user.
+         */
+        @NonNull
+        private CharSequence mCarrierName = "";
+
+        /**
+         * The source of the display name.
+         */
+        @SimDisplayNameSource
+        private int mDisplayNameSource = SubscriptionManager.NAME_SOURCE_UNKNOWN;
+
+        /**
+         * The color to be used for tinting the icon when displaying to the user.
+         */
+        private int mIconTint = 0;
+
+        /**
+         * The number presented to the user identify this subscription.
+         */
+        @NonNull
+        private String mNumber = "";
+
+        /**
+         * Whether user enables data roaming for this subscription or not. Either
+         * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or
+         * {@link SubscriptionManager#DATA_ROAMING_DISABLE}.
+         */
+        private int mDataRoaming = SubscriptionManager.DATA_ROAMING_DISABLE;
+
+        /**
+         * SIM icon bitmap cache.
+         */
+        @Nullable
+        private Bitmap mIconBitmap = null;
+
+        /**
+         * The mobile country code.
+         */
+        @Nullable
+        private String mMcc = null;
+
+        /**
+         * The mobile network code.
+         */
+        @Nullable
+        private String mMnc = null;
+
+        /**
+         * EHPLMNs associated with the subscription.
+         */
+        @NonNull
+        private String[] mEhplmns = new String[0];
+
+        /**
+         * HPLMNs associated with the subscription.
+         */
+        @NonNull
+        private String[] mHplmns = new String[0];
+
+        /**
+         * The ISO Country code for the subscription's provider.
+         */
+        @NonNull
+        private String mCountryIso = "";
+
+        /**
+         * Whether the subscription is from eSIM.
+         */
+        private boolean mIsEmbedded = false;
+
+        /**
+         * The native access rules for this subscription, if it is embedded and defines any. This
+         * does not include access rules for non-embedded subscriptions.
+         */
+        @Nullable
+        private UiccAccessRule[] mNativeAccessRules = null;
+
+        /**
+         * The card string of the SIM card.
+         */
+        @NonNull
+        private String mCardString = "";
+
+        /**
+         * The card ID of the SIM card which contains the subscription.
+         */
+        private int mCardId = TelephonyManager.UNINITIALIZED_CARD_ID;
+
+        /**
+         * Whether the subscription is opportunistic or not.
+         */
+        private boolean mIsOpportunistic = false;
+
+        /**
+         * The group UUID of the subscription group.
+         */
+        @Nullable
+        private ParcelUuid mGroupUuid = null;
+
+        /**
+         * Whether group of the subscription is disabled. This is only useful if it's a grouped
+         * opportunistic subscription. In this case, if all primary (non-opportunistic)
+         * subscriptions in the group are deactivated (unplugged pSIM or deactivated eSIM profile),
+         * we should disable this opportunistic subscription.
+         */
+        private boolean mIsGroupDisabled = false;
+
+        /**
+         * The carrier id.
+         *
+         * @see TelephonyManager#getSimCarrierId()
+         */
+        private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+
+        /**
+         * The profile class populated from the profile metadata if present. Otherwise, the profile
+         * class defaults to {@link SubscriptionManager#PROFILE_CLASS_UNSET} if there is no profile
+         * metadata or the subscription is not on an eUICC ({@link #isEmbedded} returns
+         * {@code false}).
+         */
+        @ProfileClass
+        private int mProfileClass = SubscriptionManager.PROFILE_CLASS_UNSET;
+
+        /**
+         * The subscription type.
+         */
+        @SubscriptionType
+        private int mType = SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM;
+
+        /**
+         * The owner package of group the subscription belongs to.
+         */
+        @NonNull
+        private String mGroupOwner = "";
+
+        /**
+         * The carrier certificates for this subscription that are saved in carrier configs.
+         * This does not include access rules from the Uicc, whether embedded or non-embedded.
+         */
+        @Nullable
+        private UiccAccessRule[] mCarrierConfigAccessRules = null;
+
+        /**
+         * Whether Uicc applications are configured to enable or not.
+         */
+        private boolean mAreUiccApplicationsEnabled = true;
+
+        /**
+         * the port index of the Uicc card.
+         */
+        private int mPortIndex = TelephonyManager.INVALID_PORT_INDEX;
+
+        /**
+         * Subscription's preferred usage setting.
+         */
+        @UsageSetting
+        private int mUsageSetting = SubscriptionManager.USAGE_SETTING_UNKNOWN;
+
+        /**
+         * Default constructor.
+         */
+        public Builder() {
         }
 
-        return mId == toCompare.mId
-                && mSimSlotIndex == toCompare.mSimSlotIndex
-                && mNameSource == toCompare.mNameSource
-                && mIconTint == toCompare.mIconTint
-                && mDataRoaming == toCompare.mDataRoaming
-                && mIsEmbedded == toCompare.mIsEmbedded
-                && mIsOpportunistic == toCompare.mIsOpportunistic
-                && mIsGroupDisabled == toCompare.mIsGroupDisabled
-                && mAreUiccApplicationsEnabled == toCompare.mAreUiccApplicationsEnabled
-                && mCarrierId == toCompare.mCarrierId
-                && Objects.equals(mGroupUUID, toCompare.mGroupUUID)
-                && Objects.equals(mIccId, toCompare.mIccId)
-                && Objects.equals(mNumber, toCompare.mNumber)
-                && Objects.equals(mMcc, toCompare.mMcc)
-                && Objects.equals(mMnc, toCompare.mMnc)
-                && Objects.equals(mCountryIso, toCompare.mCountryIso)
-                && Objects.equals(mCardString, toCompare.mCardString)
-                && Objects.equals(mCardId, toCompare.mCardId)
-                && mPortIndex == toCompare.mPortIndex
-                && Objects.equals(mGroupOwner, toCompare.mGroupOwner)
-                && TextUtils.equals(mDisplayName, toCompare.mDisplayName)
-                && TextUtils.equals(mCarrierName, toCompare.mCarrierName)
-                && Arrays.equals(mNativeAccessRules, toCompare.mNativeAccessRules)
-                && mProfileClass == toCompare.mProfileClass
-                && Arrays.equals(mEhplmns, toCompare.mEhplmns)
-                && Arrays.equals(mHplmns, toCompare.mHplmns)
-                && mUsageSetting == toCompare.mUsageSetting;
+        /**
+         * Constructor from {@link SubscriptionInfo}.
+         *
+         * @param info The subscription info.
+         */
+        public Builder(@NonNull SubscriptionInfo info) {
+            mId = info.mId;
+            mIccId = info.mIccId;
+            mSimSlotIndex = info.mSimSlotIndex;
+            mDisplayName = info.mDisplayName;
+            mCarrierName = info.mCarrierName;
+            mDisplayNameSource = info.mDisplayNameSource;
+            mIconTint = info.mIconTint;
+            mNumber = info.mNumber;
+            mDataRoaming = info.mDataRoaming;
+            mIconBitmap = info.mIconBitmap;
+            mMcc = info.mMcc;
+            mMnc = info.mMnc;
+            mEhplmns = info.mEhplmns;
+            mHplmns = info.mHplmns;
+            mCountryIso = info.mCountryIso;
+            mIsEmbedded = info.mIsEmbedded;
+            mNativeAccessRules = info.mNativeAccessRules;
+            mCardString = info.mCardString;
+            mCardId = info.mCardId;
+            mIsOpportunistic = info.mIsOpportunistic;
+            mGroupUuid = info.mGroupUuid;
+            mIsGroupDisabled = info.mIsGroupDisabled;
+            mCarrierId = info.mCarrierId;
+            mProfileClass = info.mProfileClass;
+            mType = info.mType;
+            mGroupOwner = info.mGroupOwner;
+            mCarrierConfigAccessRules = info.mCarrierConfigAccessRules;
+            mAreUiccApplicationsEnabled = info.mAreUiccApplicationsEnabled;
+            mPortIndex = info.mPortIndex;
+            mUsageSetting = info.mUsageSetting;
+        }
+
+        /**
+         * Set the subscription id.
+         *
+         * @param id The subscription id.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setId(int id) {
+            mId = id;
+            return this;
+        }
+
+        /**
+         * Set the ICCID of the SIM that is associated with this subscription.
+         *
+         * @param iccId The ICCID of the SIM that is associated with this subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setIccId(@Nullable String iccId) {
+            mIccId = TextUtils.emptyIfNull(iccId);
+            return this;
+        }
+
+        /**
+         * Set the SIM index of the slot that currently contains the subscription. Set to
+         * {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if the subscription is inactive.
+         *
+         * @param simSlotIndex The SIM slot index.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setSimSlotIndex(int simSlotIndex) {
+            mSimSlotIndex = simSlotIndex;
+            return this;
+        }
+
+        /**
+         * The name displayed to the user that identifies this subscription. This name is used
+         * in Settings page and can be renamed by the user.
+         *
+         * @param displayName The display name.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setDisplayName(@Nullable CharSequence displayName) {
+            mDisplayName = displayName == null ? "" : displayName;
+            return this;
+        }
+
+        /**
+         * The name displayed to the user that identifies subscription provider name. This name
+         * is the SPN displayed in status bar and many other places. Can't be renamed by the user.
+         *
+         * @param carrierName The carrier name.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setCarrierName(@Nullable CharSequence carrierName) {
+            mCarrierName = carrierName == null ? "" : carrierName;
+            return this;
+        }
+
+        /**
+         * Set the source of the display name.
+         *
+         * @param displayNameSource The source of the display name.
+         * @return The builder.
+         *
+         * @see SubscriptionInfo#getDisplayName()
+         */
+        @NonNull
+        public Builder setDisplayNameSource(@SimDisplayNameSource int displayNameSource) {
+            mDisplayNameSource = displayNameSource;
+            return this;
+        }
+
+        /**
+         * Set the color to be used for tinting the icon when displaying to the user.
+         *
+         * @param iconTint The color to be used for tinting the icon when displaying to the user.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setIconTint(int iconTint) {
+            mIconTint = iconTint;
+            return this;
+        }
+
+        /**
+         * Set the number presented to the user identify this subscription.
+         *
+         * @param number the number presented to the user identify this subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setNumber(@Nullable String number) {
+            mNumber = TextUtils.emptyIfNull(number);
+            return this;
+        }
+
+        /**
+         * Set whether user enables data roaming for this subscription or not.
+         *
+         * @param dataRoaming Data roaming mode. Either
+         * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or
+         * {@link SubscriptionManager#DATA_ROAMING_DISABLE}
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setDataRoaming(int dataRoaming) {
+            mDataRoaming = dataRoaming;
+            return this;
+        }
+
+        /**
+         * Set SIM icon bitmap cache.
+         *
+         * @param iconBitmap SIM icon bitmap cache.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setIcon(@Nullable Bitmap iconBitmap) {
+            mIconBitmap = iconBitmap;
+            return this;
+        }
+
+        /**
+         * Set the mobile country code.
+         *
+         * @param mcc The mobile country code.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setMcc(@Nullable String mcc) {
+            mMcc = mcc;
+            return this;
+        }
+
+        /**
+         * Set the mobile network code.
+         *
+         * @param mnc Mobile network code.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setMnc(@Nullable String mnc) {
+            mMnc = mnc;
+            return this;
+        }
+
+        /**
+         * Set EHPLMNs associated with the subscription.
+         *
+         * @param ehplmns EHPLMNs associated with the subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setEhplmns(@Nullable String[] ehplmns) {
+            mEhplmns = ehplmns == null ? new String[0] : ehplmns;
+            return this;
+        }
+
+        /**
+         * Set HPLMNs associated with the subscription.
+         *
+         * @param hplmns HPLMNs associated with the subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setHplmns(@Nullable String[] hplmns) {
+            mHplmns = hplmns == null ? new String[0] : hplmns;
+            return this;
+        }
+
+        /**
+         * Set the ISO country code for the subscription's provider.
+         *
+         * @param countryIso The ISO country code for the subscription's provider.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setCountryIso(@Nullable String countryIso) {
+            mCountryIso = TextUtils.emptyIfNull(countryIso);
+            return this;
+        }
+
+        /**
+         * Set whether the subscription is from eSIM or not.
+         *
+         * @param isEmbedded {@code true} if the subscription is from eSIM.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setEmbedded(boolean isEmbedded) {
+            mIsEmbedded = isEmbedded;
+            return this;
+        }
+
+        /**
+         * Set the native access rules for this subscription, if it is embedded and defines any.
+         * This does not include access rules for non-embedded subscriptions.
+         *
+         * @param nativeAccessRules The native access rules for this subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setNativeAccessRules(@Nullable UiccAccessRule[] nativeAccessRules) {
+            mNativeAccessRules = nativeAccessRules;
+            return this;
+        }
+
+        /**
+         * Set the card string of the SIM card.
+         *
+         * @param cardString The card string of the SIM card.
+         * @return The builder.
+         *
+         * @see #getCardString()
+         */
+        @NonNull
+        public Builder setCardString(@Nullable String cardString) {
+            mCardString = TextUtils.emptyIfNull(cardString);
+            return this;
+        }
+
+        /**
+         * Set the card ID of the SIM card which contains the subscription.
+         *
+         * @param cardId The card ID of the SIM card which contains the subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setCardId(int cardId) {
+            mCardId = cardId;
+            return this;
+        }
+
+        /**
+         * Set whether the subscription is opportunistic or not.
+         *
+         * @param isOpportunistic {@code true} if the subscription is opportunistic.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setOpportunistic(boolean isOpportunistic) {
+            mIsOpportunistic = isOpportunistic;
+            return this;
+        }
+
+        /**
+         * Set the group UUID of the subscription group.
+         *
+         * @param groupUuid The group UUID.
+         * @return The builder.
+         *
+         * @see #getGroupUuid()
+         */
+        @NonNull
+        public Builder setGroupUuid(@Nullable String groupUuid) {
+            mGroupUuid = groupUuid == null ? null : ParcelUuid.fromString(groupUuid);
+            return this;
+        }
+
+        /**
+         * Whether group of the subscription is disabled. This is only useful if it's a grouped
+         * opportunistic subscription. In this case, if all primary (non-opportunistic)
+         * subscriptions in the group are deactivated (unplugged pSIM or deactivated eSIM profile),
+         * we should disable this opportunistic subscription.
+         *
+         * @param isGroupDisabled {@code true} if group of the subscription is disabled.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setGroupDisabled(boolean isGroupDisabled) {
+            mIsGroupDisabled = isGroupDisabled;
+            return this;
+        }
+
+        /**
+         * Set the subscription carrier id.
+         *
+         * @param carrierId The carrier id.
+         * @return The builder
+         *
+         * @see TelephonyManager#getSimCarrierId()
+         */
+        @NonNull
+        public Builder setCarrierId(int carrierId) {
+            mCarrierId = carrierId;
+            return this;
+        }
+
+        /**
+         * Set the profile class populated from the profile metadata if present.
+         *
+         * @param profileClass the profile class populated from the profile metadata if present.
+         * @return The builder
+         *
+         * @see #getProfileClass()
+         */
+        @NonNull
+        public Builder setProfileClass(@ProfileClass int profileClass) {
+            mProfileClass = profileClass;
+            return this;
+        }
+
+        /**
+         * Set the subscription type.
+         *
+         * @param type Subscription type.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setType(@SubscriptionType int type) {
+            mType = type;
+            return this;
+        }
+
+        /**
+         * Set the owner package of group the subscription belongs to.
+         *
+         * @param groupOwner Owner package of group the subscription belongs to.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setGroupOwner(@Nullable String groupOwner) {
+            mGroupOwner = TextUtils.emptyIfNull(groupOwner);
+            return this;
+        }
+
+        /**
+         * Set the carrier certificates for this subscription that are saved in carrier configs.
+         * This does not include access rules from the Uicc, whether embedded or non-embedded.
+         *
+         * @param carrierConfigAccessRules The carrier certificates for this subscription.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setCarrierConfigAccessRules(
+                @Nullable UiccAccessRule[] carrierConfigAccessRules) {
+            mCarrierConfigAccessRules = carrierConfigAccessRules;
+            return this;
+        }
+
+        /**
+         * Set whether Uicc applications are configured to enable or not.
+         *
+         * @param uiccApplicationsEnabled {@code true} if Uicc applications are configured to
+         * enable.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setUiccApplicationsEnabled(boolean uiccApplicationsEnabled) {
+            mAreUiccApplicationsEnabled = uiccApplicationsEnabled;
+            return this;
+        }
+
+        /**
+         * Set the port index of the Uicc card.
+         *
+         * @param portIndex The port index of the Uicc card.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setPortIndex(int portIndex) {
+            mPortIndex = portIndex;
+            return this;
+        }
+
+        /**
+         * Set subscription's preferred usage setting.
+         *
+         * @param usageSetting Subscription's preferred usage setting.
+         * @return The builder.
+         */
+        @NonNull
+        public Builder setUsageSetting(@UsageSetting int usageSetting) {
+            mUsageSetting = usageSetting;
+            return this;
+        }
+
+        /**
+         * Build the {@link SubscriptionInfo}.
+         *
+         * @return The {@link SubscriptionInfo} instance.
+         */
+        public SubscriptionInfo build() {
+            return new SubscriptionInfo(this);
+        }
     }
 }
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index dc8286f..abb3cb3 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -566,6 +566,12 @@
     public static final String NAME_SOURCE = SimInfo.COLUMN_NAME_SOURCE;
 
     /**
+     * The name_source is unknown. (for initialization)
+     * @hide
+     */
+    public static final int NAME_SOURCE_UNKNOWN = SimInfo.NAME_SOURCE_UNKNOWN;
+
+    /**
      * The name_source is from the carrier id.
      * @hide
      */
@@ -600,6 +606,7 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"NAME_SOURCE_"},
             value = {
+                    NAME_SOURCE_UNKNOWN,
                     NAME_SOURCE_CARRIER_ID,
                     NAME_SOURCE_SIM_SPN,
                     NAME_SOURCE_USER_INPUT,
@@ -3079,7 +3086,7 @@
     @SystemApi
     public boolean canManageSubscription(@NonNull SubscriptionInfo info,
             @NonNull String packageName) {
-        if (info == null || info.getAllAccessRules() == null || packageName == null) {
+        if (info == null || info.getAccessRules() == null || packageName == null) {
             return false;
         }
         PackageManager packageManager = mContext.getPackageManager();
@@ -3091,7 +3098,7 @@
             logd("Unknown package: " + packageName);
             return false;
         }
-        for (UiccAccessRule rule : info.getAllAccessRules()) {
+        for (UiccAccessRule rule : info.getAccessRules()) {
             if (rule.getCarrierPrivilegeStatus(packageInfo)
                     == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
                 return true;
@@ -4133,5 +4140,66 @@
                 (iSub)-> iSub.setUsageSetting(
                         usageSetting, subscriptionId, mContext.getOpPackageName()));
     }
+
+    /**
+     * Convert display name source to string.
+     *
+     * @param source The display name source.
+     * @return The display name source in string format.
+     *
+     * @hide
+     */
+    @NonNull
+    public static String displayNameSourceToString(
+            @SubscriptionManager.SimDisplayNameSource int source) {
+        switch (source) {
+            case SubscriptionManager.NAME_SOURCE_UNKNOWN: return "UNKNOWN";
+            case SubscriptionManager.NAME_SOURCE_CARRIER_ID: return "CARRIER_ID";
+            case SubscriptionManager.NAME_SOURCE_SIM_SPN: return "SIM_SPN";
+            case SubscriptionManager.NAME_SOURCE_USER_INPUT: return "USER_INPUT";
+            case SubscriptionManager.NAME_SOURCE_CARRIER: return "CARRIER";
+            case SubscriptionManager.NAME_SOURCE_SIM_PNN: return "SIM_PNN";
+            default:
+                return "UNKNOWN(" + source + ")";
+        }
+    }
+
+    /**
+     * Convert subscription type to string.
+     *
+     * @param type The subscription type.
+     * @return The subscription type in string format.
+     *
+     * @hide
+     */
+    @NonNull
+    public static String subscriptionTypeToString(@SubscriptionManager.SubscriptionType int type) {
+        switch (type) {
+            case SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM: return "LOCAL_SIM";
+            case SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM: return "REMOTE_SIM";
+            default:
+                return "UNKNOWN(" + type + ")";
+        }
+    }
+
+    /**
+     * Convert usage setting to string.
+     *
+     * @param usageSetting Usage setting.
+     * @return The usage setting in string format.
+     *
+     * @hide
+     */
+    @NonNull
+    public static String usageSettingToString(@SubscriptionManager.UsageSetting int usageSetting) {
+        switch (usageSetting) {
+            case SubscriptionManager.USAGE_SETTING_UNKNOWN: return "UNKNOWN";
+            case SubscriptionManager.USAGE_SETTING_DEFAULT: return "DEFAULT";
+            case SubscriptionManager.USAGE_SETTING_VOICE_CENTRIC: return "VOICE_CENTRIC";
+            case SubscriptionManager.USAGE_SETTING_DATA_CENTRIC: return "DATA_CENTRIC";
+            default:
+                return "UNKNOWN(" + usageSetting + ")";
+        }
+    }
 }
 
diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java
index a6ccb22..883824f 100644
--- a/telephony/java/android/telephony/ims/ImsMmTelManager.java
+++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java
@@ -72,6 +72,7 @@
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "WIFI_MODE_", value = {
+            WIFI_MODE_UNKNOWN,
             WIFI_MODE_WIFI_ONLY,
             WIFI_MODE_CELLULAR_PREFERRED,
             WIFI_MODE_WIFI_PREFERRED
@@ -79,6 +80,12 @@
     public @interface WiFiCallingMode {}
 
     /**
+     * Wifi calling mode is unknown. This is for initialization only.
+     * @hide
+     */
+    public static final int WIFI_MODE_UNKNOWN = -1;
+
+    /**
      * Register for IMS over IWLAN if WiFi signal quality is high enough. Do not hand over to LTE
      * registration if signal quality degrades.
      */
@@ -1581,4 +1588,24 @@
                         .get());
         return binder;
     }
+
+    /**
+     * Convert Wi-Fi calling mode to string.
+     *
+     * @param mode Wi-Fi calling mode.
+     * @return The Wi-Fi calling mode in string format.
+     *
+     * @hide
+     */
+    @NonNull
+    public static String wifiCallingModeToString(@ImsMmTelManager.WiFiCallingMode int mode) {
+        switch (mode) {
+            case ImsMmTelManager.WIFI_MODE_UNKNOWN: return "UNKNOWN";
+            case ImsMmTelManager.WIFI_MODE_WIFI_ONLY: return "WIFI_ONLY";
+            case ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED: return "CELLULAR_PREFERRED";
+            case ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED: return "WIFI_PREFERRED";
+            default:
+                return "UNKNOWN(" + mode + ")";
+        }
+    }
 }
diff --git a/tests/EnforcePermission/OWNERS b/tests/EnforcePermission/OWNERS
new file mode 100644
index 0000000..39550a3
--- /dev/null
+++ b/tests/EnforcePermission/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 315013
+tweek@google.com
+brufino@google.com
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index aacc17a4..3361502 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -113,4 +113,18 @@
         }
         return false
     }
+
+    fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
+        val button = uiDevice.wait(Until.findObject(By.res(getPackage(),
+                "toggle_fixed_portrait_btn")), FIND_TIMEOUT)
+        require(button != null) {
+            "Button not found, this usually happens when the device " +
+                    "was left in an unknown state (e.g. Screen turned off)"
+        }
+        button.click()
+        mInstrumentation.waitForIdleSync()
+        // Ensure app relaunching transition finish and the IME has shown
+        wmHelper.waitForAppTransitionIdle()
+        wmHelper.waitImeShown()
+    }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
new file mode 100644
index 0000000..3b3bce6
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group2
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.traces.region.RegionSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test IME window shown on the app with fixing portrait orientation.
+ * To run this test: `atest FlickerTests:OpenImeWindowToFixedPortraitAppTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group2
+class OpenImeWindowToFixedPortraitAppTest (private val testSpec: FlickerTestParameter) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            setup {
+                eachRun {
+                    testApp.launchViaIntent(wmHelper)
+                    testApp.openIME(device, wmHelper)
+                    // Enable letterbox when the app calls setRequestedOrientation
+                    device.executeShellCommand("cmd window set-ignore-orientation-request true")
+                }
+            }
+            transitions {
+                testApp.toggleFixPortraitOrientation(wmHelper)
+            }
+            teardown {
+                eachRun {
+                    testApp.exit()
+                    device.executeShellCommand("cmd window set-ignore-orientation-request false")
+                }
+            }
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun imeLayerVisibleStart() {
+        testSpec.assertLayersStart {
+            this.isVisible(FlickerComponentName.IME)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun imeLayerExistsEnd() {
+        testSpec.assertLayersEnd {
+            this.isVisible(FlickerComponentName.IME)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun imeLayerVisibleRegionKeepsTheSame() {
+        var imeLayerVisibleRegionBeforeTransition: RegionSubject? = null
+        testSpec.assertLayersStart {
+            imeLayerVisibleRegionBeforeTransition = this.visibleRegion(FlickerComponentName.IME)
+        }
+        testSpec.assertLayersEnd {
+            this.visibleRegion(FlickerComponentName.IME)
+                    .coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun appWindowWithLetterboxCoversExactlyOnScreen() {
+        val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+        testSpec.assertLayersEnd {
+            this.visibleRegion(testApp.component, FlickerComponentName.LETTERBOX)
+                    .coversExactly(displayBounds)
+        }
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(
+                            supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270),
+                            supportedNavigationModes = listOf(
+                                    WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+                                    WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+                            )
+                    )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
index a8cbc5d..e05312f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
@@ -18,12 +18,15 @@
 
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.RequiresDevice
+import android.platform.test.rule.SettingOverrideRule
+import android.provider.Settings
 import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.ClassRule
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -98,5 +101,16 @@
             return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance()
                     .getConfigNonRotationTests(repetitions = 3)
         }
+
+        /**
+         * Ensures that posted notifications will be visible on the lockscreen and not
+         * suppressed due to being marked as seen.
+         */
+        @ClassRule
+        @JvmField
+        val disableUnseenNotifFilterRule = SettingOverrideRule(
+            Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            /* value= */ "0",
+        )
     }
-}
\ No newline at end of file
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
index cd8dea0..fcec79f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
@@ -18,6 +18,8 @@
 
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.RequiresDevice
+import android.platform.test.rule.SettingOverrideRule
+import android.provider.Settings
 import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
@@ -25,6 +27,7 @@
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.ClassRule
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -124,5 +127,16 @@
             return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance()
                     .getConfigNonRotationTests(repetitions = 3)
         }
+
+        /**
+         * Ensures that posted notifications will be visible on the lockscreen and not
+         * suppressed due to being marked as seen.
+         */
+        @ClassRule
+        @JvmField
+        val disableUnseenNotifFilterRule = SettingOverrideRule(
+            Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            /* value= */ "0",
+        )
     }
-}
\ No newline at end of file
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index b8ef195..efd80f2 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -45,7 +45,7 @@
              android:theme="@style/CutoutShortEdges"
              android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus"
              android:windowSoftInputMode="stateVisible"
-             android:configChanges="orientation|screenSize"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
              android:label="ImeAppAutoFocus"
              android:exported="true">
             <intent-filter>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index baaf707..e71fe80 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -26,14 +26,27 @@
               android:layout_width="match_parent"
 	      android:imeOptions="flagNoExtractUi"
               android:inputType="text"/>
-    <Button
-        android:id="@+id/finish_activity_btn"
+    <LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Finish activity" />
-    <Button
-        android:id="@+id/start_dialog_themed_activity_btn"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Start dialog themed activity" />
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/finish_activity_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Finish activity" />
+        <Button
+            android:id="@+id/start_dialog_themed_activity_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Dialog activity" />
+        <ToggleButton
+            android:id="@+id/toggle_fixed_portrait_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textOn="Portrait (On)"
+            android:textOff="Portrait (Off)"
+        />
+    </LinearLayout>
 </LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
index bb200f1..7ee8deb 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
@@ -16,21 +16,29 @@
 
 package com.android.server.wm.flicker.testapp;
 
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
 import android.content.Intent;
 import android.widget.Button;
 import android.widget.EditText;
+import android.widget.ToggleButton;
 
 public class ImeActivityAutoFocus extends ImeActivity {
-
     @Override
     protected void onStart() {
         super.onStart();
 
-        EditText editTextField = findViewById(R.id.plain_text_input);
-        editTextField.requestFocus();
-
         Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
         startThemedActivityButton.setOnClickListener(
                 button -> startActivity(new Intent(this, DialogThemedActivity.class)));
+
+        ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
+        toggleFixedPortraitButton.setOnCheckedChangeListener(
+                (button, isChecked) -> setRequestedOrientation(
+                        isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
+
+        EditText editTextField = findViewById(R.id.plain_text_input);
+        editTextField.requestFocus();
     }
 }
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,